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 && (
+
+ )}
+
+ {zipUrl && !loading && (
+
+
+
+ ZIP ready for download
+
+
+
+ DOWNLOAD ZIP
+
+
+ )}
+
+
+
+
+ );
+}