From b89ce6e645fc467e02a7c5f7dafe297fdb12a377 Mon Sep 17 00:00:00 2001 From: Sumit Kumar Date: Thu, 6 Aug 2026 17:43:31 +0530 Subject: [PATCH] fix(batch): give colliding PDF names unique ZIP folders Co-authored-by: Cursor --- Readme.md | 1 + frontend/src/pages/PdfPngBatch.tsx | 5 ++++- .../src/utils/uniqueZipFolderName.test.ts | 20 +++++++++++++++++ frontend/src/utils/uniqueZipFolderName.ts | 22 +++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 frontend/src/utils/uniqueZipFolderName.test.ts create mode 100644 frontend/src/utils/uniqueZipFolderName.ts diff --git a/Readme.md b/Readme.md index 3018606..e91e463 100644 --- a/Readme.md +++ b/Readme.md @@ -11,6 +11,7 @@ This project is a comprehensive full‑stack web app for doing simple, local fil **PDF Tools:** - Convert PDF pages to PNG (single page, range, or all pages) +- Batch-convert multiple PDFs to PNG with unique ZIP folders for same-named files - Merge multiple PDF files into one document - Split a PDF by extracting a page range into a new document - Convert PDF to DOCX diff --git a/frontend/src/pages/PdfPngBatch.tsx b/frontend/src/pages/PdfPngBatch.tsx index bd8aa09..ad60b74 100644 --- a/frontend/src/pages/PdfPngBatch.tsx +++ b/frontend/src/pages/PdfPngBatch.tsx @@ -17,6 +17,7 @@ import { import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; import PrimaryButton from "../components/UI/PrimaryButton"; +import { uniqueZipFolderName } from "../utils/uniqueZipFolderName"; pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker; @@ -120,6 +121,7 @@ export default function PdfPngBatch() { const zip = new JSZip(); let done = 0; + const usedFolderNames = new Set(); try { for (let i = 0; i < files.length; i++) { @@ -129,7 +131,8 @@ export default function PdfPngBatch() { 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; + const folderName = uniqueZipFolderName(result.name, usedFolderNames); + const folder = files.length > 1 ? zip.folder(folderName) : zip; for (const p of result.pages) { folder.file(p.name, p.blob); } diff --git a/frontend/src/utils/uniqueZipFolderName.test.ts b/frontend/src/utils/uniqueZipFolderName.test.ts new file mode 100644 index 0000000..964adf4 --- /dev/null +++ b/frontend/src/utils/uniqueZipFolderName.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; + +import { uniqueZipFolderName } from "./uniqueZipFolderName"; + +describe("uniqueZipFolderName", () => { + it("keeps the first name and suffixes later collisions", () => { + const used = new Set(); + + expect(uniqueZipFolderName("report", used)).toBe("report"); + expect(uniqueZipFolderName("report", used)).toBe("report-2"); + expect(uniqueZipFolderName("report", used)).toBe("report-3"); + expect(uniqueZipFolderName("invoice", used)).toBe("invoice"); + }); + + it("skips suffixes that are already claimed", () => { + const used = new Set(["report", "report-2"]); + + expect(uniqueZipFolderName("report", used)).toBe("report-3"); + }); +}); diff --git a/frontend/src/utils/uniqueZipFolderName.ts b/frontend/src/utils/uniqueZipFolderName.ts new file mode 100644 index 0000000..67ff1ca --- /dev/null +++ b/frontend/src/utils/uniqueZipFolderName.ts @@ -0,0 +1,22 @@ +/** + * Pick a ZIP folder name that does not collide with names already claimed + * in the current archive. First use keeps the base name; later collisions + * get a numeric suffix (`report`, `report-2`, ...). + */ +export function uniqueZipFolderName(baseName: string, usedNames: Set) { + const normalized = baseName.trim() || "document"; + if (!usedNames.has(normalized)) { + usedNames.add(normalized); + return normalized; + } + + let suffix = 2; + let candidate = `${normalized}-${suffix}`; + while (usedNames.has(candidate)) { + suffix += 1; + candidate = `${normalized}-${suffix}`; + } + + usedNames.add(candidate); + return candidate; +}