Enhance PDF handling and update dependencies#40
Conversation
… with drag interactions
…ime type handling
There was a problem hiding this comment.
Pull request overview
This PR migrates the app’s PDF manipulation layer from pdf-lib to @pdfme/pdf-lib, upgrades several key dependencies (React, lucide-react, Vite+ core), and introduces new PDF capabilities (embedded file attachments and a rectangle/badge stamp style) with corresponding UI and documentation updates.
Changes:
- Replace
pdf-libimports/usages with@pdfme/pdf-libacross utilities and tools, plus dependency upgrades inpackage.json/pnpm-lock.yaml. - Add a new “File Attachments” tool (view/add/extract/remove embedded files) backed by new PDF operations.
- Extend the Stamp tool with a new “rectangle/badge” mode and refresh inspector metadata icons + minor drag UX tweaks.
Reviewed changes
Copilot reviewed 18 out of 21 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/pdf-security.ts | Switch low-level PDF object imports to @pdfme/pdf-lib. |
| src/utils/pdf-operations.ts | Migrate PDF lib import; adjust metadata loading; add attachment operations and rectangle stamp implementation. |
| src/types.ts | Add new tool id "file-attachment". |
| src/tools/StampPdf.tsx | Add rectangle/badge stamp mode and migrate dynamic import to @pdfme/pdf-lib. |
| src/tools/PdfInspector.tsx | Update metadata row icons for clearer labeling. |
| src/tools/HeaderFooter.tsx | Migrate dynamic import to @pdfme/pdf-lib. |
| src/tools/FillPdfForm.tsx | Migrate dynamic import to @pdfme/pdf-lib. |
| src/tools/FileAttachment.tsx | New tool UI for listing/adding/extracting/removing PDF embedded files. |
| src/tools/CropPages.tsx | Migrate dynamic import to @pdfme/pdf-lib. |
| src/tools/ContactSheet.tsx | Migrate dynamic import to @pdfme/pdf-lib. |
| src/tools/BatesNumbering.tsx | Migrate dynamic import to @pdfme/pdf-lib. |
| src/tools/AddSignature.tsx | Migrate dynamic import to @pdfme/pdf-lib. |
| src/tools/AddPageNumbers.tsx | Migrate dynamic import to @pdfme/pdf-lib. |
| src/tools/AddBookmarks.tsx | Migrate dynamic import to @pdfme/pdf-lib. |
| src/hooks/useSortableDrag.ts | Add iOS touch-callout/user-select prevention styles for drag UX. |
| src/App.tsx | Register new File Attachments tool and route/component mapping. |
| README.md | Document library migration and list File Attachments as a feature. |
| public/icons/og-image.svg | Update OG image layout/text for marketing preview. |
| package.json | Replace pdf-lib with @pdfme/pdf-lib and bump core deps. |
| pnpm-lock.yaml | Lockfile updates reflecting new/updated dependencies. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| const pdf = await PDFDocument.load(arrayBuffer, { updateMetadata: false }); | ||
|
|
||
| pdf.setTitle(metadata.title); | ||
| pdf.setAuthor(metadata.author); | ||
| pdf.setSubject(metadata.subject); | ||
| pdf.setKeywords(metadata.keywords.split(",").map((k) => k.trim())); | ||
| pdf.setKeywords([metadata.keywords]); | ||
| pdf.setCreator(metadata.creator); | ||
| pdf.setProducer(metadata.producer); |
There was a problem hiding this comment.
setPdfMetadata now calls pdf.setKeywords([metadata.keywords]), but the UI describes this field as “Comma-separated keywords” (see EditMetadata tool) and the previous behavior split/trimmed the string. As written, a user-entered "a, b" will be stored as a single keyword value including the comma. Consider restoring comma-splitting (with trimming and empty filtering) or otherwise aligning storage format with the UI and getPdfMetadata expectations.
| if (!stream || !("getContents" in stream)) continue; | ||
|
|
||
| const data = (stream as unknown as { getContents(): Uint8Array }).getContents(); | ||
| const streamDict = stream as unknown as PDFDict; | ||
| const paramsDict = streamDict.lookup?.(PDFName.of("Params"), PDFDict); | ||
| const sizeNum = paramsDict?.lookup(PDFName.of("Size"), PDFNumber); | ||
|
|
||
| const subtypeObj = streamDict.lookup?.(PDFName.of("Subtype"), PDFName); |
There was a problem hiding this comment.
In listPdfAttachments, stream is treated as if it were a PDFDict (cast to PDFDict and then lookup is called). For embedded files, the /EF entry typically points to a stream object (e.g. PDFRawStream/PDFStream) whose dictionary is accessed via stream.dict (see how streams are handled in pdf-security.ts). As written, streamDict.lookup?.(...) will likely be undefined at runtime, and MIME type / size extraction will fail. Consider narrowing stream to the proper stream type and reading Params/Subtype from its stream dictionary.
| if (!stream || !("getContents" in stream)) continue; | |
| const data = (stream as unknown as { getContents(): Uint8Array }).getContents(); | |
| const streamDict = stream as unknown as PDFDict; | |
| const paramsDict = streamDict.lookup?.(PDFName.of("Params"), PDFDict); | |
| const sizeNum = paramsDict?.lookup(PDFName.of("Size"), PDFNumber); | |
| const subtypeObj = streamDict.lookup?.(PDFName.of("Subtype"), PDFName); | |
| if ( | |
| !stream || | |
| !("getContents" in stream) || | |
| !("dict" in stream) || | |
| !(stream.dict instanceof PDFDict) | |
| ) { | |
| continue; | |
| } | |
| const embeddedFileStream = stream as unknown as { | |
| getContents(): Uint8Array; | |
| dict: PDFDict; | |
| }; | |
| const data = embeddedFileStream.getContents(); | |
| const streamDict = embeddedFileStream.dict; | |
| const paramsDict = streamDict.lookup(PDFName.of("Params"), PDFDict); | |
| const sizeNum = paramsDict?.lookup(PDFName.of("Size"), PDFNumber); | |
| const subtypeObj = streamDict.lookup(PDFName.of("Subtype"), PDFName); |
| const namesDict = catalog.lookup(PDFName.of("Names")); | ||
| if (!(namesDict instanceof PDFDict)) return []; | ||
|
|
||
| const efDict = namesDict.lookup(PDFName.of("EmbeddedFiles")); | ||
| if (!(efDict instanceof PDFDict)) return []; | ||
|
|
||
| const namesArray = efDict.lookup(PDFName.of("Names")); | ||
| if (!(namesArray instanceof PDFArray)) return []; | ||
|
|
There was a problem hiding this comment.
listPdfAttachments assumes the EmbeddedFiles name tree is always represented as a flat /Names array (/Names -> /EmbeddedFiles -> /Names). Per the PDF spec, name trees can also be represented with /Kids (and /Limits), in which case this will incorrectly return an empty list even when attachments exist. Consider implementing a small recursive name-tree traversal that handles both the /Names leaf form and the /Kids internal-node form.
| const keepIndices: number[] = []; | ||
| for (let i = 0; i < namesArray.size(); i += 2) { | ||
| const nameObj = namesArray.lookup(i); | ||
| const name = | ||
| nameObj instanceof PDFString | ||
| ? nameObj.decodeText() | ||
| : nameObj instanceof PDFName | ||
| ? nameObj.decodeText() | ||
| : ""; | ||
| if (!namesToRemove.has(name)) { |
There was a problem hiding this comment.
removeAttachmentsFromPdf matches entries only against the name-tree key (nameObj). However, listPdfAttachments prefers the filespec’s /UF or /F when presenting the attachment name to the user. If the displayed name differs from the name-tree key, the UI may pass a name that this removal logic can never match, so removal will silently do nothing. Consider using the same name-resolution logic as listPdfAttachments (and/or matching against both the tree key and /UF//F).
| const keepIndices: number[] = []; | |
| for (let i = 0; i < namesArray.size(); i += 2) { | |
| const nameObj = namesArray.lookup(i); | |
| const name = | |
| nameObj instanceof PDFString | |
| ? nameObj.decodeText() | |
| : nameObj instanceof PDFName | |
| ? nameObj.decodeText() | |
| : ""; | |
| if (!namesToRemove.has(name)) { | |
| const decodePdfText = (value: unknown): string => { | |
| if (value instanceof PDFString || value instanceof PDFName) { | |
| return value.decodeText(); | |
| } | |
| return ""; | |
| }; | |
| const keepIndices: number[] = []; | |
| for (let i = 0; i < namesArray.size(); i += 2) { | |
| const nameObj = namesArray.lookup(i); | |
| const fileSpecObj = namesArray.lookup(i + 1); | |
| const candidateNames = new Set<string>(); | |
| const treeKeyName = decodePdfText(nameObj); | |
| if (treeKeyName) { | |
| candidateNames.add(treeKeyName); | |
| } | |
| if (fileSpecObj instanceof PDFDict) { | |
| const unicodeFileName = decodePdfText(fileSpecObj.lookup(PDFName.of("UF"))); | |
| const fileName = decodePdfText(fileSpecObj.lookup(PDFName.of("F"))); | |
| if (unicodeFileName) { | |
| candidateNames.add(unicodeFileName); | |
| } | |
| if (fileName) { | |
| candidateNames.add(fileName); | |
| } | |
| } | |
| const shouldRemove = Array.from(candidateNames).some((name) => | |
| namesToRemove.has(name), | |
| ); | |
| if (!shouldRemove) { |
| const namesDict = catalog.lookup(PDFName.of("Names")); | ||
| if (!(namesDict instanceof PDFDict)) return pdf.save(); | ||
|
|
||
| const efDict = namesDict.lookup(PDFName.of("EmbeddedFiles")); | ||
| if (!(efDict instanceof PDFDict)) return pdf.save(); | ||
|
|
||
| const namesArray = efDict.lookup(PDFName.of("Names")); | ||
| if (!(namesArray instanceof PDFArray)) return pdf.save(); | ||
|
|
There was a problem hiding this comment.
removeAttachmentsFromPdf only supports the leaf form of the EmbeddedFiles name tree (/Names array). If a PDF uses the internal-node form with /Kids (and /Limits), this function won’t find or remove attachments. Consider reusing a shared name-tree traversal helper so both listing and removal can handle /Kids as well as /Names.
This pull request updates the project's PDF manipulation library to use
@pdfme/pdf-libin place ofpdf-lib, and makes several dependency upgrades throughout the codebase. It also updates documentation to reflect the new library and adds a mention of file attachment features.PDF Library Migration:
pdf-libwith@pdfme/pdf-libas the project's PDF manipulation library in bothpackage.jsonandpnpm-lock.yaml, and updates all related documentation references. This impacts all PDF operations such as merging, splitting, rotation, watermarking, and more. [1] [2] [3] [4] [5] [6]Dependency Upgrades:
lucide-react,react,react-dom, andvite, as well as various transitive dependencies inpnpm-lock.yamlsuch asbaseline-browser-mapping,call-bind,caniuse-lite,electron-to-chromium,defu,es-abstract,lru-cache,postcss,side-channel-list,tinyexec, andtinyglobby. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19]Documentation Improvements:
README.md.@pdfme/pdf-libinstead of the oldpdf-libpackage, ensuring consistency and clarity for users and contributors. [1] [2]New Transitive Dependencies:
@pdfme/pdf-lib, such asnode-html-better-parserandpako@2.1.0, as reflected in the lockfile. [1] [2] [3]These changes ensure the project uses a maintained and compatible PDF library, improve feature clarity, and keep the dependency tree up to date.