diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index abcd8d2..e407637 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -30,6 +30,7 @@ const NotFound = lazy(() => import("./pages/NotFound")); const DocxPdf = lazy(() => import("./pages/DocxPdf")); const PdfSplit = lazy(() => import("./pages/PdfSplit")); const PdfRotateFlip = lazy(() => import("./pages/PdfRotateFlip")); +const PdfPngBatch = lazy(() => import("./pages/PdfPngBatch")); const PDFWatermark = lazy(() => import("./pages/PDFWatermark")); const ImageOCR = lazy(() => import("./pages/ImageOCR")); const ImageWatermark = lazy(() => import("./pages/ImageWatermark")); @@ -72,6 +73,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/data/toolsData.jsx b/frontend/src/data/toolsData.jsx index 3fdd97d..5f42cf6 100644 --- a/frontend/src/data/toolsData.jsx +++ b/frontend/src/data/toolsData.jsx @@ -19,6 +19,7 @@ import { Tags, Type, BookOpen, + Layers, } from "lucide-react"; const tools = [ @@ -33,6 +34,16 @@ const tools = [ gradient: "from-amber-500/10 to-orange-500/10", iconGradient: "from-amber-500 to-orange-500", }, + { + id: "pdf-to-png-batch", + name: "Batch PDF to PNG", + category: "PDF Tools", + icon: , + description: "Convert multiple PDF files to PNG images at once and download them as a ZIP.", + path: "/pdf-to-png-batch", + gradient: "from-amber-500/10 to-orange-500/10", + iconGradient: "from-amber-500 to-orange-500", + }, { id: "image-to-pdf", name: "Image to PDF", diff --git a/frontend/src/pages/PdfPngBatch.jsx b/frontend/src/pages/PdfPngBatch.jsx new file mode 100644 index 0000000..43f420f --- /dev/null +++ b/frontend/src/pages/PdfPngBatch.jsx @@ -0,0 +1,352 @@ +import { useState, useRef } from "react"; +import * as pdfjsLib from "pdfjs-dist/legacy/build/pdf"; +import pdfWorker from "pdfjs-dist/legacy/build/pdf.worker.min.mjs?url"; +import JSZip from "jszip"; +import { Toaster, toast } from "sonner"; +// eslint-disable-next-line no-unused-vars +import { motion } from "framer-motion"; +import { + FileText, + Download, + RefreshCcw, + AlertCircle, + CheckCircle2, + Upload, + Trash2, + Files, +} from "lucide-react"; +import { clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker; + +function cn(...inputs) { + return twMerge(clsx(inputs)); +} + +// Convert a single PDF File into one PNG per page. +// Returns { name, pages: [{ name, blob }] }. +async function convertPdfToPngs(file, scale, onProgress) { + const arrayBuffer = await file.arrayBuffer(); + const pdf = await pdfjsLib.getDocument({ data: arrayBuffer, verbosity: 0 }) + .promise; + const baseName = file.name.replace(/\.pdf$/i, ""); + const pages = []; + + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const viewport = page.getViewport({ scale }); + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + canvas.height = viewport.height; + canvas.width = viewport.width; + await page.render({ canvasContext: context, viewport }).promise; + + const blob = await new Promise((resolve) => + canvas.toBlob(resolve, "image/png") + ); + pages.push({ name: `${baseName}-page-${i}.png`, blob }); + if (onProgress) onProgress(i, pdf.numPages); + } + + return { name: baseName, pages }; +} + +export default function PdfPngBatch() { + const [files, setFiles] = useState([]); + const [scale, setScale] = useState(2); + const [loading, setLoading] = useState(false); + const [currentFile, setCurrentFile] = useState(null); + const [fileProgress, setFileProgress] = useState(0); // pages done in current file + const [overallProgress, setOverallProgress] = useState(0); // 0-100 across all files + const [error, setError] = useState(null); + const [zipUrl, setZipUrl] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const inputRef = useRef(null); + + const addFiles = (fileList) => { + const pdfs = Array.from(fileList).filter( + (f) => f.type === "application/pdf" || f.name.toLowerCase().endsWith(".pdf") + ); + if (pdfs.length === 0) { + setError("Please select PDF files only."); + return; + } + setError(null); + setZipUrl(null); + setFiles((prev) => [...prev, ...pdfs]); + }; + + const removeFile = (idx) => { + setFiles((prev) => prev.filter((_, i) => i !== idx)); + }; + + const clearAll = () => { + setFiles([]); + setError(null); + setZipUrl((url) => { + if (url) URL.revokeObjectURL(url); + return null; + }); + }; + + const runBatch = async () => { + if (files.length === 0 || loading) return; + + setLoading(true); + setError(null); + setZipUrl((url) => { + if (url) URL.revokeObjectURL(url); + return null; + }); + setOverallProgress(0); + + // Pre-compute total page count for an honest overall progress bar. + let totalPages = 0; + const perFileCounts = []; + try { + for (const f of files) { + const buf = await f.arrayBuffer(); + const pdf = await pdfjsLib.getDocument({ data: buf, verbosity: 0 }) + .promise; + perFileCounts.push(pdf.numPages); + totalPages += pdf.numPages; + } + } catch (e) { + setError("Could not read one of the PDFs: " + (e.message || String(e))); + setLoading(false); + return; + } + + const zip = new JSZip(); + let done = 0; + + try { + for (let i = 0; i < files.length; i++) { + setCurrentFile(files[i].name); + setFileProgress(0); + const result = await convertPdfToPngs(files[i], scale, (page, total) => { + setFileProgress(Math.round((page / total) * 100)); + }); + // If a batch contains multiple files, namespace PNGs into a folder per file. + const folder = files.length > 1 ? zip.folder(result.name) : zip; + for (const p of result.pages) { + folder.file(p.name, p.blob); + } + done += perFileCounts[i]; + setOverallProgress(Math.round((done / totalPages) * 100)); + } + + setCurrentFile(null); + const zipBlob = await zip.generateAsync({ type: "blob" }); + setZipUrl(URL.createObjectURL(zipBlob)); + toast.success( + `Batch complete! ${files.length} PDF${files.length > 1 ? "s" : ""} converted to PNGs.` + ); + } catch (e) { + console.error(e); + setError("Batch conversion failed: " + (e.message || String(e))); + toast.error(e.message || "Batch failed"); + } finally { + setLoading(false); + setFileProgress(0); + } + }; + + const totalPdfs = files.length; + + return ( +
+ + + + Batch PDF to PNG + + +

+ Convert multiple PDF files to PNG images at once, then download all the + results as a single ZIP archive. +

+ +
+ {/* Left Panel */} +
+
{ + e.preventDefault(); + setIsDragging(false); + addFiles(e.dataTransfer.files); + }} + onDragOver={(e) => { + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={() => setIsDragging(false)} + onClick={() => inputRef.current?.click()} + className={cn( + "w-full border-2 border-dashed rounded-3xl p-10 flex flex-col items-center justify-center cursor-pointer transition-all duration-300", + isDragging + ? "border-[#4361ee] bg-blue-50 scale-[1.03] shadow-lg" + : "border-slate-200 bg-slate-50/50 hover:border-[#4361ee] hover:bg-white hover:shadow-xl" + )} + > + { + addFiles(e.target.files); + e.target.value = ""; + }} + /> +
+
+ +
+

+ Click or drag & drop multiple PDFs +

+

+ Select several files for bulk conversion +

+
+
+ + {files.length > 0 && ( +
+
+
+ {files.length} file + {files.length > 1 ? "s" : ""} queued +
+ +
+ +
    + {files.map((f, idx) => ( +
  • +
    + +
    +
    +

    + {f.name} +

    +

    + {(f.size / 1024).toFixed(1)} KB +

    +
    + +
  • + ))} +
+
+ )} +
+ + {/* Right Panel */} +
+
+
+ Settings & Convert +
+ + + + + + + {loading && ( +
+ {currentFile && ( +

+ Now: {currentFile}{" "} + ({fileProgress}%) +

+ )} +
+ + Overall + + {overallProgress}% +
+
+ +
+
+ )} + + {error && ( +
+ + {error} +
+ )} + + {zipUrl && !loading && ( +
+
+ + ZIP ready for download +
+ + + DOWNLOAD ZIP + +
+ )} +
+
+
+
+ ); +}