PDF Page Extractor - Select and extract specific pages into a new PDF.
import React, { useState, useRef, useEffect } from "react";
import { createRoot } from "react-dom/client";
import * as pdfjs from "pdfjs-dist";
import { PDFDocument } from "pdf-lib";
// Setup PDF.js Worker using the Typebulb Proxy Pattern.
// Use pdfjs.version so the worker always matches the loaded API version.
pdfjs.GlobalWorkerOptions.workerSrc = tb.proxy(`https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`);
const App = () => {
const [file, setFile] = useState<File | null>(null);
const [pages, setPages] = useState<{ id: number; url: string }[]>([]);
const [selectedPages, setSelectedPages] = useState<Set<number>>(new Set());
const [loading, setLoading] = useState(false);
const [processing, setProcessing] = useState(false);
const [status, setStatus] = useState("Drop a PDF or click to upload");
const [isDragging, setIsDragging] = useState(false);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
processFile(e.target.files?.[0]);
};
const processFile = async (selectedFile?: File) => {
if (!selectedFile || selectedFile.type !== "application/pdf") return;
setFile(selectedFile);
setLoading(true);
setPages([]);
setSelectedPages(new Set());
setStatus("Loading PDF...");
try {
const arrayBuffer = await selectedFile.arrayBuffer();
const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 0.3 }); // Small scale for thumbnails
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context!, viewport, canvas }).promise;
const url = canvas.toDataURL();
setPages(prev => [...prev, { id: i, url }]);
setStatus(`Rendering page ${i}/${pdf.numPages}...`);
}
setStatus("Select pages to extract");
} catch (err) {
console.error(err);
setStatus("Error loading PDF");
} finally {
setLoading(false);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
processFile(e.dataTransfer.files?.[0]);
};
const togglePage = (index: number) => {
const next = new Set(selectedPages);
if (next.has(index)) next.delete(index);
else next.add(index);
setSelectedPages(next);
};
const selectAll = () => {
setSelectedPages(new Set(pages.map(p => p.id)));
};
const handleExport = async () => {
if (!file || selectedPages.size === 0) return;
setProcessing(true);
setStatus("Generating new PDF...");
try {
const existingPdfBytes = await file.arrayBuffer();
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const newPdfDoc = await PDFDocument.create();
// Page indices are 0-based in pdf-lib
const indices = Array.from(selectedPages).sort((a, b) => a - b).map(i => i - 1);
const copiedPages = await newPdfDoc.copyPages(pdfDoc, indices);
copiedPages.forEach((page) => newPdfDoc.addPage(page));
const pdfBytes = await newPdfDoc.save();
const blob = new Blob([pdfBytes], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `extracted_pages_${file.name}`;
link.click();
setStatus("Export successful!");
} catch (err) {
console.error(err);
setStatus("Export failed");
} finally {
setProcessing(false);
}
};
const ua = navigator.userAgent;
const isWebView = (/iPhone|iPad|iPod/i.test(ua) && !ua.includes('Safari')) || /; wv\)/.test(ua);
if (isWebView) return (
<div className="container">
<div className="in-app-banner">
Your in-app browser can't download files. Please open this page directly in your browser.
<button className="copy-link-btn" onClick={async () => tb.copy(await tb.url())}>
Copy Link
</button>
</div>
</div>
);
return (
<div className="container">
<header className="main-header">
<h1>PDF Page Extractor</h1>
<p className="subtitle">Select and extract specific pages into a new PDF. Runs entirely in your browser.</p>
</header>
<div
className={`upload-section ${isDragging ? 'dragging' : ''}`}
onDragOver={handleDragOver}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
>
<label className={`upload-btn ${loading ? 'loading' : ''} ${file ? 'has-file' : ''}`}>
{file ? `File: ${file.name}` : "Choose PDF File"}
<input type="file" accept=".pdf" onChange={handleFileChange} disabled={loading} />
</label>
<span className="status-badge">{status}</span>
</div>
{pages.length > 0 && (
<div className="workspace animate-in">
<div className="toolbar">
<button
className="primary-btn"
onClick={handleExport}
disabled={selectedPages.size === 0 || processing}
>
{processing ? "Processing..." : `Export ${selectedPages.size} Selected Pages`}
</button>
<button className="secondary-btn" onClick={selectAll}>
Select All
</button>
<button className="secondary-btn" onClick={() => setSelectedPages(new Set())}>
Clear Selection
</button>
</div>
<div className="thumbnail-grid">
{pages.map((page) => (
<div
key={page.id}
className={`page-card ${selectedPages.has(page.id) ? 'selected' : ''}`}
onClick={() => togglePage(page.id)}
>
<div className="page-number">{page.id}</div>
<img src={page.url} alt={`Page ${page.id}`} />
<div className="selection-indicator">
<div className="check">✓</div>
</div>
</div>
))}
</div>
</div>
)}
{!file && !loading && (
<div className="placeholder-card">
<div className="icon">📄</div>
<h3>No PDF Loaded</h3>
<p>Upload a document to view and extract specific pages locally in your browser.</p>
</div>
)}
</div>
);
};
const container = document.getElementById("root");
const root = createRoot(container!);
root.render(<App />);