PDF Extractor

PDF Page Extractor - Select and extract specific pages into a new PDF.

code.tsx

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 />);

Markdown source · More bulbs by samples · Typebulb home