mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
feat(frontend): Upgrade embedPDF to v2.6.0 and migrate to pdf-lib fork, fix attachment/bookmark panel (#5723)
# Description of Changes Upgrades embedPDF from v2.5.0 to v2.6.0 and migrates from unmaintained pdf-lib to @cantoo/pdf-lib fork. Adds defensive error handling for malformed PDFs and improves bridge lifecycle management. ### Changes **Dependencies** - Upgrade all @embedpdf/* packages from ^2.5.0 to ^2.6.0 - Replace pdf-lib with @cantoo/pdf-lib (maintained fork with better TypeScript support) **PDF Viewer Infrastructure (attachment/bookmark fix)** - Add useDocumentReady hook to track document lifecycle across bridges - Implement defensive bridge cleanup to prevent stale registrations - Fix race condition in document ready state detection by subscribing to events before checking state **Link Extraction (updated to cantoo/pdf-lib)** - Add graceful error handling for PDFs with invalid catalog structures - Extract enhanced link metadata (tooltips, colors, border styles, highlight modes) - Return empty results instead of throwing on malformed PDFs - Add validation for link creation (destination page bounds, rect dimensions, color values) **Signature Flattening (updated to cantoo/pdf-lib)** - Improve SVG embedding with three-tier fallback strategy (native vector, rasterized PNG, placeholder) - Add proper Unicode handling for PDF form tooltips via PDFString.decodeText() - Extract SVG utilities into cleaner strategy pattern **Form Field Processing (updated to cantoo/pdf-lib)** - Add support for display labels vs export values in dropdown/list fields per PDF spec 12.7.4.4 - Implement caching for expensive field property lookups - Add proper handling of malformed /Opt arrays <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [X] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [X] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [X] I have performed a self-review of my own code - [X] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [X] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
This commit is contained in:
parent
b8ce4e47c1
commit
0a1d2effdc
430
frontend/package-lock.json
generated
430
frontend/package-lock.json
generated
@ -10,30 +10,31 @@
|
||||
"license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@cantoo/pdf-lib": "^2.5.3",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@embedpdf/core": "^2.5.0",
|
||||
"@embedpdf/engines": "^2.5.0",
|
||||
"@embedpdf/models": "^2.5.0",
|
||||
"@embedpdf/plugin-annotation": "^2.5.0",
|
||||
"@embedpdf/plugin-attachment": "^2.5.0",
|
||||
"@embedpdf/plugin-bookmark": "^2.5.0",
|
||||
"@embedpdf/plugin-document-manager": "^2.5.0",
|
||||
"@embedpdf/plugin-export": "^2.5.0",
|
||||
"@embedpdf/plugin-history": "^2.5.0",
|
||||
"@embedpdf/plugin-interaction-manager": "^2.5.0",
|
||||
"@embedpdf/plugin-pan": "^2.5.0",
|
||||
"@embedpdf/plugin-print": "^2.5.0",
|
||||
"@embedpdf/plugin-redaction": "^2.5.0",
|
||||
"@embedpdf/plugin-render": "^2.5.0",
|
||||
"@embedpdf/plugin-rotate": "^2.5.0",
|
||||
"@embedpdf/plugin-scroll": "^2.5.0",
|
||||
"@embedpdf/plugin-search": "^2.5.0",
|
||||
"@embedpdf/plugin-selection": "^2.5.0",
|
||||
"@embedpdf/plugin-spread": "^2.5.0",
|
||||
"@embedpdf/plugin-thumbnail": "^2.5.0",
|
||||
"@embedpdf/plugin-tiling": "^2.5.0",
|
||||
"@embedpdf/plugin-viewport": "^2.5.0",
|
||||
"@embedpdf/plugin-zoom": "^2.5.0",
|
||||
"@embedpdf/core": "^2.6.0",
|
||||
"@embedpdf/engines": "^2.6.0",
|
||||
"@embedpdf/models": "^2.6.0",
|
||||
"@embedpdf/plugin-annotation": "^2.6.0",
|
||||
"@embedpdf/plugin-attachment": "^2.6.0",
|
||||
"@embedpdf/plugin-bookmark": "^2.6.0",
|
||||
"@embedpdf/plugin-document-manager": "^2.6.0",
|
||||
"@embedpdf/plugin-export": "^2.6.0",
|
||||
"@embedpdf/plugin-history": "^2.6.0",
|
||||
"@embedpdf/plugin-interaction-manager": "^2.6.0",
|
||||
"@embedpdf/plugin-pan": "^2.6.0",
|
||||
"@embedpdf/plugin-print": "^2.6.0",
|
||||
"@embedpdf/plugin-redaction": "^2.6.0",
|
||||
"@embedpdf/plugin-render": "^2.6.0",
|
||||
"@embedpdf/plugin-rotate": "^2.6.0",
|
||||
"@embedpdf/plugin-scroll": "^2.6.0",
|
||||
"@embedpdf/plugin-search": "^2.6.0",
|
||||
"@embedpdf/plugin-selection": "^2.6.0",
|
||||
"@embedpdf/plugin-spread": "^2.6.0",
|
||||
"@embedpdf/plugin-thumbnail": "^2.6.0",
|
||||
"@embedpdf/plugin-tiling": "^2.6.0",
|
||||
"@embedpdf/plugin-viewport": "^2.6.0",
|
||||
"@embedpdf/plugin-zoom": "^2.6.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
@ -61,7 +62,6 @@
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"jszip": "^3.10.1",
|
||||
"license-report": "^6.8.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^5.4.149",
|
||||
"peerjs": "^1.5.5",
|
||||
"posthog-js": "^1.268.0",
|
||||
@ -366,6 +366,21 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@cantoo/pdf-lib": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@cantoo/pdf-lib/-/pdf-lib-2.5.3.tgz",
|
||||
"integrity": "sha512-SBQp8i/XdWNUhLutn5P67Pwj4X9vU046BRpfOMODJZuYVrgChtsTfgdnlW2O7x8gdXs8j7NoTaWI/b78E2oVmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pdf-lib/standard-fonts": "^1.0.0",
|
||||
"@pdf-lib/upng": "^1.0.1",
|
||||
"color": "^4.2.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"node-html-better-parser": ">=1.4.0",
|
||||
"pako": "^1.0.11",
|
||||
"tslib": ">=2"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
@ -555,13 +570,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/core": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.5.0.tgz",
|
||||
"integrity": "sha512-nI7GnA5xCNtJHAdKBLPKJVvi4+yAKjy1sysaDf+qp+z3D81Hy8oAcl///QTaZ9ob0SL2jyqi3x//hKl0Rwmgrw==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.6.0.tgz",
|
||||
"integrity": "sha512-859GUvZ3BLpJuKTiwcPPMNn9CSlMaPjQ4yXnyQRngfbvDAiijIIpVLaC98B08Nx6QsUcD3cs/6+wkB888lNsDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/engines": "2.5.0",
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/engines": "2.6.0",
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
@ -572,9 +587,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/engines": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.5.0.tgz",
|
||||
"integrity": "sha512-SEknNmQrYvkAZgJllRKXuvXSrHSndDQsr7b3mrIVa9bzV6TeZua0a/YUlvI3/jf74Sdajru3XKPe22iHEOH4Zg==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.6.0.tgz",
|
||||
"integrity": "sha512-zW3927u0wbFBD2tQLWbE45DEBIMkZyN7n5O2p70er6u7mP1XYEz7Ud9NxcPL/3b5MzDfPBTSyxM3T12e+ZeAxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/fonts-arabic": "1.0.0",
|
||||
@ -584,8 +599,8 @@
|
||||
"@embedpdf/fonts-latin": "1.0.0",
|
||||
"@embedpdf/fonts-sc": "1.0.0",
|
||||
"@embedpdf/fonts-tc": "1.0.0",
|
||||
"@embedpdf/models": "2.5.0",
|
||||
"@embedpdf/pdfium": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0",
|
||||
"@embedpdf/pdfium": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
@ -638,31 +653,31 @@
|
||||
"license": "OFL-1.1"
|
||||
},
|
||||
"node_modules/@embedpdf/models": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.5.0.tgz",
|
||||
"integrity": "sha512-wu7XgargYBQEh46hVnfsmkTF6TvuoP9nAkTASR60s5ourjlT12qL9RiFLpwGkOBfs8E58h8V5hkgKsra5t03Lw==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.6.0.tgz",
|
||||
"integrity": "sha512-6zuoJE79WXyRXKhJXhl+8p4njuC1nxPpKYRIs54PRLgTkHOLaou+G+ZunEd99XOoVssHLCjxWBUpg46ihQwXDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@embedpdf/pdfium": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.5.0.tgz",
|
||||
"integrity": "sha512-2VEO4cNZsV8ig9upS+C+x3Tb58aqNxiAdaUMlD2ZZT8FgszhsV9xMyEuM2maFRdjeT7EO37FtzYBdXc/K67ivA==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.6.0.tgz",
|
||||
"integrity": "sha512-eYXU1VvVI0e9OqOzvsTcsU6YSLq9F7jcAiIbtMB+NxApvvH3kHz3FPEcf8ha2ZiLftF5OAD8K89SSE5GLE6t1A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-annotation": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.5.0.tgz",
|
||||
"integrity": "sha512-S5zCeWU3hM9jrnaGuW5RAXt+AzXXvQbFtAdCtxHW1hFADiZ97FKr8KS9MGCkkj6C9madtZP6iUJikvnhoLCABQ==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.6.0.tgz",
|
||||
"integrity": "sha512-FJgGy6lhKrWsiJjh7jZ92NwMBob5GOwfYejQl28JFk6muEQORLtysz5gaeyMpMIyxnfjlf9Eqv8Z6LBBfGLGOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0",
|
||||
"@embedpdf/utils": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0",
|
||||
"@embedpdf/utils": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/plugin-history": "2.5.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.5.0",
|
||||
"@embedpdf/plugin-selection": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"@embedpdf/plugin-history": "2.6.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.6.0",
|
||||
"@embedpdf/plugin-selection": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -671,15 +686,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-attachment": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-attachment/-/plugin-attachment-2.5.0.tgz",
|
||||
"integrity": "sha512-dVVnklI2V1Tsnkf2Ob1PY/R8U6bImhaYwiWm8TwJnNzXXT3aTHQ81lGQOv+pIA9f02L8Y4J5OOKkVrKIpqBHug==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-attachment/-/plugin-attachment-2.6.0.tgz",
|
||||
"integrity": "sha512-6UZkj7jFWCruR69OPQFMqbJTgwdra4rnJSBfLA8yLxgz2zTsgt3owjfQDmlJvAQ7G1/rZM2T+EJeuozulj4NoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -688,15 +703,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-bookmark": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.5.0.tgz",
|
||||
"integrity": "sha512-2N5kGoamUrQqWZC5SMWIhdyBHqZN/CdcGf8GVH71FFw3AU6rmZ1AD/AkLzgqoYGIuZFE8ACckdrhtbpsZMmSDQ==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.6.0.tgz",
|
||||
"integrity": "sha512-4JmaFD+gFaLj8Bayi6Fm5qxMoRH+JUy+L3S6xk1KM8YWjJyzsoz9C2mHSXKJ0GBMgiOkjaBuJSWqLgMe/oz7OQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -705,15 +720,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-document-manager": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.5.0.tgz",
|
||||
"integrity": "sha512-I8Z/0B7R/YhtVaJFruwFO+QBLIDmQfHx9WVlrDXWZs68YiGwEbjSyizEIEqtulUJxcXfPs2Tf7oIBbdSuPG2NQ==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.6.0.tgz",
|
||||
"integrity": "sha512-fcx0JKDboEV8eQ4r++ksDHPDuUz40oOmtHDqxYLw6cpos0fqW0p55OP+fKp6LfC/bY7ULVDrmcEQf1cD9Qho4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -722,15 +737,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-export": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.5.0.tgz",
|
||||
"integrity": "sha512-KC9jXqwcxe76QqfxLx0tnrSdFoApTFOpT+dwrvox186uxYKSmSt1JHFWe4THB/A63hCNr8uMwyswYdFO8fWNHw==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.6.0.tgz",
|
||||
"integrity": "sha512-i1Xy7qUipVVLDPnnY22hm3RNMx33lvuNbCuPggql5Ws6WBLG9YhDsK+v0JVe2sDSlifa5SJwuBlMHZWPRTZyxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -739,15 +754,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-history": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.5.0.tgz",
|
||||
"integrity": "sha512-Av9NBSE9Or1Y6cXcNWpx0bBZN3yI4vywa6kSNjhaqOrgpQDWMaTO57eApJpyHzBodqEztY+klE9YJ7MH88zm6w==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.6.0.tgz",
|
||||
"integrity": "sha512-cfVoBjkIbFiRsQu/cwPEi0rrTAF7jriAGzABWawnSTKYEPFrU3LDHO7TewgBz45kHl9pSwvRexaIdTR8ECIKbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -756,15 +771,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-interaction-manager": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.5.0.tgz",
|
||||
"integrity": "sha512-QrmowLVvC5FNZdvVr2kczSDdnHHOuhf+So0VG5Ythts/OL1bIR/0OOpuyJsScTyo5boYnRkXv8yPf8htL57YKQ==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.6.0.tgz",
|
||||
"integrity": "sha512-9bruF6M6GKVdABRTinHsZ+izf2tDQwDEcNI0CHVc5gurrz3CQfAGP2sJkv8uQrXyYTK3zV2Oq6zGknk7Hdx9mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -773,17 +788,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-pan": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.5.0.tgz",
|
||||
"integrity": "sha512-DfdA+hBm9kGYYy7OuJym6azk2h2U/Geirud+tmVzFSL7+OZ3tZ3K9fqj07w66zx0msyUVlYrXzkYSU9NEmwpLA==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.6.0.tgz",
|
||||
"integrity": "sha512-r8AXcXUy6NMYDaQeixScbeFfmZIvWpUUjx3gxjP4J90xfxXnuz/g/lnh4D2DBaiK4mt6crIVBpXU9IUwMIcUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.5.0",
|
||||
"@embedpdf/plugin-viewport": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.6.0",
|
||||
"@embedpdf/plugin-viewport": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -792,15 +807,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-print": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.5.0.tgz",
|
||||
"integrity": "sha512-qejq7/0K9hh3hzop+u+Qmn7ijTqGcDhxaiXoPkyl91CZVOyAD8qMBzWnhC7vRNOB7hcYgBP81uegE3se+EIlcA==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.6.0.tgz",
|
||||
"integrity": "sha512-cgWRqVtRgCCLCn1ViuZEFr+ZJ3QI61/5s9tl3T9x81rwkBN4HT582BYzyRnLBzTMYKxkQZDr1WxfS8ctdlHEUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0",
|
||||
@ -809,20 +824,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-redaction": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.5.0.tgz",
|
||||
"integrity": "sha512-G0cm1hLWi09gU8WV+IShq2XHkmLtEbk+EvD3dIiyJV2kbOjgwGSC2Ezt8br3DzH6R/0bF6RAbDIpFyS2Q0oMfg==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.6.0.tgz",
|
||||
"integrity": "sha512-DdDnmOl9K0N4dpTeUohavxQyrfollhkjT+zdfkna3Fc7F4jfl3Vg6uKoGmT71A+Vp4uTGNLt6cNCscbJW9E9kQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0",
|
||||
"@embedpdf/utils": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0",
|
||||
"@embedpdf/utils": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/plugin-annotation": "2.5.0",
|
||||
"@embedpdf/plugin-history": "2.5.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.5.0",
|
||||
"@embedpdf/plugin-selection": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"@embedpdf/plugin-annotation": "2.6.0",
|
||||
"@embedpdf/plugin-history": "2.6.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.6.0",
|
||||
"@embedpdf/plugin-selection": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -831,15 +846,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-render": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.5.0.tgz",
|
||||
"integrity": "sha512-nrTmg8cVMohcKYiQ/7erErsaWlyaq20OtXbVjmnPNnqz4amJLAjlPyudTJRlWWPyIiri9SF4A0ue5ICDY2sypg==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.6.0.tgz",
|
||||
"integrity": "sha512-Rk4QCxDOzhQrvKPt/G3G+p5ELwnKFkC5ljHMd7ND23atR9E3wm5W3+Nx3FaAYYPrpfqQ7BrbKnfQ7SkUbDxS3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -848,15 +863,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-rotate": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.5.0.tgz",
|
||||
"integrity": "sha512-crFsXduaxNZJmVRfgklBpO4x4i9cRxPmfFBvdIoyJ1ea6AGOCL0rQKQcfqHTFdgtPzlVUiIg6Hi2v+033jdLUg==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.6.0.tgz",
|
||||
"integrity": "sha512-zgF2S5cfkOxkOWrwoLQLN8scJgKBEhyhVOv/RNdeAKP6qE3h28AGRmDeMsekBDbiInlIxIHzynE5vVcTNf5EnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -865,16 +880,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-scroll": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.5.0.tgz",
|
||||
"integrity": "sha512-AdLuSgvAaukLl1uQ0FbswcAIPFaR3Jk2ZbEJpWLd9E6iQ+66Cta0Sz8d5J6ndx7VBlRYAZwoqiXF85utJxpQ5g==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.6.0.tgz",
|
||||
"integrity": "sha512-BEgSy6cs9+MLCS0Z3/FYMdA4Ygt6ddYIAg28XlF20kN3tLj8BQUo5qx6adI+SlwrFFGY52VAjjK7VSBuGfn19g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/plugin-viewport": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"@embedpdf/plugin-viewport": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -883,15 +898,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-search": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.5.0.tgz",
|
||||
"integrity": "sha512-ycHJh05vBZ1PTSdEMgdx6K1py0oklwbwY2eXO4nD54EN9EVZgWlYC4Q+u8nyGOiNL6VmJYcqJ0HmjSWBdmGWBw==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.6.0.tgz",
|
||||
"integrity": "sha512-GSzJkmuK9LE7LmlTwnDl71KdD9prHlCjgFs5Tm0K8qjELOSH+oduFXusIuf654+UQveDYczpzBVUcqb4yBf1xA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -900,17 +915,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-selection": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.5.0.tgz",
|
||||
"integrity": "sha512-M3WDjahig/6KE83SZGvTaJWhqEOIzH002k2fpJVuks926UBnfgYCH8uqV7SOUQTneQDmIa0PlyFiuEXDw1Ocrw==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.6.0.tgz",
|
||||
"integrity": "sha512-VrW0duVxLwaquInwmuNDMz8o0tfCDwe3j81fvTUDW/s7KqnzFbxK7vEuq5TEtxWuSng2DXxyX3r1ntCm4X/NCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0",
|
||||
"@embedpdf/utils": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0",
|
||||
"@embedpdf/utils": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"@embedpdf/plugin-interaction-manager": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -919,15 +934,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-spread": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.5.0.tgz",
|
||||
"integrity": "sha512-kG8HZMZmbpUVDxCOEyQzIiMPW+VjjebOl93V+quAH+GAI5Tkg6exPyyQ2+/DOJPCtYX4Kh2z2aeoyK2b7NRgIQ==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.6.0.tgz",
|
||||
"integrity": "sha512-0mzPCJlw1X7jWeDg5JssU6/HCFtyOP7scEdbIaASYzofGXa2Rj8/+L+UDBrb+KTF6CR4X6fEfeNmxWmAatOAWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -936,16 +951,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-thumbnail": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.5.0.tgz",
|
||||
"integrity": "sha512-iWofJSXKbWrgvS2fe8v3U1+e2wjBRXD2i1DUcJKnTrqyfjZ8YzUomc5EzdG2RT7uUjtqrcu7463TZ9JHXUkASQ==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.6.0.tgz",
|
||||
"integrity": "sha512-Sj4jCV1MNk+19zKWX4KfSl5c0YHrqVG83pEYfqexjSkSX7y7HRwAOtMBtNd3uLInPPSBnzDxj+KlJlIe8RPPJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/plugin-render": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"@embedpdf/plugin-render": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -954,18 +969,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-tiling": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.5.0.tgz",
|
||||
"integrity": "sha512-oih0GyGOJvfaXPLSEY+qfC05UUU1ZkADEbr6uCwRMmdHIXu/0ZTJnAToegfWXtfE+Sw0J5wscVkipXXIX5azlw==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.6.0.tgz",
|
||||
"integrity": "sha512-qyiHWljryHWQ7uzip2WDg4x28o/1QM0wh9oIyz5WlBnrDaK6bLJGsWUym5P6WfLp0Y8h6GFAslNNcgjBv6E3qw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/plugin-render": "2.5.0",
|
||||
"@embedpdf/plugin-scroll": "2.5.0",
|
||||
"@embedpdf/plugin-viewport": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"@embedpdf/plugin-render": "2.6.0",
|
||||
"@embedpdf/plugin-scroll": "2.6.0",
|
||||
"@embedpdf/plugin-viewport": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -974,15 +989,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-viewport": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.5.0.tgz",
|
||||
"integrity": "sha512-z0AXHA9Z3rZdCLje7P2NsQbxKLJ4b/l8lgzXOVn5Ow/pIPE0D2P3fn9WzImHTNI1RNrZMdkW9OH3lfkEXFTqHw==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.6.0.tgz",
|
||||
"integrity": "sha512-Ea7s+LivQ4ph01mVngU2tu2Ni/zulxzIyiifCpMaBMHmvjGFQjJcNNYHR90YuM8keto82KCszxdNDuAEEzT6Wg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -991,17 +1006,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-zoom": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.5.0.tgz",
|
||||
"integrity": "sha512-HWJlqXOXdv/kttV+XWCCStUZAeLl66AuaO8BsnPlAPwEADLLCH4tR4XqJQoWr7/r5watKP7UeQ00FsWu0oGclw==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.6.0.tgz",
|
||||
"integrity": "sha512-2XUgasN2ZQm2MgpB6ls/re/SKhsREvt2D1gIcvJgXvGkene0NcpxGNIRi/+JN7W0fw4x3QtwLQtyF+/0uMgPmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "2.5.0"
|
||||
"@embedpdf/models": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "2.5.0",
|
||||
"@embedpdf/plugin-scroll": "2.5.0",
|
||||
"@embedpdf/plugin-viewport": "2.5.0",
|
||||
"@embedpdf/core": "2.6.0",
|
||||
"@embedpdf/plugin-scroll": "2.6.0",
|
||||
"@embedpdf/plugin-viewport": "2.6.0",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@ -1010,9 +1025,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/utils": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.5.0.tgz",
|
||||
"integrity": "sha512-JjYj6BRzu9oesA1JOqKPFMEWKinjvJIjziWu1j6lDXxLsE59bkShjUKbaEG+lkXRspuZRWNP++rzE2p2Ht4veg==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.6.0.tgz",
|
||||
"integrity": "sha512-FT6U6L3Et688urUTyISpYH05w4sG+WzoWxaI7aPU4ieh4c/vVgadUtjZj/QCC8v+DebPYRAX1gpUY7e0Y0HlTQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
@ -6480,11 +6495,23 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1",
|
||||
"color-string": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@ -6497,9 +6524,18 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@ -6618,6 +6654,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
|
||||
@ -8867,6 +8909,22 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-entities": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
|
||||
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/mdevils"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://patreon.com/mdevils"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@ -10713,6 +10771,15 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-html-better-parser": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/node-html-better-parser/-/node-html-better-parser-1.5.8.tgz",
|
||||
"integrity": "sha512-t/wAKvaTSKco43X+yf9+76RiMt18MtMmzd4wc7rKj+fWav6DV4ajDEKdWlLzSE8USDF5zr/06uGj0Wr/dGAFtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"html-entities": "^2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
@ -11268,24 +11335,6 @@
|
||||
"node": ">= 14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-lib": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
||||
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pdf-lib/standard-fonts": "^1.0.0",
|
||||
"@pdf-lib/upng": "^1.0.1",
|
||||
"pako": "^1.0.11",
|
||||
"tslib": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-lib/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/pdfjs-dist": {
|
||||
"version": "5.4.530",
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.530.tgz",
|
||||
@ -12993,6 +13042,21 @@
|
||||
"integrity": "sha512-zyxW5vuJVnQdGcU+kAj9FYl7WaAunY3kA5S7mPg0xJiujL9+sPAWfSQHS5tXaJXDUa4FuZeKhfdCDQ6K3wfkpQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
||||
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle/node_modules/is-arrayish": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
||||
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
|
||||
|
||||
@ -7,29 +7,29 @@
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@embedpdf/core": "^2.5.0",
|
||||
"@embedpdf/engines": "^2.5.0",
|
||||
"@embedpdf/models": "^2.5.0",
|
||||
"@embedpdf/plugin-annotation": "^2.5.0",
|
||||
"@embedpdf/plugin-attachment": "^2.5.0",
|
||||
"@embedpdf/plugin-bookmark": "^2.5.0",
|
||||
"@embedpdf/plugin-export": "^2.5.0",
|
||||
"@embedpdf/plugin-history": "^2.5.0",
|
||||
"@embedpdf/plugin-document-manager": "^2.5.0",
|
||||
"@embedpdf/plugin-interaction-manager": "^2.5.0",
|
||||
"@embedpdf/plugin-pan": "^2.5.0",
|
||||
"@embedpdf/plugin-print": "^2.5.0",
|
||||
"@embedpdf/plugin-redaction": "^2.5.0",
|
||||
"@embedpdf/plugin-render": "^2.5.0",
|
||||
"@embedpdf/plugin-rotate": "^2.5.0",
|
||||
"@embedpdf/plugin-scroll": "^2.5.0",
|
||||
"@embedpdf/plugin-search": "^2.5.0",
|
||||
"@embedpdf/plugin-selection": "^2.5.0",
|
||||
"@embedpdf/plugin-spread": "^2.5.0",
|
||||
"@embedpdf/plugin-thumbnail": "^2.5.0",
|
||||
"@embedpdf/plugin-tiling": "^2.5.0",
|
||||
"@embedpdf/plugin-viewport": "^2.5.0",
|
||||
"@embedpdf/plugin-zoom": "^2.5.0",
|
||||
"@embedpdf/core": "^2.6.0",
|
||||
"@embedpdf/engines": "^2.6.0",
|
||||
"@embedpdf/models": "^2.6.0",
|
||||
"@embedpdf/plugin-annotation": "^2.6.0",
|
||||
"@embedpdf/plugin-attachment": "^2.6.0",
|
||||
"@embedpdf/plugin-bookmark": "^2.6.0",
|
||||
"@embedpdf/plugin-export": "^2.6.0",
|
||||
"@embedpdf/plugin-history": "^2.6.0",
|
||||
"@embedpdf/plugin-document-manager": "^2.6.0",
|
||||
"@embedpdf/plugin-interaction-manager": "^2.6.0",
|
||||
"@embedpdf/plugin-pan": "^2.6.0",
|
||||
"@embedpdf/plugin-print": "^2.6.0",
|
||||
"@embedpdf/plugin-redaction": "^2.6.0",
|
||||
"@embedpdf/plugin-render": "^2.6.0",
|
||||
"@embedpdf/plugin-rotate": "^2.6.0",
|
||||
"@embedpdf/plugin-scroll": "^2.6.0",
|
||||
"@embedpdf/plugin-search": "^2.6.0",
|
||||
"@embedpdf/plugin-selection": "^2.6.0",
|
||||
"@embedpdf/plugin-spread": "^2.6.0",
|
||||
"@embedpdf/plugin-thumbnail": "^2.6.0",
|
||||
"@embedpdf/plugin-tiling": "^2.6.0",
|
||||
"@embedpdf/plugin-viewport": "^2.6.0",
|
||||
"@embedpdf/plugin-zoom": "^2.6.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
@ -57,7 +57,7 @@
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"jszip": "^3.10.1",
|
||||
"license-report": "^6.8.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"@cantoo/pdf-lib": "^2.5.3",
|
||||
"pdfjs-dist": "^5.4.149",
|
||||
"peerjs": "^1.5.5",
|
||||
"posthog-js": "^1.268.0",
|
||||
|
||||
@ -8,6 +8,7 @@ import type {
|
||||
AnnotationEvent,
|
||||
AnnotationPatch,
|
||||
} from '@app/components/viewer/viewerTypes';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
type NoteIcon = NonNullable<AnnotationToolOptions['icon']>;
|
||||
|
||||
@ -290,6 +291,7 @@ const TOOL_DEFAULT_BUILDERS: Record<AnnotationToolId, ToolDefaultsBuilder> = {
|
||||
export const AnnotationAPIBridge = forwardRef<AnnotationAPI>(function AnnotationAPIBridge(_props, ref) {
|
||||
// Use the provided annotation API just like SignatureAPIBridge/HistoryAPIBridge
|
||||
const { provides: annotationApi } = useAnnotationCapability();
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
const buildAnnotationDefaults = useCallback(
|
||||
(toolId: AnnotationToolId, options?: AnnotationToolOptions) =>
|
||||
@ -323,6 +325,7 @@ export const AnnotationAPIBridge = forwardRef<AnnotationAPI>(function Annotation
|
||||
activateAnnotationTool: (toolId: AnnotationToolId, options?: AnnotationToolOptions) => {
|
||||
configureAnnotationTool(toolId, options);
|
||||
},
|
||||
isReady: () => !!annotationApi && documentReady,
|
||||
setAnnotationStyle: (toolId: AnnotationToolId, options?: AnnotationToolOptions) => {
|
||||
const defaults = buildAnnotationDefaults(toolId, options);
|
||||
const api = annotationApi as AnnotationApiSurface | undefined;
|
||||
|
||||
@ -3,7 +3,11 @@ import { useAttachmentCapability } from '@embedpdf/plugin-attachment/react';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { AttachmentState, AttachmentAPIWrapper } from '@app/contexts/viewer/viewerBridges';
|
||||
import { PdfAttachmentObject } from '@embedpdf/models';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* Connects the PDF attachment plugin to the shared ViewerContext.
|
||||
*/
|
||||
export function AttachmentAPIBridge() {
|
||||
const { provides: attachmentCapability } = useAttachmentCapability();
|
||||
const { registerBridge } = useViewer();
|
||||
@ -12,10 +16,19 @@ export function AttachmentAPIBridge() {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
const fetchAttachments = useCallback(
|
||||
async () => {
|
||||
if (!attachmentCapability) return [];
|
||||
if (!attachmentCapability || !documentReady) {
|
||||
// Set error state instead of throwing for better user experience
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: 'Document not ready or attachment capability not available',
|
||||
isLoading: false
|
||||
}));
|
||||
return [];
|
||||
}
|
||||
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
try {
|
||||
@ -42,11 +55,12 @@ export function AttachmentAPIBridge() {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[attachmentCapability]
|
||||
[attachmentCapability, documentReady]
|
||||
);
|
||||
|
||||
const api = useMemo<AttachmentAPIWrapper | null>(() => {
|
||||
if (!attachmentCapability) return null;
|
||||
// Only provide API when both capability AND document are ready
|
||||
if (!attachmentCapability || !documentReady) return null;
|
||||
|
||||
return {
|
||||
getAttachments: fetchAttachments,
|
||||
@ -84,15 +98,23 @@ export function AttachmentAPIBridge() {
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [attachmentCapability, fetchAttachments]);
|
||||
}, [attachmentCapability, documentReady, fetchAttachments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
if (!api) {
|
||||
// If API becomes null (e.g. document transitions), ensure we unregister stale bridge
|
||||
registerBridge('attachment', null);
|
||||
return;
|
||||
}
|
||||
|
||||
registerBridge('attachment', {
|
||||
state,
|
||||
api,
|
||||
});
|
||||
|
||||
return () => {
|
||||
registerBridge('attachment', null);
|
||||
};
|
||||
}, [api, state, registerBridge]);
|
||||
|
||||
return null;
|
||||
|
||||
@ -2,7 +2,11 @@ import { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import { useBookmarkCapability, BookmarkCapability } from '@embedpdf/plugin-bookmark/react';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { BookmarkState, BookmarkAPIWrapper } from '@app/contexts/viewer/viewerBridges';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* Connects the PDF bookmark plugin to the shared ViewerContext.
|
||||
*/
|
||||
export function BookmarkAPIBridge() {
|
||||
const { provides: bookmarkCapability } = useBookmarkCapability();
|
||||
const { registerBridge } = useViewer();
|
||||
@ -11,9 +15,19 @@ export function BookmarkAPIBridge() {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
const fetchBookmarks = useCallback(
|
||||
async (capability: BookmarkCapability) => {
|
||||
if (!documentReady) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: 'Document not ready or bookmark capability not available',
|
||||
isLoading: false,
|
||||
}));
|
||||
return [];
|
||||
}
|
||||
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
try {
|
||||
const task = capability.getBookmarks();
|
||||
@ -34,11 +48,12 @@ export function BookmarkAPIBridge() {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[]
|
||||
[documentReady]
|
||||
);
|
||||
|
||||
const api = useMemo<BookmarkAPIWrapper | null>(() => {
|
||||
if (!bookmarkCapability) return null;
|
||||
// Only provide API when both capability AND document are ready
|
||||
if (!bookmarkCapability || !documentReady) return null;
|
||||
|
||||
return {
|
||||
fetchBookmarks: () => fetchBookmarks(bookmarkCapability),
|
||||
@ -57,15 +72,22 @@ export function BookmarkAPIBridge() {
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [bookmarkCapability, fetchBookmarks]);
|
||||
}, [bookmarkCapability, documentReady, fetchBookmarks]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
if (!api) {
|
||||
registerBridge('bookmark', null);
|
||||
return;
|
||||
}
|
||||
|
||||
registerBridge('bookmark', {
|
||||
state,
|
||||
api,
|
||||
});
|
||||
|
||||
return () => {
|
||||
registerBridge('bookmark', null);
|
||||
};
|
||||
}, [api, state, registerBridge]);
|
||||
|
||||
return null;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
import {
|
||||
PdfPermissionFlag,
|
||||
DocumentPermissionsState,
|
||||
@ -25,6 +26,7 @@ export function DocumentPermissionsAPIBridge({
|
||||
permissions = PdfPermissionFlag.AllowAll,
|
||||
}: DocumentPermissionsAPIBridgeProps) {
|
||||
const { registerBridge } = useViewer();
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
const state = useMemo<DocumentPermissionsState>(() => ({
|
||||
isEncrypted,
|
||||
@ -42,7 +44,7 @@ export function DocumentPermissionsAPIBridge({
|
||||
|
||||
const api = useMemo<DocumentPermissionsAPIWrapper>(() => ({
|
||||
hasPermission: (flag: PdfPermissionFlag) => hasPermissionFlag(permissions, flag),
|
||||
hasAllPermissions: (flags: PdfPermissionFlag[]) =>
|
||||
hasAllPermissions: (flags: PdfPermissionFlag[]) =>
|
||||
flags.every(flag => hasPermissionFlag(permissions, flag)),
|
||||
getEffectivePermission: (flag: PdfPermissionFlag) => {
|
||||
if (isOwnerUnlocked) return true;
|
||||
@ -51,11 +53,17 @@ export function DocumentPermissionsAPIBridge({
|
||||
}), [permissions, isOwnerUnlocked]);
|
||||
|
||||
useEffect(() => {
|
||||
registerBridge('permissions', {
|
||||
state,
|
||||
api,
|
||||
});
|
||||
}, [registerBridge, state, api]);
|
||||
if (documentReady) {
|
||||
registerBridge('permissions', {
|
||||
state,
|
||||
api,
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
registerBridge('permissions', null);
|
||||
};
|
||||
}, [registerBridge, state, api, documentReady]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useExportCapability } from '@embedpdf/plugin-export/react';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* Component that runs inside EmbedPDF context and provides export functionality
|
||||
@ -8,9 +9,10 @@ import { useViewer } from '@app/contexts/ViewerContext';
|
||||
export function ExportAPIBridge() {
|
||||
const { provides: exportApi } = useExportCapability();
|
||||
const { registerBridge } = useViewer();
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
useEffect(() => {
|
||||
if (exportApi) {
|
||||
if (exportApi && documentReady) {
|
||||
// Register this bridge with ViewerContext
|
||||
registerBridge('export', {
|
||||
state: {
|
||||
@ -19,7 +21,11 @@ export function ExportAPIBridge() {
|
||||
api: exportApi
|
||||
});
|
||||
}
|
||||
}, [exportApi, registerBridge]);
|
||||
|
||||
return () => {
|
||||
registerBridge('export', null);
|
||||
};
|
||||
}, [exportApi, documentReady, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,19 +2,25 @@ import { useImperativeHandle, forwardRef, useEffect, useRef } from 'react';
|
||||
import { useHistoryCapability } from '@embedpdf/plugin-history/react';
|
||||
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||
import { useSignature } from '@app/contexts/SignatureContext';
|
||||
import { PdfAnnotationSubtype, uuidV4 } from '@embedpdf/models';
|
||||
import { uuidV4, PdfAnnotationSubtype } from '@embedpdf/models';
|
||||
import type { HistoryAPI } from '@app/components/viewer/viewerTypes';
|
||||
import { ANNOTATION_RECREATION_DELAY_MS, ANNOTATION_VERIFICATION_DELAY_MS } from '@app/constants/app';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* Connects the PDF history (undo/redo) plugin to the shared ViewerContext.
|
||||
*/
|
||||
|
||||
export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge(_, ref) {
|
||||
const { provides: historyApi } = useHistoryCapability();
|
||||
const { provides: annotationApi } = useAnnotationCapability();
|
||||
const { getImageData, storeImageData } = useSignature();
|
||||
const documentReady = useDocumentReady();
|
||||
const restoringIds = useRef<Set<string>>(new Set());
|
||||
|
||||
// Monitor annotation events to detect when annotations are restored
|
||||
useEffect(() => {
|
||||
if (!annotationApi) return;
|
||||
if (!annotationApi || !documentReady) return;
|
||||
|
||||
const handleAnnotationEvent = (event: any) => {
|
||||
const annotation = event.annotation;
|
||||
@ -146,6 +152,8 @@ export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge
|
||||
return historyApi ? historyApi.canRedo() : false;
|
||||
},
|
||||
|
||||
isReady: () => !!historyApi && documentReady,
|
||||
|
||||
purgeByMetadata: <T,>(predicate: (metadata: T | undefined) => boolean, topic?: string) => {
|
||||
if (historyApi?.purgeByMetadata) {
|
||||
return historyApi.purgeByMetadata(predicate, topic);
|
||||
|
||||
@ -3,7 +3,8 @@ import { useDocumentState } from '@embedpdf/core/react';
|
||||
import { useScroll } from '@embedpdf/plugin-scroll/react';
|
||||
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||
import { PdfAnnotationSubtype } from '@embedpdf/models';
|
||||
import { usePdfLibLinks, type PdfLibLink } from '@app/hooks/usePdfLibLinks';
|
||||
import { usePdfLibLinks } from '@app/hooks/usePdfLibLinks';
|
||||
import type { PdfLibLink } from '@app/utils/pdfLinkUtils';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Inline SVG icons (thin-stroke, modern)
|
||||
|
||||
@ -125,7 +125,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
}),
|
||||
createPluginRegistration(ScrollPluginPackage),
|
||||
createPluginRegistration(RenderPluginPackage, {
|
||||
withForms: !enableFormFill, // Disable native form rendering when our interactive overlay is active
|
||||
withForms: true,
|
||||
withAnnotations: showBakedAnnotations && !enableAnnotations, // Show baked annotations only when: visibility is ON and annotation layer is OFF
|
||||
}),
|
||||
|
||||
@ -204,7 +204,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
|
||||
// Register print plugin for printing PDFs
|
||||
createPluginRegistration(PrintPluginPackage),
|
||||
];
|
||||
}, [pdfUrl, enableAnnotations, enableFormFill, showBakedAnnotations, fileName, file, url]);
|
||||
}, [pdfUrl, enableAnnotations, showBakedAnnotations, fileName, file, url]);
|
||||
|
||||
// Initialize the engine with the React hook - use local WASM for offline support
|
||||
const { engine, isLoading, error } = usePdfiumEngine({
|
||||
|
||||
@ -2,28 +2,33 @@ import { useEffect, useRef } from 'react';
|
||||
import { usePan } from '@embedpdf/plugin-pan/react';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* Connects the PDF pan (hand tool) plugin to the shared ViewerContext.
|
||||
*/
|
||||
export function PanAPIBridge() {
|
||||
const activeDocumentId = useActiveDocumentId();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID
|
||||
if (!activeDocumentId) {
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID and the document is ready
|
||||
if (!activeDocumentId || !documentReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return <PanAPIBridgeInner documentId={activeDocumentId} />;
|
||||
}
|
||||
|
||||
function PanAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
const { provides: pan, isPanning } = usePan(documentId);
|
||||
const { registerBridge, triggerImmediatePanUpdate } = useViewer();
|
||||
|
||||
|
||||
// Keep pan ref updated to avoid re-running effect when object reference changes
|
||||
const panRef = useRef(pan);
|
||||
useEffect(() => {
|
||||
panRef.current = pan;
|
||||
}, [pan]);
|
||||
|
||||
|
||||
// Track previous isPanning value to detect changes
|
||||
const prevIsPanningRef = useRef<boolean>(isPanning);
|
||||
|
||||
@ -57,13 +62,16 @@ function PanAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger immediate pan update if the value changed
|
||||
|
||||
if (prevIsPanningRef.current !== isPanning) {
|
||||
prevIsPanningRef.current = isPanning;
|
||||
triggerImmediatePanUpdate(isPanning);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
registerBridge('pan', null);
|
||||
};
|
||||
}, [isPanning, registerBridge, triggerImmediatePanUpdate]);
|
||||
|
||||
return null;
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
import { useEffect } from 'react';
|
||||
import { usePrintCapability } from '@embedpdf/plugin-print/react';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* Component that runs inside EmbedPDF context and exposes print API to ViewerContext
|
||||
* Connects the PDF print plugin to the shared ViewerContext.
|
||||
*/
|
||||
export function PrintAPIBridge() {
|
||||
const { provides: print } = usePrintCapability();
|
||||
const { registerBridge } = useViewer();
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
useEffect(() => {
|
||||
if (print) {
|
||||
if (print && documentReady) {
|
||||
// Register this bridge with ViewerContext
|
||||
registerBridge('print', {
|
||||
state: {},
|
||||
@ -19,7 +21,11 @@ export function PrintAPIBridge() {
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [print, registerBridge]);
|
||||
|
||||
return () => {
|
||||
registerBridge('print', null);
|
||||
};
|
||||
}, [print, documentReady, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -4,17 +4,18 @@ import { PdfAnnotationSubtype } from '@embedpdf/models';
|
||||
import { useRedaction } from '@app/contexts/RedactionContext';
|
||||
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
|
||||
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* RedactionAPIBridge - Uses embedPDF v2.5.0
|
||||
* Bridges between the EmbedPDF redaction plugin and the Stirling-PDF RedactionContext.
|
||||
* Uses the unified redaction mode (toggleRedact/enableRedact/endRedact).
|
||||
*/
|
||||
export function RedactionAPIBridge() {
|
||||
const activeDocumentId = useActiveDocumentId();
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID
|
||||
if (!activeDocumentId) {
|
||||
// Don't render the inner component until we have a valid document ID and document is ready
|
||||
if (!activeDocumentId || !documentReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -69,9 +70,7 @@ function RedactionAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
}, [annotationProvides, manualRedactColor]);
|
||||
|
||||
// Expose the EmbedPDF API through our context's ref
|
||||
// Uses v2.5.0 unified redaction mode
|
||||
useImperativeHandle(redactionApiRef, () => ({
|
||||
// Unified redaction methods (v2.5.0)
|
||||
toggleRedact: () => {
|
||||
redactionProvides?.toggleRedact();
|
||||
},
|
||||
|
||||
@ -2,15 +2,20 @@ import { useEffect, useRef } from 'react';
|
||||
import { useRotate } from '@embedpdf/plugin-rotate/react';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* Connects the PDF rotation plugin to the shared ViewerContext.
|
||||
*/
|
||||
export function RotateAPIBridge() {
|
||||
const activeDocumentId = useActiveDocumentId();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID
|
||||
if (!activeDocumentId) {
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID and document is ready
|
||||
if (!activeDocumentId || !documentReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return <RotateAPIBridgeInner documentId={activeDocumentId} />;
|
||||
}
|
||||
|
||||
@ -42,7 +47,11 @@ function RotateAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
registerBridge('rotation', null);
|
||||
};
|
||||
}, [rotation, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,15 +2,20 @@ import { useEffect, useRef } from 'react';
|
||||
import { useScroll } from '@embedpdf/plugin-scroll/react';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* Connects the PDF scroll plugin to the shared ViewerContext.
|
||||
*/
|
||||
export function ScrollAPIBridge() {
|
||||
const activeDocumentId = useActiveDocumentId();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID
|
||||
if (!activeDocumentId) {
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID and document is ready
|
||||
if (!activeDocumentId || !documentReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return <ScrollAPIBridgeInner documentId={activeDocumentId} />;
|
||||
}
|
||||
|
||||
@ -35,7 +40,7 @@ function ScrollAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
currentPage,
|
||||
totalPages,
|
||||
};
|
||||
|
||||
|
||||
// Trigger immediate update for responsive UI
|
||||
triggerImmediateScrollUpdate(newState.currentPage, newState.totalPages);
|
||||
|
||||
@ -44,6 +49,10 @@ function ScrollAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
api: currentScroll
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
registerBridge('scroll', null);
|
||||
};
|
||||
}, [currentPage, totalPages, registerBridge, triggerImmediateScrollUpdate]);
|
||||
|
||||
return null;
|
||||
|
||||
@ -2,6 +2,7 @@ import { useEffect, useState, useRef } from 'react';
|
||||
import { useSearch } from '@embedpdf/plugin-search/react';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
interface SearchResult {
|
||||
pageIndex: number;
|
||||
@ -11,30 +12,35 @@ interface SearchResult {
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchAPIBridge - Updated for embedPDF v2.6.0
|
||||
* Connects the PDF search plugin to the shared ViewerContext.
|
||||
*/
|
||||
export function SearchAPIBridge() {
|
||||
const activeDocumentId = useActiveDocumentId();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID
|
||||
if (!activeDocumentId) {
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID and document is ready
|
||||
if (!activeDocumentId || !documentReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return <SearchAPIBridgeInner documentId={activeDocumentId} />;
|
||||
}
|
||||
|
||||
function SearchAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
const { provides: search } = useSearch(documentId);
|
||||
const { registerBridge, scrollActions } = useViewer();
|
||||
|
||||
|
||||
// Keep search ref updated to avoid re-running effects when object reference changes
|
||||
const searchRef = useRef(search);
|
||||
const isSearchingRef = useRef(false);
|
||||
const lastScrolledIndexRef = useRef<number | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
searchRef.current = search;
|
||||
}, [search]);
|
||||
|
||||
|
||||
const [localState, setLocalState] = useState({
|
||||
results: null as SearchResult[] | null,
|
||||
activeIndex: 0
|
||||
@ -42,14 +48,14 @@ function SearchAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
|
||||
// Subscribe to search result changes from EmbedPDF
|
||||
const subscriptionRef = useRef<(() => void) | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// Cleanup previous subscription
|
||||
if (subscriptionRef.current) {
|
||||
subscriptionRef.current();
|
||||
subscriptionRef.current = null;
|
||||
}
|
||||
|
||||
|
||||
if (!search) return;
|
||||
|
||||
subscriptionRef.current = search.onSearchResultStateChange?.((state: any) => {
|
||||
@ -87,11 +93,11 @@ function SearchAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
lastScrolledIndexRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Only scroll if the active index actually changed
|
||||
const activeResultIndex = localActiveIndex - 1; // Convert back to 0-based
|
||||
if (activeResultIndex >= 0 &&
|
||||
activeResultIndex < localResults.length &&
|
||||
if (activeResultIndex >= 0 &&
|
||||
activeResultIndex < localResults.length &&
|
||||
lastScrolledIndexRef.current !== activeResultIndex) {
|
||||
const activeResult = localResults[activeResultIndex];
|
||||
if (activeResult) {
|
||||
@ -114,13 +120,13 @@ function SearchAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
if (isSearchingRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (!currentSearch?.startSearch || !currentSearch?.searchAllPages) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
isSearchingRef.current = true;
|
||||
|
||||
|
||||
try {
|
||||
currentSearch.startSearch();
|
||||
const results = await currentSearch.searchAllPages(query);
|
||||
@ -171,6 +177,10 @@ function SearchAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
registerBridge('search', null);
|
||||
};
|
||||
}, [localResults, localActiveIndex, registerBridge]);
|
||||
|
||||
return null;
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useSelectionCapability } from '@embedpdf/plugin-selection/react';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* Connects the PDF selection plugin to the shared ViewerContext.
|
||||
*/
|
||||
export function SelectionAPIBridge() {
|
||||
const { provides: selection } = useSelectionCapability();
|
||||
const { registerBridge } = useViewer();
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
|
||||
const hasSelectionRef = useRef(false);
|
||||
const selectedTextRef = useRef('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!selection) return;
|
||||
if (!selection || !documentReady) return;
|
||||
|
||||
const buildApi = () => ({
|
||||
copyToClipboard: () => selection.copyToClipboard(),
|
||||
@ -68,8 +73,9 @@ export function SelectionAPIBridge() {
|
||||
unsubCopy?.();
|
||||
document.removeEventListener('copy', handleCopy);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
registerBridge('selection', null);
|
||||
};
|
||||
}, [selection]);
|
||||
}, [selection, documentReady, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -5,6 +5,11 @@ import { useSignature } from '@app/contexts/SignatureContext';
|
||||
import type { SignatureAPI } from '@app/components/viewer/viewerTypes';
|
||||
import type { SignParameters } from '@app/hooks/tools/sign/useSignParameters';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* Connects the PDF signature (stamp/ink) tools to the shared ViewerContext and SignatureContext.
|
||||
*/
|
||||
|
||||
// Minimum allowed width/height (in pixels) for a signature image or text stamp.
|
||||
// This prevents rendering issues and ensures signatures are always visible and usable.
|
||||
@ -132,6 +137,7 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
||||
const { provides: annotationApi } = useAnnotationCapability();
|
||||
const { signatureConfig, storeImageData, isPlacementMode, placementPreviewSize, setSignaturesApplied } = useSignature();
|
||||
const { getZoomState, registerImmediateZoomUpdate } = useViewer();
|
||||
const documentReady = useDocumentReady();
|
||||
const [currentZoom, setCurrentZoom] = useState(() => getZoomState()?.currentZoom ?? 1);
|
||||
const lastStampImageRef = useRef<string | null>(null);
|
||||
|
||||
@ -211,7 +217,7 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
||||
// Enable keyboard deletion of selected annotations
|
||||
useEffect(() => {
|
||||
// Always enable delete key when we have annotation API and are in sign mode
|
||||
if (!annotationApi || (isPlacementMode === undefined)) return;
|
||||
if (!annotationApi || (isPlacementMode === undefined) || !documentReady) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Skip delete/backspace while a text input/textarea is focused (e.g., editing textbox)
|
||||
@ -391,7 +397,7 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
||||
}), [annotationApi, signatureConfig, placementPreviewSize, applyStampDefaults]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!annotationApi?.onAnnotationEvent) {
|
||||
if (!annotationApi?.onAnnotationEvent || !documentReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -430,10 +436,10 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [annotationApi, storeImageData, setSignaturesApplied]);
|
||||
}, [annotationApi, storeImageData, setSignaturesApplied, documentReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlacementMode) {
|
||||
if (!isPlacementMode || !documentReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -447,66 +453,7 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isPlacementMode, configureStampDefaults, placementPreviewSize, signatureConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!annotationApi?.onAnnotationEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsubscribe = annotationApi.onAnnotationEvent(event => {
|
||||
if (event.type !== 'create' && event.type !== 'update') {
|
||||
return;
|
||||
}
|
||||
|
||||
const annotation: any = event.annotation;
|
||||
const annotationId: string | undefined = annotation?.id;
|
||||
if (!annotationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark signatures as not applied when a new signature is placed
|
||||
if (event.type === 'create') {
|
||||
setSignaturesApplied(false);
|
||||
}
|
||||
|
||||
const directData =
|
||||
extractDataUrl(annotation.imageSrc) ||
|
||||
extractDataUrl(annotation.imageData) ||
|
||||
extractDataUrl(annotation.appearance) ||
|
||||
extractDataUrl(annotation.stampData) ||
|
||||
extractDataUrl(annotation.contents) ||
|
||||
extractDataUrl(annotation.data) ||
|
||||
extractDataUrl(annotation.customData) ||
|
||||
extractDataUrl(annotation.asset);
|
||||
|
||||
const dataToStore = directData || lastStampImageRef.current;
|
||||
if (dataToStore) {
|
||||
storeImageData(annotationId, dataToStore);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [annotationApi, storeImageData, setSignaturesApplied]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlacementMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
configureStampDefaults().catch((error) => {
|
||||
if (!cancelled) {
|
||||
console.error('Error updating signature defaults:', error);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isPlacementMode, configureStampDefaults, placementPreviewSize, signatureConfig]);
|
||||
}, [isPlacementMode, configureStampDefaults, placementPreviewSize, signatureConfig, documentReady]);
|
||||
|
||||
|
||||
return null; // This is a bridge component with no UI
|
||||
|
||||
@ -2,15 +2,21 @@ import { useEffect, useRef } from 'react';
|
||||
import { useSpread, SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* SpreadAPIBridge - Updated for embedPDF v2.6.0
|
||||
* Connects the PDF spread mode (single/dual page) plugin to the shared ViewerContext.
|
||||
*/
|
||||
export function SpreadAPIBridge() {
|
||||
const activeDocumentId = useActiveDocumentId();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID
|
||||
if (!activeDocumentId) {
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID and document is ready
|
||||
if (!activeDocumentId || !documentReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return <SpreadAPIBridgeInner documentId={activeDocumentId} />;
|
||||
}
|
||||
|
||||
@ -52,6 +58,10 @@ function SpreadAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
});
|
||||
|
||||
triggerImmediateSpreadUpdate(spreadMode);
|
||||
|
||||
return () => {
|
||||
registerBridge('spread', null);
|
||||
};
|
||||
}, [spreadMode, registerBridge, triggerImmediateSpreadUpdate]);
|
||||
|
||||
return null;
|
||||
|
||||
@ -1,23 +1,29 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useThumbnailCapability } from '@embedpdf/plugin-thumbnail/react';
|
||||
import { useViewer } from '@app/contexts/ViewerContext';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* ThumbnailAPIBridge provides thumbnail generation functionality.
|
||||
* Exposes thumbnail API to UI components without managing state.
|
||||
* ThumbnailAPIBridge - Updated for embedPDF v2.6.0
|
||||
* Provides thumbnail generation functionality.
|
||||
*/
|
||||
export function ThumbnailAPIBridge() {
|
||||
const { provides: thumbnail } = useThumbnailCapability();
|
||||
const { registerBridge } = useViewer();
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
useEffect(() => {
|
||||
if (thumbnail) {
|
||||
if (thumbnail && documentReady) {
|
||||
registerBridge('thumbnail', {
|
||||
state: null, // No state - just provides API
|
||||
api: thumbnail
|
||||
});
|
||||
}
|
||||
}, [thumbnail]);
|
||||
|
||||
return () => {
|
||||
registerBridge('thumbnail', null);
|
||||
};
|
||||
}, [thumbnail, documentReady, registerBridge]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -11,15 +11,20 @@ import {
|
||||
useFitWidthResize,
|
||||
} from '@app/utils/viewerZoom';
|
||||
import { getFirstPageAspectRatioFromStub } from '@app/utils/pageMetadata';
|
||||
import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady';
|
||||
|
||||
/**
|
||||
* Connects the PDF zoom plugin to the shared ViewerContext.
|
||||
*/
|
||||
export function ZoomAPIBridge() {
|
||||
const activeDocumentId = useActiveDocumentId();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID
|
||||
if (!activeDocumentId) {
|
||||
const documentReady = useDocumentReady();
|
||||
|
||||
// Don't render the inner component until we have a valid document ID and document is ready
|
||||
if (!activeDocumentId || !documentReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return <ZoomAPIBridgeInner documentId={activeDocumentId} />;
|
||||
}
|
||||
|
||||
@ -60,7 +65,7 @@ function ZoomAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
// Extract primitive values from zoomState for dependency arrays
|
||||
const zoomLevel = zoomState?.zoomLevel;
|
||||
const currentZoomLevel = zoomState?.currentZoomLevel;
|
||||
|
||||
|
||||
// Extract metadata aspect ratio as a primitive to avoid object reference issues
|
||||
const metadataAspectRatio = getFirstPageAspectRatioFromStub(firstFileStub);
|
||||
|
||||
@ -200,14 +205,14 @@ function ZoomAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
|
||||
// Subscribe to zoom changes - use ref to avoid re-subscribing when zoom reference changes
|
||||
const zoomSubscriptionRef = useRef<(() => void) | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// Cleanup previous subscription if any
|
||||
if (zoomSubscriptionRef.current) {
|
||||
zoomSubscriptionRef.current();
|
||||
zoomSubscriptionRef.current = null;
|
||||
}
|
||||
|
||||
|
||||
if (!zoom) {
|
||||
return;
|
||||
}
|
||||
@ -255,4 +260,4 @@ function ZoomAPIBridgeInner({ documentId }: { documentId: string }) {
|
||||
}, [zoomStateCurrentZoomLevel, registerBridge, triggerImmediateZoomUpdate]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useDocumentManagerCapability } from '@embedpdf/plugin-document-manager/react';
|
||||
|
||||
/**
|
||||
* useDocumentReady - Custom hook to track whether a PDF document is fully loaded
|
||||
* and ready for interaction.
|
||||
*
|
||||
* Subscribes to both onDocumentOpened (sets true) and onDocumentClosed (resets
|
||||
* to false) so the flag correctly tracks the document lifecycle across
|
||||
* open → close → reopen transitions.
|
||||
*
|
||||
* The initial check is synchronous (getActiveDocument is sync) — no debounce
|
||||
* needed.
|
||||
*/
|
||||
export function useDocumentReady() {
|
||||
const { provides: documentManagerCapability } = useDocumentManagerCapability();
|
||||
const [documentReady, setDocumentReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!documentManagerCapability) {
|
||||
setDocumentReady(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let mounted = true;
|
||||
|
||||
const unsubOpen = documentManagerCapability.onDocumentOpened?.((event: any) => {
|
||||
if (mounted && (event?.documentId || event?.id)) {
|
||||
setDocumentReady(true);
|
||||
}
|
||||
});
|
||||
|
||||
const unsubClose = documentManagerCapability.onDocumentClosed?.(() => {
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
const remaining = documentManagerCapability.getActiveDocument?.();
|
||||
if (!remaining?.id && mounted) {
|
||||
setDocumentReady(false);
|
||||
}
|
||||
} catch {
|
||||
if (mounted) setDocumentReady(false);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const activeDoc = documentManagerCapability.getActiveDocument?.();
|
||||
if (mounted) {
|
||||
setDocumentReady(!!activeDoc?.id);
|
||||
}
|
||||
} catch {
|
||||
if (mounted) setDocumentReady(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
if (typeof unsubOpen === 'function') {
|
||||
unsubOpen();
|
||||
}
|
||||
if (typeof unsubClose === 'function') {
|
||||
unsubClose();
|
||||
}
|
||||
};
|
||||
}, [documentManagerCapability]);
|
||||
|
||||
return documentReady;
|
||||
}
|
||||
@ -151,7 +151,7 @@ interface ViewerContextType {
|
||||
// Bridge registration - internal use by bridges
|
||||
registerBridge: <K extends BridgeKey>(
|
||||
type: K,
|
||||
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]>
|
||||
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]> | null
|
||||
) => void;
|
||||
|
||||
// Save changes function - registered by EmbedPdfViewer
|
||||
@ -242,7 +242,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
const registerBridge = useCallback(
|
||||
<K extends BridgeKey>(
|
||||
type: K,
|
||||
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]>
|
||||
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]> | null
|
||||
) => {
|
||||
setBridgeRef(bridgeRefs.current, type, ref);
|
||||
},
|
||||
|
||||
@ -227,7 +227,7 @@ export const createBridgeRegistry = (): ViewerBridgeRegistry => ({
|
||||
export function registerBridge<K extends BridgeKey>(
|
||||
registry: ViewerBridgeRegistry,
|
||||
type: K,
|
||||
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]>
|
||||
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]> | null
|
||||
): void {
|
||||
registry[type] = ref as ViewerBridgeRegistry[K];
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolType, useToolOperation, CustomProcessorResult } from '@app/hooks/tools/shared/useToolOperation';
|
||||
import { AdjustContrastParameters, defaultParameters } from '@app/hooks/tools/adjustContrast/useAdjustContrastParameters';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { PDFDocument as PDFLibDocument } from '@cantoo/pdf-lib';
|
||||
import { applyAdjustmentsToCanvas } from '@app/components/tools/adjustContrast/utils';
|
||||
import { pdfWorkerManager } from '@app/services/pdfWorkerManager';
|
||||
import { createFileFromApiResponse } from '@app/utils/fileResponseUtils';
|
||||
|
||||
@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useToolOperation, ToolType, CustomProcessorResult } from '@app/hooks/tools/shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '@app/utils/toolErrorHandler';
|
||||
import { RemoveAnnotationsParameters, defaultParameters } from '@app/hooks/tools/removeAnnotations/useRemoveAnnotationsParameters';
|
||||
import { PDFDocument, PDFName, PDFRef, PDFDict } from 'pdf-lib';
|
||||
import { PDFDocument, PDFName, PDFRef, PDFDict } from '@cantoo/pdf-lib';
|
||||
// Client-side PDF processing using PDF-lib
|
||||
const removeAnnotationsProcessor = async (_parameters: RemoveAnnotationsParameters, files: File[]): Promise<CustomProcessorResult> => {
|
||||
const processedFiles: File[] = [];
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PDFFont, PDFPage, rgb } from 'pdf-lib';
|
||||
import { PDFFont, PDFPage, rgb } from '@cantoo/pdf-lib';
|
||||
import { wrapText } from '@app/hooks/tools/validateSignature/utils/pdfText';
|
||||
import { colorPalette } from '@app/hooks/tools/validateSignature/utils/pdfPalette';
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PDFFont, PDFPage } from 'pdf-lib';
|
||||
import { PDFFont, PDFPage } from '@cantoo/pdf-lib';
|
||||
import { wrapText } from '@app/hooks/tools/validateSignature/utils/pdfText';
|
||||
import { colorPalette } from '@app/hooks/tools/validateSignature/utils/pdfPalette';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
import { PDFFont, PDFPage } from 'pdf-lib';
|
||||
import { PDFFont, PDFPage } from '@cantoo/pdf-lib';
|
||||
import { SignatureValidationSignature } from '@app/types/validateSignature';
|
||||
import { drawFieldBox } from '@app/hooks/tools/validateSignature/outputtedPDFSections/FieldBoxSection';
|
||||
import { drawStatusBadge } from '@app/hooks/tools/validateSignature/outputtedPDFSections/StatusBadgeSection';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PDFFont, PDFPage, rgb } from 'pdf-lib';
|
||||
import { PDFFont, PDFPage, rgb } from '@cantoo/pdf-lib';
|
||||
|
||||
interface StatusBadgeOptions {
|
||||
page: PDFPage;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
import { PDFFont, PDFImage, PDFPage } from 'pdf-lib';
|
||||
import { PDFFont, PDFImage, PDFPage } from '@cantoo/pdf-lib';
|
||||
import { SignatureValidationReportEntry } from '@app/types/validateSignature';
|
||||
import { drawFieldBox } from '@app/hooks/tools/validateSignature/outputtedPDFSections/FieldBoxSection';
|
||||
import { drawThumbnailImage, drawThumbnailPlaceholder } from '@app/hooks/tools/validateSignature/outputtedPDFSections/ThumbnailSection';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PDFFont, PDFPage, PDFImage } from 'pdf-lib';
|
||||
import { PDFFont, PDFPage, PDFImage } from '@cantoo/pdf-lib';
|
||||
import { colorPalette } from '@app/hooks/tools/validateSignature/utils/pdfPalette';
|
||||
|
||||
export const drawThumbnailPlaceholder = (
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PDFDocument, PDFPage, StandardFonts } from 'pdf-lib';
|
||||
import { PDFDocument, PDFPage, StandardFonts } from '@cantoo/pdf-lib';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { SignatureValidationReportEntry } from '@app/types/validateSignature';
|
||||
import { REPORT_PDF_FILENAME } from '@app/hooks/tools/validateSignature/utils/signatureUtils';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PDFDocument, PDFFont, PDFImage } from 'pdf-lib';
|
||||
import { PDFDocument, PDFFont, PDFImage } from '@cantoo/pdf-lib';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { colorPalette } from '@app/hooks/tools/validateSignature/utils/pdfPalette';
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { rgb } from 'pdf-lib';
|
||||
import { rgb } from '@cantoo/pdf-lib';
|
||||
|
||||
type RgbTuple = [number, number, number];
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PDFFont } from 'pdf-lib';
|
||||
import { PDFFont } from '@cantoo/pdf-lib';
|
||||
|
||||
export const wrapText = (text: string, font: PDFFont, fontSize: number, maxWidth: number): string[] => {
|
||||
const lines: string[] = [];
|
||||
|
||||
@ -1,32 +1,14 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
PDFDocument,
|
||||
PDFDict,
|
||||
PDFName,
|
||||
PDFArray,
|
||||
PDFString,
|
||||
PDFHexString,
|
||||
PDFNumber,
|
||||
PDFRef,
|
||||
PDFPage,
|
||||
PDFContext,
|
||||
} from 'pdf-lib';
|
||||
} from '@cantoo/pdf-lib';
|
||||
import {
|
||||
PdfLibLink,
|
||||
extractLinksFromPage,
|
||||
} from '@app/utils/pdfLinkUtils';
|
||||
|
||||
|
||||
export type LinkType = 'internal' | 'external' | 'unknown';
|
||||
|
||||
export interface PdfLibLink {
|
||||
id: string;
|
||||
/** Index of this annotation in the page's /Annots array (used for deletion matching). */
|
||||
annotIndex: number;
|
||||
/** Rectangle in PDF-page coordinate space (top-left origin, unscaled). */
|
||||
rect: { x: number; y: number; width: number; height: number };
|
||||
type: LinkType;
|
||||
/** 0-based target page index (internal links). */
|
||||
targetPage?: number;
|
||||
/** URI for external links. */
|
||||
uri?: string;
|
||||
}
|
||||
export type { PdfLibLink };
|
||||
|
||||
export interface PdfLibLinksResult {
|
||||
links: PdfLibLink[];
|
||||
@ -43,6 +25,9 @@ interface CachedDoc {
|
||||
refCount: number;
|
||||
/** Per-page extracted links (lazy, filled on first request). */
|
||||
pageLinks: Map<number, { links: PdfLibLink[]; width: number; height: number }>;
|
||||
/** Set to true when the PDF catalog/pages tree is invalid, so we
|
||||
* skip link extraction on all subsequent calls without retrying. */
|
||||
invalidCatalog?: boolean;
|
||||
}
|
||||
|
||||
const docCache = new Map<string, Promise<CachedDoc>>();
|
||||
@ -54,11 +39,17 @@ async function acquireDocument(url: string): Promise<CachedDoc> {
|
||||
const buffer = await response.arrayBuffer();
|
||||
const doc = await PDFDocument.load(new Uint8Array(buffer), {
|
||||
ignoreEncryption: true,
|
||||
updateMetadata: false,
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
|
||||
return { doc, refCount: 0, pageLinks: new Map() };
|
||||
})();
|
||||
docCache.set(url, promise);
|
||||
|
||||
promise.catch(() => {
|
||||
docCache.delete(url);
|
||||
});
|
||||
}
|
||||
const cached = await docCache.get(url)!;
|
||||
cached.refCount++;
|
||||
@ -76,241 +67,7 @@ function releaseDocument(url: string): void {
|
||||
});
|
||||
}
|
||||
|
||||
function num(ctx: PDFContext, value: unknown): number {
|
||||
if (value instanceof PDFRef) value = ctx.lookup(value);
|
||||
if (value instanceof PDFNumber) return value.asNumber();
|
||||
if (typeof value === 'number') return value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function str(ctx: PDFContext, value: unknown): string | undefined {
|
||||
if (value instanceof PDFRef) value = ctx.lookup(value);
|
||||
if (value instanceof PDFString) return value.decodeText();
|
||||
if (value instanceof PDFHexString) return value.decodeText();
|
||||
if (typeof value === 'string') return value;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolvePageIndex(doc: PDFDocument, pageRef: PDFRef): number | undefined {
|
||||
const pages = doc.getPages();
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const ref = pages[i].ref;
|
||||
if (
|
||||
ref === pageRef ||
|
||||
(ref.objectNumber === pageRef.objectNumber &&
|
||||
ref.generationNumber === pageRef.generationNumber)
|
||||
) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveDestArray(
|
||||
doc: PDFDocument,
|
||||
ctx: PDFContext,
|
||||
destArr: PDFArray,
|
||||
): number | undefined {
|
||||
if (destArr.size() < 1) return undefined;
|
||||
const first = destArr.get(0);
|
||||
if (first instanceof PDFRef) {
|
||||
return resolvePageIndex(doc, first);
|
||||
}
|
||||
const n = num(ctx, first);
|
||||
if (typeof n === 'number' && n >= 0) return n;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveNamedDest(
|
||||
doc: PDFDocument,
|
||||
ctx: PDFContext,
|
||||
name: string,
|
||||
): number | undefined {
|
||||
try {
|
||||
const catalog = doc.catalog;
|
||||
|
||||
const namesRaw = catalog.get(PDFName.of('Names'));
|
||||
const namesDict = namesRaw instanceof PDFRef ? ctx.lookup(namesRaw) : namesRaw;
|
||||
if (namesDict instanceof PDFDict) {
|
||||
const destsRaw = namesDict.get(PDFName.of('Dests'));
|
||||
const destsTree = destsRaw instanceof PDFRef ? ctx.lookup(destsRaw) : destsRaw;
|
||||
if (destsTree instanceof PDFDict) {
|
||||
const result = searchNameTree(doc, ctx, destsTree, name);
|
||||
if (result !== undefined) return result;
|
||||
}
|
||||
}
|
||||
|
||||
const destsRaw = catalog.get(PDFName.of('Dests'));
|
||||
const destsDict = destsRaw instanceof PDFRef ? ctx.lookup(destsRaw) : destsRaw;
|
||||
if (destsDict instanceof PDFDict) {
|
||||
const dest = destsDict.get(PDFName.of(name));
|
||||
const destResolved = dest instanceof PDFRef ? ctx.lookup(dest) : dest;
|
||||
if (destResolved instanceof PDFArray) {
|
||||
return resolveDestArray(doc, ctx, destResolved);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Swallow – named dest resolution is best-effort
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function searchNameTree(
|
||||
doc: PDFDocument,
|
||||
ctx: PDFContext,
|
||||
node: PDFDict,
|
||||
name: string,
|
||||
): number | undefined {
|
||||
const namesArr = node.get(PDFName.of('Names'));
|
||||
const resolved = namesArr instanceof PDFRef ? ctx.lookup(namesArr) : namesArr;
|
||||
if (resolved instanceof PDFArray) {
|
||||
for (let i = 0; i < resolved.size(); i += 2) {
|
||||
const key = str(ctx, resolved.get(i));
|
||||
if (key === name) {
|
||||
const val = resolved.get(i + 1);
|
||||
const valResolved = val instanceof PDFRef ? ctx.lookup(val) : val;
|
||||
if (valResolved instanceof PDFArray) {
|
||||
return resolveDestArray(doc, ctx, valResolved);
|
||||
}
|
||||
if (valResolved instanceof PDFDict) {
|
||||
const d = valResolved.get(PDFName.of('D'));
|
||||
const dResolved = d instanceof PDFRef ? ctx.lookup(d) : d;
|
||||
if (dResolved instanceof PDFArray) {
|
||||
return resolveDestArray(doc, ctx, dResolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const kidsArr = node.get(PDFName.of('Kids'));
|
||||
const kidsResolved = kidsArr instanceof PDFRef ? ctx.lookup(kidsArr) : kidsArr;
|
||||
if (kidsResolved instanceof PDFArray) {
|
||||
for (let i = 0; i < kidsResolved.size(); i++) {
|
||||
const kidRef = kidsResolved.get(i);
|
||||
const kid = kidRef instanceof PDFRef ? ctx.lookup(kidRef) : kidRef;
|
||||
if (kid instanceof PDFDict) {
|
||||
const limits = kid.get(PDFName.of('Limits'));
|
||||
const limitsResolved = limits instanceof PDFRef ? ctx.lookup(limits) : limits;
|
||||
if (limitsResolved instanceof PDFArray && limitsResolved.size() >= 2) {
|
||||
const lo = str(ctx, limitsResolved.get(0)) ?? '';
|
||||
const hi = str(ctx, limitsResolved.get(1)) ?? '';
|
||||
if (name < lo || name > hi) continue;
|
||||
}
|
||||
const result = searchNameTree(doc, ctx, kid, name);
|
||||
if (result !== undefined) return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractLinksFromPage(
|
||||
doc: PDFDocument,
|
||||
page: PDFPage,
|
||||
pageIndex: number,
|
||||
): PdfLibLink[] {
|
||||
const links: PdfLibLink[] = [];
|
||||
const ctx = doc.context;
|
||||
const { height: pageHeight } = page.getSize();
|
||||
|
||||
const annotsRaw = page.node.get(PDFName.of('Annots'));
|
||||
if (!annotsRaw) return links;
|
||||
|
||||
const annots = annotsRaw instanceof PDFRef ? ctx.lookup(annotsRaw) : annotsRaw;
|
||||
if (!(annots instanceof PDFArray)) return links;
|
||||
|
||||
for (let i = 0; i < annots.size(); i++) {
|
||||
try {
|
||||
const annotRaw = annots.get(i);
|
||||
const annot = annotRaw instanceof PDFRef ? ctx.lookup(annotRaw) : annotRaw;
|
||||
if (!(annot instanceof PDFDict)) continue;
|
||||
|
||||
const subtype = annot.get(PDFName.of('Subtype'));
|
||||
if (subtype?.toString() !== '/Link') continue;
|
||||
|
||||
const rectRaw = annot.get(PDFName.of('Rect'));
|
||||
const rect = rectRaw instanceof PDFRef ? ctx.lookup(rectRaw) : rectRaw;
|
||||
if (!(rect instanceof PDFArray) || rect.size() < 4) continue;
|
||||
|
||||
const x1 = num(ctx, rect.get(0));
|
||||
const y1 = num(ctx, rect.get(1));
|
||||
const x2 = num(ctx, rect.get(2));
|
||||
const y2 = num(ctx, rect.get(3));
|
||||
|
||||
const left = Math.min(x1, x2);
|
||||
const bottom = Math.min(y1, y2);
|
||||
const width = Math.abs(x2 - x1);
|
||||
const height = Math.abs(y2 - y1);
|
||||
|
||||
const top = pageHeight - bottom - height;
|
||||
|
||||
let linkType: LinkType = 'unknown';
|
||||
let targetPage: number | undefined;
|
||||
let uri: string | undefined;
|
||||
|
||||
const actionRaw = annot.get(PDFName.of('A'));
|
||||
const action = actionRaw instanceof PDFRef ? ctx.lookup(actionRaw) : actionRaw;
|
||||
|
||||
if (action instanceof PDFDict) {
|
||||
const actionType = action.get(PDFName.of('S'))?.toString();
|
||||
|
||||
if (actionType === '/URI') {
|
||||
linkType = 'external';
|
||||
uri = str(ctx, action.get(PDFName.of('URI')));
|
||||
} else if (actionType === '/GoTo') {
|
||||
linkType = 'internal';
|
||||
const dest = action.get(PDFName.of('D'));
|
||||
const destResolved = dest instanceof PDFRef ? ctx.lookup(dest) : dest;
|
||||
if (destResolved instanceof PDFArray) {
|
||||
targetPage = resolveDestArray(doc, ctx, destResolved);
|
||||
} else {
|
||||
const destName = str(ctx, destResolved);
|
||||
if (destName) {
|
||||
targetPage = resolveNamedDest(doc, ctx, destName);
|
||||
}
|
||||
}
|
||||
} else if (actionType === '/GoToR') {
|
||||
linkType = 'external';
|
||||
uri = str(ctx, action.get(PDFName.of('F')));
|
||||
} else if (actionType === '/Launch') {
|
||||
linkType = 'external';
|
||||
uri = str(ctx, action.get(PDFName.of('F')));
|
||||
}
|
||||
}
|
||||
|
||||
if (linkType === 'unknown') {
|
||||
const destRaw = annot.get(PDFName.of('Dest'));
|
||||
const dest = destRaw instanceof PDFRef ? ctx.lookup(destRaw) : destRaw;
|
||||
|
||||
if (dest instanceof PDFArray) {
|
||||
linkType = 'internal';
|
||||
targetPage = resolveDestArray(doc, ctx, dest);
|
||||
} else {
|
||||
const destName = str(ctx, dest);
|
||||
if (destName) {
|
||||
linkType = 'internal';
|
||||
targetPage = resolveNamedDest(doc, ctx, destName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
links.push({
|
||||
id: `pdflib-link-${pageIndex}-${i}`,
|
||||
annotIndex: i,
|
||||
rect: { x: left, y: top, width, height },
|
||||
type: linkType,
|
||||
targetPage,
|
||||
uri,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[usePdfLibLinks] Failed to parse annotation:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
export function usePdfLibLinks(
|
||||
pdfUrl: string | null,
|
||||
@ -350,20 +107,41 @@ export function usePdfLibLinks(
|
||||
return;
|
||||
}
|
||||
|
||||
if (cached.invalidCatalog) {
|
||||
setResult({ links: [], pdfPageWidth: 0, pdfPageHeight: 0, loading: false });
|
||||
releaseDocument(url);
|
||||
return;
|
||||
}
|
||||
|
||||
let pageData = cached.pageLinks.get(pageIndex);
|
||||
if (!pageData) {
|
||||
const pageCount = cached.doc.getPageCount();
|
||||
let pageCount: number;
|
||||
try {
|
||||
pageCount = cached.doc.getPageCount();
|
||||
} catch {
|
||||
cached.invalidCatalog = true;
|
||||
setResult({ links: [], pdfPageWidth: 0, pdfPageHeight: 0, loading: false });
|
||||
releaseDocument(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageIndex < 0 || pageIndex >= pageCount) {
|
||||
setResult({ links: [], pdfPageWidth: 0, pdfPageHeight: 0, loading: false });
|
||||
releaseDocument(url);
|
||||
return;
|
||||
}
|
||||
|
||||
const page = cached.doc.getPage(pageIndex);
|
||||
const { width, height } = page.getSize();
|
||||
const links = extractLinksFromPage(cached.doc, page, pageIndex);
|
||||
pageData = { links, width, height };
|
||||
cached.pageLinks.set(pageIndex, pageData);
|
||||
try {
|
||||
const page = cached.doc.getPage(pageIndex);
|
||||
const { width, height } = page.getSize();
|
||||
const links = extractLinksFromPage(cached.doc, page, pageIndex);
|
||||
pageData = { links, width, height };
|
||||
cached.pageLinks.set(pageIndex, pageData);
|
||||
} catch (pageError) {
|
||||
console.warn(`[usePdfLibLinks] Failed to read page ${pageIndex}:`, pageError);
|
||||
pageData = { links: [], width: 0, height: 0 };
|
||||
cached.pageLinks.set(pageIndex, pageData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!cancelled && mountedRef.current) {
|
||||
@ -377,7 +155,7 @@ export function usePdfLibLinks(
|
||||
|
||||
releaseDocument(url);
|
||||
} catch (error) {
|
||||
console.error('[usePdfLibLinks] Failed to extract links:', error);
|
||||
console.warn('[usePdfLibLinks] Failed to extract links:', error);
|
||||
if (!cancelled && mountedRef.current) {
|
||||
setResult({ links: [], pdfPageWidth: 0, pdfPageHeight: 0, loading: false });
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PDFDocument as PDFLibDocument, degrees, PageSizes } from 'pdf-lib';
|
||||
import { PDFDocument as PDFLibDocument, degrees, PageSizes } from '@cantoo/pdf-lib';
|
||||
import { downloadFile } from '@app/services/downloadService';
|
||||
import { PDFDocument, PDFPage } from '@app/types/pageEditor';
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PDFDocument, PageSizes } from 'pdf-lib';
|
||||
import { PDFDocument, PageSizes } from '@cantoo/pdf-lib';
|
||||
|
||||
export interface ImageToPdfOptions {
|
||||
imageResolution?: 'full' | 'reduced';
|
||||
|
||||
553
frontend/src/core/utils/pdfLinkUtils.ts
Normal file
553
frontend/src/core/utils/pdfLinkUtils.ts
Normal file
@ -0,0 +1,553 @@
|
||||
/**
|
||||
* pdfLinkUtils — Create, modify, and extract link annotations in PDF documents.
|
||||
*/
|
||||
import {
|
||||
PDFDocument,
|
||||
PDFPage,
|
||||
PDFName,
|
||||
PDFString,
|
||||
PDFArray,
|
||||
PDFDict,
|
||||
PDFRef,
|
||||
PDFContext,
|
||||
PDFNumber,
|
||||
PDFHexString,
|
||||
} from '@cantoo/pdf-lib';
|
||||
|
||||
export type LinkType = 'internal' | 'external' | 'unknown';
|
||||
export type LinkBorderStyle = 'solid' | 'dashed' | 'beveled' | 'inset' | 'underline';
|
||||
export type LinkHighlightMode = 'none' | 'invert' | 'outline' | 'push';
|
||||
|
||||
export interface PdfLibLink {
|
||||
id: string;
|
||||
/** Index of this annotation in the page's /Annots array (used for deletion matching). */
|
||||
annotIndex: number;
|
||||
/** Rectangle in PDF-page coordinate space (top-left origin, unscaled). */
|
||||
rect: { x: number; y: number; width: number; height: number };
|
||||
type: LinkType;
|
||||
/** 0-based target page index (internal links). */
|
||||
targetPage?: number;
|
||||
/** URI for external links. */
|
||||
uri?: string;
|
||||
/** Tooltip / alt text from the /Contents entry. */
|
||||
title?: string;
|
||||
/** RGB color of the link annotation border (each component 0–1). */
|
||||
color?: [number, number, number];
|
||||
/** Border width and style. */
|
||||
borderStyle?: { width: number; style: LinkBorderStyle };
|
||||
/** Visual feedback when the link is clicked. */
|
||||
highlightMode?: LinkHighlightMode;
|
||||
}
|
||||
|
||||
export interface CreateLinkOptions {
|
||||
/** Page to place the link on. */
|
||||
page: PDFPage;
|
||||
/** Link rectangle in PDF user-space coordinates (lower-left origin). */
|
||||
rect: { x: number; y: number; width: number; height: number };
|
||||
/** External URL (mutually exclusive with destinationPage). */
|
||||
url?: string;
|
||||
/** Internal destination page index, 0-based (mutually exclusive with url). */
|
||||
destinationPage?: number;
|
||||
/** Tooltip text shown on hover (stored in /Contents). */
|
||||
title?: string;
|
||||
/** RGB colour for the border, each component 0–1. Defaults to blue. */
|
||||
color?: [number, number, number];
|
||||
/** Border width in points. 0 = invisible (PDF convention). */
|
||||
borderWidth?: number;
|
||||
/** Border line style. */
|
||||
borderStyle?: LinkBorderStyle;
|
||||
/** Visual feedback when the link is clicked. */
|
||||
highlightMode?: LinkHighlightMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a link annotation on a PDF page.
|
||||
* Supports both external URIs and internal GoTo page destinations.
|
||||
*/
|
||||
export function createLinkAnnotation(
|
||||
pdfDoc: PDFDocument,
|
||||
options: CreateLinkOptions,
|
||||
): void {
|
||||
const {
|
||||
page,
|
||||
rect,
|
||||
url,
|
||||
destinationPage,
|
||||
title,
|
||||
color = [0, 0, 1],
|
||||
borderWidth = 0,
|
||||
borderStyle = 'solid',
|
||||
highlightMode = 'invert',
|
||||
} = options;
|
||||
|
||||
if (!url && destinationPage === undefined) {
|
||||
throw new Error('createLinkAnnotation: must provide either url or destinationPage');
|
||||
}
|
||||
if (url && destinationPage !== undefined) {
|
||||
throw new Error('createLinkAnnotation: url and destinationPage are mutually exclusive');
|
||||
}
|
||||
if (destinationPage !== undefined) {
|
||||
const pageCount = pdfDoc.getPageCount();
|
||||
if (destinationPage < 0 || destinationPage >= pageCount) {
|
||||
throw new RangeError(
|
||||
`createLinkAnnotation: destinationPage ${destinationPage} out of range [0, ${pageCount})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
throw new Error('createLinkAnnotation: rect dimensions must be positive');
|
||||
}
|
||||
if (color.some((c) => c < 0 || c > 1)) {
|
||||
throw new RangeError('createLinkAnnotation: color components must be between 0 and 1');
|
||||
}
|
||||
if (borderWidth < 0) {
|
||||
throw new RangeError('createLinkAnnotation: borderWidth must be non-negative');
|
||||
}
|
||||
|
||||
const ctx = pdfDoc.context;
|
||||
|
||||
const entries: Record<string, any> = {
|
||||
Type: 'Annot',
|
||||
Subtype: 'Link',
|
||||
Rect: [rect.x, rect.y, rect.x + rect.width, rect.y + rect.height],
|
||||
Border: [0, 0, borderWidth],
|
||||
C: color,
|
||||
H: PDFName.of(highlightModeCode(highlightMode)),
|
||||
};
|
||||
|
||||
if (title) {
|
||||
entries.Contents = PDFString.of(title);
|
||||
}
|
||||
|
||||
const annotDict = ctx.obj(entries);
|
||||
|
||||
if (borderStyle !== 'solid' && borderWidth > 0) {
|
||||
const bsDict = ctx.obj({
|
||||
W: borderWidth,
|
||||
S: PDFName.of(borderStyleCode(borderStyle)),
|
||||
});
|
||||
(annotDict as PDFDict).set(PDFName.of('BS'), bsDict);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
const actionDict = ctx.obj({
|
||||
S: 'URI',
|
||||
URI: PDFString.of(url),
|
||||
});
|
||||
(annotDict as PDFDict).set(PDFName.of('A'), actionDict);
|
||||
} else if (destinationPage !== undefined) {
|
||||
const destPage = pdfDoc.getPage(destinationPage);
|
||||
const destArray = ctx.obj([destPage.ref, 'XYZ', null, null, null]);
|
||||
(annotDict as PDFDict).set(PDFName.of('Dest'), destArray);
|
||||
}
|
||||
|
||||
const annotRef = ctx.register(annotDict);
|
||||
|
||||
const existingAnnots = page.node.get(PDFName.of('Annots'));
|
||||
if (existingAnnots) {
|
||||
const resolvedAnnots =
|
||||
existingAnnots instanceof PDFRef ? ctx.lookup(existingAnnots) : existingAnnots;
|
||||
if (resolvedAnnots instanceof PDFArray) {
|
||||
resolvedAnnots.push(annotRef);
|
||||
} else {
|
||||
page.node.set(PDFName.of('Annots'), ctx.obj([annotRef]));
|
||||
}
|
||||
} else {
|
||||
page.node.set(PDFName.of('Annots'), ctx.obj([annotRef]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a link annotation from a page by its index in the /Annots array.
|
||||
* Returns true if the annotation was found and removed.
|
||||
*/
|
||||
export function removeLinkAnnotation(
|
||||
pdfDoc: PDFDocument,
|
||||
page: PDFPage,
|
||||
annotIndex: number,
|
||||
): boolean {
|
||||
const ctx = pdfDoc.context;
|
||||
const annotsRaw = page.node.get(PDFName.of('Annots'));
|
||||
if (!annotsRaw) return false;
|
||||
|
||||
const annots =
|
||||
annotsRaw instanceof PDFRef ? ctx.lookup(annotsRaw) : annotsRaw;
|
||||
if (!(annots instanceof PDFArray)) return false;
|
||||
|
||||
if (annotIndex < 0 || annotIndex >= annots.size()) return false;
|
||||
|
||||
const entry = annots.get(annotIndex);
|
||||
if (entry instanceof PDFRef) {
|
||||
ctx.delete(entry);
|
||||
}
|
||||
|
||||
annots.remove(annotIndex);
|
||||
|
||||
if (annots.size() === 0) {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all link annotations from a given PDF page.
|
||||
*/
|
||||
export function extractLinksFromPage(
|
||||
doc: PDFDocument,
|
||||
page: PDFPage,
|
||||
pageIndex: number,
|
||||
): PdfLibLink[] {
|
||||
const links: PdfLibLink[] = [];
|
||||
const ctx = doc.context;
|
||||
const { height: pageHeight } = page.getSize();
|
||||
|
||||
const annotsRaw = page.node.get(PDFName.of('Annots'));
|
||||
if (!annotsRaw) return links;
|
||||
|
||||
const annots = annotsRaw instanceof PDFRef ? ctx.lookup(annotsRaw) : annotsRaw;
|
||||
if (!(annots instanceof PDFArray)) return links;
|
||||
|
||||
for (let i = 0; i < annots.size(); i++) {
|
||||
try {
|
||||
const annotRaw = annots.get(i);
|
||||
const annot = annotRaw instanceof PDFRef ? ctx.lookup(annotRaw) : annotRaw;
|
||||
if (!(annot instanceof PDFDict)) continue;
|
||||
|
||||
const subtype = annot.get(PDFName.of('Subtype'));
|
||||
if (subtype?.toString() !== '/Link') continue;
|
||||
|
||||
const rectRaw = annot.get(PDFName.of('Rect'));
|
||||
const rect = rectRaw instanceof PDFRef ? ctx.lookup(rectRaw) : rectRaw;
|
||||
if (!(rect instanceof PDFArray) || rect.size() < 4) continue;
|
||||
|
||||
const x1 = num(ctx, rect.get(0));
|
||||
const y1 = num(ctx, rect.get(1));
|
||||
const x2 = num(ctx, rect.get(2));
|
||||
const y2 = num(ctx, rect.get(3));
|
||||
|
||||
const left = Math.min(x1, x2);
|
||||
const bottom = Math.min(y1, y2);
|
||||
const width = Math.abs(x2 - x1);
|
||||
const height = Math.abs(y2 - y1);
|
||||
|
||||
const top = pageHeight - bottom - height;
|
||||
|
||||
let linkType: LinkType = 'unknown';
|
||||
let targetPage: number | undefined;
|
||||
let uri: string | undefined;
|
||||
|
||||
const actionRaw = annot.get(PDFName.of('A'));
|
||||
const action = actionRaw instanceof PDFRef ? ctx.lookup(actionRaw) : actionRaw;
|
||||
|
||||
if (action instanceof PDFDict) {
|
||||
const actionType = action.get(PDFName.of('S'))?.toString();
|
||||
|
||||
if (actionType === '/URI') {
|
||||
linkType = 'external';
|
||||
uri = str(ctx, action.get(PDFName.of('URI')));
|
||||
} else if (actionType === '/GoTo') {
|
||||
linkType = 'internal';
|
||||
const dest = action.get(PDFName.of('D'));
|
||||
const destResolved = dest instanceof PDFRef ? ctx.lookup(dest) : dest;
|
||||
if (destResolved instanceof PDFArray) {
|
||||
targetPage = resolveDestArray(doc, ctx, destResolved);
|
||||
} else {
|
||||
const destName = str(ctx, destResolved);
|
||||
if (destName) {
|
||||
targetPage = resolveNamedDest(doc, ctx, destName);
|
||||
}
|
||||
}
|
||||
} else if (actionType === '/GoToR' || actionType === '/Launch') {
|
||||
linkType = 'external';
|
||||
uri = str(ctx, action.get(PDFName.of('F')));
|
||||
}
|
||||
}
|
||||
|
||||
if (linkType === 'unknown') {
|
||||
const destRaw = annot.get(PDFName.of('Dest'));
|
||||
const dest = destRaw instanceof PDFRef ? ctx.lookup(destRaw) : destRaw;
|
||||
|
||||
if (dest instanceof PDFArray) {
|
||||
linkType = 'internal';
|
||||
targetPage = resolveDestArray(doc, ctx, dest);
|
||||
} else {
|
||||
const destName = str(ctx, dest);
|
||||
if (destName) {
|
||||
linkType = 'internal';
|
||||
targetPage = resolveNamedDest(doc, ctx, destName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const title = extractTitle(ctx, annot);
|
||||
const color = extractColor(ctx, annot);
|
||||
const borderStyle = extractBorderStyle(ctx, annot);
|
||||
const highlightMode = parseHighlightMode(ctx, annot.get(PDFName.of('H')));
|
||||
|
||||
links.push({
|
||||
id: `pdflib-link-${pageIndex}-${i}`,
|
||||
annotIndex: i,
|
||||
rect: { x: left, y: top, width, height },
|
||||
type: linkType,
|
||||
targetPage,
|
||||
uri,
|
||||
title,
|
||||
color,
|
||||
borderStyle,
|
||||
highlightMode,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[pdfLinkUtils] Failed to parse annotation:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private Helpers (Internal to extraction logic)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function num(ctx: PDFContext, value: unknown): number {
|
||||
const resolved = value instanceof PDFRef ? ctx.lookup(value) : value;
|
||||
if (resolved instanceof PDFNumber) return resolved.asNumber();
|
||||
if (typeof resolved === 'number') return resolved;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function str(ctx: PDFContext, value: unknown): string | undefined {
|
||||
const resolved = value instanceof PDFRef ? ctx.lookup(value) : value;
|
||||
if (resolved instanceof PDFString) return resolved.decodeText();
|
||||
if (resolved instanceof PDFHexString) return resolved.decodeText();
|
||||
if (typeof resolved === 'string') return resolved;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolvePageIndex(doc: PDFDocument, pageRef: PDFRef): number | undefined {
|
||||
const pages = doc.getPages();
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const ref = pages[i].ref;
|
||||
if (
|
||||
ref === pageRef ||
|
||||
(ref.objectNumber === pageRef.objectNumber &&
|
||||
ref.generationNumber === pageRef.generationNumber)
|
||||
) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveDestArray(
|
||||
doc: PDFDocument,
|
||||
ctx: PDFContext,
|
||||
destArr: PDFArray,
|
||||
): number | undefined {
|
||||
if (destArr.size() < 1) return undefined;
|
||||
const first = destArr.get(0);
|
||||
if (first instanceof PDFRef) {
|
||||
return resolvePageIndex(doc, first);
|
||||
}
|
||||
const n = num(ctx, first);
|
||||
if (typeof n === 'number' && n >= 0) return n;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveNamedDest(
|
||||
doc: PDFDocument,
|
||||
ctx: PDFContext,
|
||||
name: string,
|
||||
): number | undefined {
|
||||
try {
|
||||
const catalog = doc.catalog;
|
||||
|
||||
const namesRaw = catalog.get(PDFName.of('Names'));
|
||||
const namesDict = namesRaw instanceof PDFRef ? ctx.lookup(namesRaw) : namesRaw;
|
||||
if (namesDict instanceof PDFDict) {
|
||||
const destsRaw = namesDict.get(PDFName.of('Dests'));
|
||||
const destsTree = destsRaw instanceof PDFRef ? ctx.lookup(destsRaw) : destsRaw;
|
||||
if (destsTree instanceof PDFDict) {
|
||||
const result = searchNameTree(doc, ctx, destsTree, name);
|
||||
if (result !== undefined) return result;
|
||||
}
|
||||
}
|
||||
|
||||
const destsRaw = catalog.get(PDFName.of('Dests'));
|
||||
const destsDict = destsRaw instanceof PDFRef ? ctx.lookup(destsRaw) : destsRaw;
|
||||
if (destsDict instanceof PDFDict) {
|
||||
const dest = destsDict.get(PDFName.of(name));
|
||||
const destResolved = dest instanceof PDFRef ? ctx.lookup(dest) : dest;
|
||||
if (destResolved instanceof PDFArray) {
|
||||
return resolveDestArray(doc, ctx, destResolved);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function searchNameTree(
|
||||
doc: PDFDocument,
|
||||
ctx: PDFContext,
|
||||
node: PDFDict,
|
||||
name: string,
|
||||
): number | undefined {
|
||||
const namesArr = node.get(PDFName.of('Names'));
|
||||
const resolved = namesArr instanceof PDFRef ? ctx.lookup(namesArr) : namesArr;
|
||||
if (resolved instanceof PDFArray) {
|
||||
for (let i = 0; i < resolved.size(); i += 2) {
|
||||
const key = str(ctx, resolved.get(i));
|
||||
if (key === name) {
|
||||
const val = resolved.get(i + 1);
|
||||
const valResolved = val instanceof PDFRef ? ctx.lookup(val) : val;
|
||||
if (valResolved instanceof PDFArray) {
|
||||
return resolveDestArray(doc, ctx, valResolved);
|
||||
}
|
||||
if (valResolved instanceof PDFDict) {
|
||||
const d = valResolved.get(PDFName.of('D'));
|
||||
const dResolved = d instanceof PDFRef ? ctx.lookup(d) : d;
|
||||
if (dResolved instanceof PDFArray) {
|
||||
return resolveDestArray(doc, ctx, dResolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const kidsArr = node.get(PDFName.of('Kids'));
|
||||
const kidsResolved = kidsArr instanceof PDFRef ? ctx.lookup(kidsArr) : kidsArr;
|
||||
if (kidsResolved instanceof PDFArray) {
|
||||
for (let i = 0; i < kidsResolved.size(); i++) {
|
||||
const kidRef = kidsResolved.get(i);
|
||||
const kid = kidRef instanceof PDFRef ? ctx.lookup(kidRef) : kidRef;
|
||||
if (kid instanceof PDFDict) {
|
||||
const limits = kid.get(PDFName.of('Limits'));
|
||||
const limitsResolved = limits instanceof PDFRef ? ctx.lookup(limits) : limits;
|
||||
if (limitsResolved instanceof PDFArray && limitsResolved.size() >= 2) {
|
||||
const lo = str(ctx, limitsResolved.get(0)) ?? '';
|
||||
const hi = str(ctx, limitsResolved.get(1)) ?? '';
|
||||
if (name < lo || name > hi) continue;
|
||||
}
|
||||
const result = searchNameTree(doc, ctx, kid, name);
|
||||
if (result !== undefined) return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function borderStyleCode(style: LinkBorderStyle): string {
|
||||
switch (style) {
|
||||
case 'dashed': return 'D';
|
||||
case 'beveled': return 'B';
|
||||
case 'inset': return 'I';
|
||||
case 'underline': return 'U';
|
||||
default: return 'S';
|
||||
}
|
||||
}
|
||||
|
||||
function highlightModeCode(mode: LinkHighlightMode): string {
|
||||
switch (mode) {
|
||||
case 'none': return 'N';
|
||||
case 'outline': return 'O';
|
||||
case 'push': return 'P';
|
||||
default: return 'I';
|
||||
}
|
||||
}
|
||||
|
||||
function parseBorderStyleName(ctx: PDFContext, value: unknown): LinkBorderStyle {
|
||||
if (!value) return 'solid';
|
||||
const resolved = value instanceof PDFRef ? ctx.lookup(value) : value;
|
||||
const s = resolved instanceof PDFName ? resolved.decodeText() : String(resolved);
|
||||
switch (s) {
|
||||
case 'D': return 'dashed';
|
||||
case 'B': return 'beveled';
|
||||
case 'I': return 'inset';
|
||||
case 'U': return 'underline';
|
||||
default: return 'solid';
|
||||
}
|
||||
}
|
||||
|
||||
function parseHighlightMode(ctx: PDFContext, value: unknown): LinkHighlightMode {
|
||||
if (!value) return 'invert';
|
||||
const resolved = value instanceof PDFRef ? ctx.lookup(value) : value;
|
||||
const s = resolved instanceof PDFName ? resolved.decodeText() : String(resolved);
|
||||
switch (s) {
|
||||
case 'N': return 'none';
|
||||
case 'I': return 'invert';
|
||||
case 'O': return 'outline';
|
||||
case 'P': return 'push';
|
||||
default: return 'invert';
|
||||
}
|
||||
}
|
||||
|
||||
function extractBorderStyle(
|
||||
ctx: PDFContext,
|
||||
annot: PDFDict,
|
||||
): PdfLibLink['borderStyle'] | undefined {
|
||||
const bsRaw = annot.get(PDFName.of('BS'));
|
||||
const bs = bsRaw instanceof PDFRef ? ctx.lookup(bsRaw) : bsRaw;
|
||||
if (bs instanceof PDFDict) {
|
||||
const w = bs.get(PDFName.of('W'));
|
||||
const s = bs.get(PDFName.of('S'));
|
||||
return {
|
||||
width: num(ctx, w) || 1,
|
||||
style: parseBorderStyleName(ctx, s),
|
||||
};
|
||||
}
|
||||
|
||||
const borderRaw = annot.get(PDFName.of('Border'));
|
||||
const border = borderRaw instanceof PDFRef ? ctx.lookup(borderRaw) : borderRaw;
|
||||
if (border instanceof PDFArray && border.size() >= 3) {
|
||||
const width = num(ctx, border.get(2));
|
||||
const style: LinkBorderStyle = border.size() >= 4 ? 'dashed' : 'solid';
|
||||
return { width, style };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractColor(
|
||||
ctx: PDFContext,
|
||||
annot: PDFDict,
|
||||
): [number, number, number] | undefined {
|
||||
const cRaw = annot.get(PDFName.of('C'));
|
||||
const c = cRaw instanceof PDFRef ? ctx.lookup(cRaw) : cRaw;
|
||||
if (!(c instanceof PDFArray)) return undefined;
|
||||
|
||||
const len = c.size();
|
||||
if (len === 3) {
|
||||
return [num(ctx, c.get(0)), num(ctx, c.get(1)), num(ctx, c.get(2))];
|
||||
}
|
||||
if (len === 1) {
|
||||
const g = num(ctx, c.get(0));
|
||||
return [g, g, g];
|
||||
}
|
||||
if (len === 4) {
|
||||
const cVal = num(ctx, c.get(0));
|
||||
const m = num(ctx, c.get(1));
|
||||
const y = num(ctx, c.get(2));
|
||||
const k = num(ctx, c.get(3));
|
||||
return [
|
||||
(1 - cVal) * (1 - k),
|
||||
(1 - m) * (1 - k),
|
||||
(1 - y) * (1 - k),
|
||||
];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractTitle(
|
||||
ctx: PDFContext,
|
||||
annot: PDFDict,
|
||||
): string | undefined {
|
||||
const raw = annot.get(PDFName.of('Contents'));
|
||||
const resolved = raw instanceof PDFRef ? ctx.lookup(raw) : raw;
|
||||
if (resolved instanceof PDFString || resolved instanceof PDFHexString) {
|
||||
return resolved.decodeText();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
import { PDFDocument, rgb } from 'pdf-lib';
|
||||
import { PdfAnnotationSubtype } from '@embedpdf/models';
|
||||
import { generateThumbnailWithMetadata } from '@app/utils/thumbnailUtils';
|
||||
import { createProcessedFile, createChildStub } from '@app/contexts/file/fileActions';
|
||||
import { createStirlingFile, StirlingFile, FileId, StirlingFileStub } from '@app/types/fileContext';
|
||||
import type { SignatureAPI } from '@app/components/viewer/viewerTypes';
|
||||
import {PDFDocument, rgb} from '@cantoo/pdf-lib';
|
||||
import {PdfAnnotationSubtype} from '@embedpdf/models';
|
||||
import {generateThumbnailWithMetadata} from '@app/utils/thumbnailUtils';
|
||||
import {createChildStub, createProcessedFile} from '@app/contexts/file/fileActions';
|
||||
import {createStirlingFile, FileId, StirlingFile, StirlingFileStub} from '@app/types/fileContext';
|
||||
import type {SignatureAPI} from '@app/components/viewer/viewerTypes';
|
||||
|
||||
interface MinimalFileContextSelectors {
|
||||
getAllFileIds: () => FileId[];
|
||||
@ -38,29 +38,20 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
|
||||
if (signatureApiRef?.current) {
|
||||
|
||||
// Get actual page count from viewer
|
||||
const scrollState = getScrollState();
|
||||
const totalPages = scrollState.totalPages;
|
||||
|
||||
// Check only actual pages that exist in the document
|
||||
for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
try {
|
||||
const pageAnnotations = await signatureApiRef.current.getPageAnnotations(pageIndex);
|
||||
if (pageAnnotations && pageAnnotations.length > 0) {
|
||||
// Filter to only include annotations added in this session
|
||||
const sessionAnnotations = pageAnnotations.filter(annotation => {
|
||||
// Check if this annotation has stored image data (indicates it was added this session)
|
||||
const hasStoredImageData = annotation.id && getImageData(annotation.id);
|
||||
|
||||
// Also check if it has image data directly in the annotation (new signatures)
|
||||
const hasDirectImageData = annotation.imageData || annotation.appearance ||
|
||||
annotation.stampData || annotation.imageSrc ||
|
||||
annotation.contents || annotation.data;
|
||||
|
||||
const isSessionAnnotation = hasStoredImageData || (hasDirectImageData && typeof hasDirectImageData === 'string' && hasDirectImageData.startsWith('data:image'));
|
||||
|
||||
|
||||
return isSessionAnnotation;
|
||||
return hasStoredImageData || (hasDirectImageData && typeof hasDirectImageData === 'string' && hasDirectImageData.startsWith('data:image'));
|
||||
});
|
||||
|
||||
if (sessionAnnotations.length > 0) {
|
||||
@ -74,12 +65,11 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
}
|
||||
|
||||
// Step 2: Delete ONLY session annotations from EmbedPDF before export (they'll be rendered manually)
|
||||
// Leave old annotations alone - they will remain as annotations in the PDF
|
||||
if (allAnnotations.length > 0 && signatureApiRef?.current) {
|
||||
for (const pageData of allAnnotations) {
|
||||
for (const annotation of pageData.annotations) {
|
||||
try {
|
||||
await signatureApiRef.current.deleteAnnotation(annotation.id, pageData.pageIndex);
|
||||
signatureApiRef.current.deleteAnnotation(annotation.id, pageData.pageIndex);
|
||||
} catch (deleteError) {
|
||||
console.warn(`Failed to delete annotation ${annotation.id}:`, deleteError);
|
||||
}
|
||||
@ -96,17 +86,12 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
|
||||
if (pdfArrayBuffer) {
|
||||
|
||||
// Try loading with more permissive PDF-lib options
|
||||
|
||||
// Convert ArrayBuffer to File
|
||||
const blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' });
|
||||
|
||||
// Get the current file - try from originalFile first, then from all files
|
||||
let currentFile = originalFile;
|
||||
if (!currentFile) {
|
||||
const allFileIds = selectors.getAllFileIds();
|
||||
if (allFileIds.length > 0) {
|
||||
// Use activeFileIndex if provided, otherwise default to 0
|
||||
const fileIndex = activeFileIndex !== undefined && activeFileIndex < allFileIds.length ? activeFileIndex : 0;
|
||||
const fileStub = selectors.getStirlingFileStub(allFileIds[fileIndex]);
|
||||
const fileObject = selectors.getFile(allFileIds[fileIndex]);
|
||||
@ -128,7 +113,6 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
try {
|
||||
const pdfArrayBufferForFlattening = await signedFile.arrayBuffer();
|
||||
|
||||
// Try different loading options to handle problematic PDFs
|
||||
let pdfDoc: PDFDocument;
|
||||
try {
|
||||
pdfDoc = await PDFDocument.load(pdfArrayBufferForFlattening, {
|
||||
@ -139,7 +123,6 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
} catch {
|
||||
console.warn('Failed to load with standard options, trying createProxy...');
|
||||
try {
|
||||
// Create a fresh PDF and copy pages instead of modifying
|
||||
pdfDoc = await PDFDocument.create();
|
||||
const sourcePdf = await PDFDocument.load(pdfArrayBufferForFlattening, {
|
||||
ignoreEncryption: true,
|
||||
@ -169,22 +152,18 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
const rect = annotation.rect || annotation.bounds || annotation.rectangle || annotation.position;
|
||||
|
||||
if (rect) {
|
||||
// Extract original annotation position and size
|
||||
const originalX = rect.origin?.x || rect.x || rect.left || 0;
|
||||
const originalY = rect.origin?.y || rect.y || rect.top || 0;
|
||||
const width = rect.size?.width || rect.width || 100;
|
||||
const height = rect.size?.height || rect.height || 50;
|
||||
|
||||
// Convert EmbedPDF coordinates to PDF-lib coordinates
|
||||
const pdfX = originalX;
|
||||
const pdfY = pageHeight - originalY - height;
|
||||
|
||||
|
||||
// Try to get annotation image data
|
||||
let imageDataUrl = annotation.imageData || annotation.appearance || annotation.stampData ||
|
||||
annotation.imageSrc || annotation.contents || annotation.data;
|
||||
|
||||
// If no image data found directly, try to get it from storage
|
||||
if (!imageDataUrl && annotation.id) {
|
||||
const storedImageData = getImageData(annotation.id);
|
||||
if (storedImageData) {
|
||||
@ -192,24 +171,63 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
}
|
||||
}
|
||||
|
||||
if (imageDataUrl && typeof imageDataUrl === 'string' && imageDataUrl.startsWith('data:image')) {
|
||||
if (imageDataUrl && typeof imageDataUrl === 'string' && imageDataUrl.startsWith('data:image/svg+xml')) {
|
||||
let svgRendered = false;
|
||||
try {
|
||||
const svgContent = decodeSvgDataUrl(imageDataUrl);
|
||||
if (svgContent && typeof (page as any).drawSvg === 'function') {
|
||||
// drawSvg from @cantoo/pdf-lib renders SVG natively as
|
||||
(page as any).drawSvg(svgContent, {
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
svgRendered = true;
|
||||
}
|
||||
} catch (svgError) {
|
||||
console.warn('Native SVG embed failed, falling back to raster:', svgError);
|
||||
}
|
||||
|
||||
if (!svgRendered) {
|
||||
try {
|
||||
const pngBytes = await rasteriseSvgToPng(imageDataUrl, width * 2, height * 2);
|
||||
if (pngBytes) {
|
||||
const image = await pdfDoc.embedPng(pngBytes);
|
||||
page.drawImage(image, { x: pdfX, y: pdfY, width, height });
|
||||
svgRendered = true;
|
||||
}
|
||||
} catch (rasterError) {
|
||||
console.error('SVG raster fallback also failed:', rasterError);
|
||||
}
|
||||
}
|
||||
|
||||
if (!svgRendered) {
|
||||
page.drawRectangle({
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
width: width,
|
||||
height: height,
|
||||
borderColor: rgb(0.8, 0, 0),
|
||||
borderWidth: 1,
|
||||
color: rgb(1, 0.95, 0.95),
|
||||
opacity: 0.7,
|
||||
});
|
||||
}
|
||||
} else if (imageDataUrl && typeof imageDataUrl === 'string' && imageDataUrl.startsWith('data:image')) {
|
||||
try {
|
||||
// Convert data URL to bytes
|
||||
const base64Data = imageDataUrl.split(',')[1];
|
||||
const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
|
||||
|
||||
// Embed image in PDF based on data URL type
|
||||
let image;
|
||||
if (imageDataUrl.includes('data:image/jpeg') || imageDataUrl.includes('data:image/jpg')) {
|
||||
image = await pdfDoc.embedJpg(imageBytes);
|
||||
} else if (imageDataUrl.includes('data:image/png')) {
|
||||
image = await pdfDoc.embedPng(imageBytes);
|
||||
} else {
|
||||
// Default to PNG for other formats (including converted SVGs)
|
||||
image = await pdfDoc.embedPng(imageBytes);
|
||||
}
|
||||
|
||||
// Draw image on page at annotation position
|
||||
page.drawImage(image, {
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
@ -221,8 +239,6 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
console.error('Failed to render image annotation:', imageError);
|
||||
}
|
||||
} else if (annotation.content || annotation.text) {
|
||||
console.warn('Rendering text annotation instead');
|
||||
// Handle text annotations
|
||||
page.drawText(annotation.content || annotation.text, {
|
||||
x: pdfX,
|
||||
y: pdfY + height - 12, // Adjust for text baseline
|
||||
@ -230,26 +246,17 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
color: rgb(0, 0, 0)
|
||||
});
|
||||
} else if (annotation.type === PdfAnnotationSubtype.INK || annotation.type === PdfAnnotationSubtype.LINE) {
|
||||
// Handle ink annotations (drawn signatures)
|
||||
page.drawRectangle({
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
width: width,
|
||||
height: height,
|
||||
borderColor: rgb(0, 0, 0),
|
||||
borderWidth: 2,
|
||||
color: rgb(0.9, 0.9, 0.9), // Light gray background
|
||||
opacity: 0.8
|
||||
});
|
||||
|
||||
page.drawText('Drawn Signature', {
|
||||
x: pdfX + 5,
|
||||
y: pdfY + height / 2,
|
||||
size: 10,
|
||||
color: rgb(0, 0, 0)
|
||||
borderWidth: 1,
|
||||
color: rgb(0.95, 0.95, 0.95),
|
||||
opacity: 0.6
|
||||
});
|
||||
} else {
|
||||
// Handle other annotation types
|
||||
page.drawRectangle({
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
@ -257,7 +264,7 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
height: height,
|
||||
borderColor: rgb(1, 0, 0),
|
||||
borderWidth: 2,
|
||||
color: rgb(1, 1, 0), // Yellow background
|
||||
color: rgb(1, 1, 0),
|
||||
opacity: 0.5
|
||||
});
|
||||
}
|
||||
@ -270,7 +277,6 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
}
|
||||
|
||||
|
||||
// Save the PDF with rendered annotations
|
||||
const flattenedPdfBytes = await pdfDoc.save({ useObjectStreams: false, addDefaultPage: false });
|
||||
|
||||
const arrayBuffer = new ArrayBuffer(flattenedPdfBytes.length);
|
||||
@ -284,11 +290,9 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
}
|
||||
}
|
||||
|
||||
// Generate thumbnail and metadata for the signed file
|
||||
const thumbnailResult = await generateThumbnailWithMetadata(signedFile);
|
||||
const processedFileMetadata = createProcessedFile(thumbnailResult.pageCount, thumbnailResult.thumbnail);
|
||||
|
||||
// Prepare input file data for replacement
|
||||
const inputFileIds: FileId[] = [currentFile.fileId];
|
||||
|
||||
const record = selectors.getStirlingFileStub(currentFile.fileId);
|
||||
@ -297,7 +301,6 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create output stub and file as a child of the original (increments version)
|
||||
const outputStub = createChildStub(
|
||||
record,
|
||||
{ toolId: 'sign', timestamp: Date.now() },
|
||||
@ -307,7 +310,6 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
);
|
||||
const outputStirlingFile = createStirlingFile(signedFile, outputStub.id);
|
||||
|
||||
// Return the flattened file data for consumption by caller
|
||||
return {
|
||||
inputFileIds,
|
||||
outputStirlingFile,
|
||||
@ -321,3 +323,61 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an SVG data URL to its raw XML string.
|
||||
* Handles both base64-encoded and URI-encoded SVG data URLs.
|
||||
*/
|
||||
function decodeSvgDataUrl(dataUrl: string): string | null {
|
||||
try {
|
||||
if (dataUrl.includes(';base64,')) {
|
||||
const base64 = dataUrl.split(',')[1];
|
||||
return atob(base64);
|
||||
}
|
||||
// URI-encoded SVG
|
||||
const encoded = dataUrl.split(',')[1];
|
||||
return decodeURIComponent(encoded);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rasterise an SVG data URL to PNG bytes via an offscreen canvas.
|
||||
* Used as a fallback when native SVG embedding is unavailable.
|
||||
*/
|
||||
function rasteriseSvgToPng(svgDataUrl: string, width: number, height: number): Promise<Uint8Array | null> {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.max(1, Math.round(width));
|
||||
canvas.height = Math.max(1, Math.round(height));
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
blob.arrayBuffer().then(
|
||||
(buf) => resolve(new Uint8Array(buf)),
|
||||
() => resolve(null),
|
||||
);
|
||||
},
|
||||
'image/png',
|
||||
);
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
img.onerror = () => resolve(null);
|
||||
img.src = svgDataUrl;
|
||||
});
|
||||
}
|
||||
|
||||
@ -60,8 +60,8 @@ function WidgetInputInner({
|
||||
|
||||
const borderColor = error ? '#f44336' : (isActive ? '#2196F3' : 'rgba(33, 150, 243, 0.4)');
|
||||
const bgColor = error
|
||||
? 'rgba(244, 67, 54, 0.08)'
|
||||
: (isActive ? 'rgba(33, 150, 243, 0.08)' : 'rgba(255, 255, 255, 0.85)');
|
||||
? '#FFEBEE' // Red 50 (Opaque)
|
||||
: (isActive ? '#E3F2FD' : '#FFFFFF'); // Blue 50 (Opaque) : White (Opaque)
|
||||
|
||||
const commonStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
@ -84,10 +84,41 @@ function WidgetInputInner({
|
||||
alignItems: field.multiline ? 'stretch' : 'center',
|
||||
};
|
||||
|
||||
// Scale font size with the widget height (using Y scale as a proxy for uniform font scaling).
|
||||
// PDF form fields use fontSize=0 to mean "auto-size" (scale to fit the box).
|
||||
// For single-line fields (e.g. Title), scale closer to the box height.
|
||||
// For multiline fields (e.g. Description), use a smaller capped size.
|
||||
const stopPropagation = (e: React.SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
// Also stop immediate propagation to native listeners to block non-React subscribers
|
||||
if (e.nativeEvent) {
|
||||
e.nativeEvent.stopImmediatePropagation?.();
|
||||
}
|
||||
};
|
||||
|
||||
const commonProps = {
|
||||
style: commonStyle,
|
||||
onPointerDown: stopPropagation,
|
||||
onPointerUp: stopPropagation,
|
||||
onMouseDown: stopPropagation,
|
||||
onMouseUp: stopPropagation,
|
||||
onClick: stopPropagation,
|
||||
onDoubleClick: stopPropagation,
|
||||
onKeyDown: stopPropagation,
|
||||
onKeyUp: stopPropagation,
|
||||
onKeyPress: stopPropagation,
|
||||
onDragStart: stopPropagation,
|
||||
onSelect: stopPropagation,
|
||||
onContextMenu: stopPropagation,
|
||||
};
|
||||
|
||||
const captureStopProps = {
|
||||
onPointerDownCapture: stopPropagation,
|
||||
onPointerUpCapture: stopPropagation,
|
||||
onMouseDownCapture: stopPropagation,
|
||||
onMouseUpCapture: stopPropagation,
|
||||
onClickCapture: stopPropagation,
|
||||
onKeyDownCapture: stopPropagation,
|
||||
onKeyUpCapture: stopPropagation,
|
||||
onKeyPressCapture: stopPropagation,
|
||||
};
|
||||
|
||||
const fontSize = widget.fontSize
|
||||
? widget.fontSize * scaleY
|
||||
: field.multiline
|
||||
@ -115,7 +146,7 @@ function WidgetInputInner({
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<div style={commonStyle} title={error || field.tooltip || field.label}>
|
||||
<div {...commonProps} title={error || field.tooltip || field.label}>
|
||||
{field.multiline ? (
|
||||
<textarea
|
||||
value={value}
|
||||
@ -129,6 +160,7 @@ function WidgetInputInner({
|
||||
overflow: 'auto',
|
||||
paddingTop: `${Math.max(1, 2 * scaleY)}px`,
|
||||
}}
|
||||
{...captureStopProps}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
@ -144,6 +176,7 @@ function WidgetInputInner({
|
||||
aria-required={field.required}
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={error ? `${field.name}-error` : undefined}
|
||||
{...captureStopProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -156,6 +189,7 @@ function WidgetInputInner({
|
||||
const onValue = widget.exportValue || 'Yes';
|
||||
return (
|
||||
<div
|
||||
{...commonProps}
|
||||
style={{
|
||||
...commonStyle,
|
||||
display: 'flex',
|
||||
@ -164,10 +198,11 @@ function WidgetInputInner({
|
||||
cursor: field.readOnly ? 'default' : 'pointer',
|
||||
}}
|
||||
title={error || field.tooltip || field.label}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (field.readOnly) return;
|
||||
handleFocus();
|
||||
onChange(field.name, isChecked ? 'Off' : onValue);
|
||||
stopPropagation(e);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
@ -206,7 +241,7 @@ function WidgetInputInner({
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={commonStyle} title={error || field.tooltip || field.label}>
|
||||
<div {...commonProps} title={error || field.tooltip || field.label}>
|
||||
<select
|
||||
id={inputId}
|
||||
value={selectValue}
|
||||
@ -224,6 +259,7 @@ function WidgetInputInner({
|
||||
aria-label={field.label || field.name}
|
||||
aria-required={field.required}
|
||||
aria-invalid={!!error}
|
||||
{...captureStopProps}
|
||||
>
|
||||
{!field.multiSelect && <option value="">— select —</option>}
|
||||
{(field.options || []).map((opt, idx) => (
|
||||
@ -243,6 +279,7 @@ function WidgetInputInner({
|
||||
const isSelected = value === optionValue;
|
||||
return (
|
||||
<div
|
||||
{...commonProps}
|
||||
style={{
|
||||
...commonStyle,
|
||||
display: 'flex',
|
||||
@ -251,10 +288,11 @@ function WidgetInputInner({
|
||||
cursor: field.readOnly ? 'default' : 'pointer',
|
||||
}}
|
||||
title={error || field.tooltip || `${field.label}: ${optionValue}`}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (field.readOnly || value === optionValue) return; // Don't deselect radio buttons
|
||||
handleFocus();
|
||||
onChange(field.name, optionValue);
|
||||
stopPropagation(e);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
@ -276,6 +314,7 @@ function WidgetInputInner({
|
||||
// Just render a highlighted area — not editable
|
||||
return (
|
||||
<div
|
||||
{...commonProps}
|
||||
style={{
|
||||
...commonStyle,
|
||||
background: 'rgba(200,200,200,0.3)',
|
||||
@ -289,7 +328,7 @@ function WidgetInputInner({
|
||||
|
||||
default:
|
||||
return (
|
||||
<div style={commonStyle} title={field.tooltip || field.label}>
|
||||
<div {...commonProps} title={field.tooltip || field.label}>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
@ -297,6 +336,7 @@ function WidgetInputInner({
|
||||
onFocus={handleFocus}
|
||||
disabled={field.readOnly}
|
||||
style={inputBaseStyle}
|
||||
{...captureStopProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -16,7 +16,8 @@
|
||||
*/
|
||||
import { PDFDocument, PDFForm, PDFField, PDFTextField, PDFCheckBox,
|
||||
PDFDropdown, PDFRadioGroup, PDFOptionList, PDFButton, PDFSignature,
|
||||
PDFName, PDFDict, PDFArray, PDFNumber, PDFRef, PDFPage } from 'pdf-lib';
|
||||
PDFName, PDFDict, PDFArray, PDFNumber, PDFRef, PDFPage,
|
||||
PDFString, PDFHexString } from '@cantoo/pdf-lib';
|
||||
import type { FormField, FormFieldType, WidgetCoordinates } from '@proprietary/tools/formFill/types';
|
||||
import type { IFormDataProvider } from '@proprietary/tools/formFill/providers/types';
|
||||
|
||||
@ -141,11 +142,18 @@ function extractWidgets(
|
||||
if (ap instanceof PDFDict) {
|
||||
const normal = ap.lookup(PDFName.of('N'));
|
||||
if (normal instanceof PDFDict) {
|
||||
// The keys of /N (other than /Off) are the export values
|
||||
const keys = normal.entries()
|
||||
.map(([k]) => k.decodeText())
|
||||
.filter(k => k !== 'Off');
|
||||
if (keys.length > 0) exportValue = keys[0];
|
||||
// The keys of /N (other than /Off) are the export values.
|
||||
// PDFDict.entries() reliably returns [PDFName, PDFObject][] in
|
||||
// @cantoo/pdf-lib — no optional chaining needed.
|
||||
try {
|
||||
const entries = normal.entries();
|
||||
const keys = entries
|
||||
.map(([k]) => k.decodeText())
|
||||
.filter((k) => k !== 'Off');
|
||||
if (keys.length > 0) exportValue = keys[0];
|
||||
} catch {
|
||||
// Malformed AP dict — skip export value extraction
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also check /AS for current appearance state
|
||||
@ -353,7 +361,12 @@ function mapAppearanceStateToOption(
|
||||
const normal = ap.lookup(PDFName.of('N'));
|
||||
if (!(normal instanceof PDFDict)) continue;
|
||||
|
||||
const keys = normal.entries().map(([k]) => k.decodeText());
|
||||
let keys: string[] = [];
|
||||
try {
|
||||
keys = normal.entries().map(([k]) => k.decodeText());
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (keys.includes(stateName) && i < options.length) {
|
||||
return options[i];
|
||||
}
|
||||
@ -381,7 +394,7 @@ function resolveRadioValueForSelect(
|
||||
}
|
||||
|
||||
const lower = value.toLowerCase();
|
||||
const match = options.find(o => o.toLowerCase() === lower);
|
||||
const match = options.find((o: string) => o.toLowerCase() === lower);
|
||||
if (match) return match;
|
||||
|
||||
return null;
|
||||
@ -407,6 +420,68 @@ function getFieldOptions(field: PDFField): string[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract display labels from the /Opt array if it contains [export, display]
|
||||
* pairs. PDF spec §12.7.4.4: each element of /Opt may be either a text
|
||||
* string (export value == display value) or a two-element array where the
|
||||
* first element is the export value and the second is the display text.
|
||||
*
|
||||
* Returns null when every display value equals its export value (no distinct
|
||||
* display labels exist), keeping the interface lean for the common case.
|
||||
*/
|
||||
function getFieldDisplayOptions(field: PDFField): string[] | null {
|
||||
if (!(field instanceof PDFDropdown) && !(field instanceof PDFOptionList)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const acroDict = (field.acroField as any).dict as PDFDict;
|
||||
const optRaw = acroDict.lookup(PDFName.of('Opt'));
|
||||
if (!(optRaw instanceof PDFArray)) return null;
|
||||
|
||||
const displays: string[] = [];
|
||||
let hasDifference = false;
|
||||
|
||||
for (let i = 0; i < optRaw.size(); i++) {
|
||||
try {
|
||||
const entry = optRaw.lookup(i);
|
||||
|
||||
if (entry instanceof PDFArray && entry.size() >= 2) {
|
||||
// [exportValue, displayValue] pair
|
||||
const exp = decodeText(entry.lookup(0));
|
||||
const disp = decodeText(entry.lookup(1));
|
||||
displays.push(disp);
|
||||
if (exp !== disp) hasDifference = true;
|
||||
} else {
|
||||
// Plain string — export and display are the same
|
||||
const val = decodeText(entry);
|
||||
displays.push(val);
|
||||
}
|
||||
} catch {
|
||||
// Malformed /Opt entry — skip but continue processing remaining entries
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (displays.length === 0) return null;
|
||||
return hasDifference ? displays : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Decode a PDFString, PDFHexString, or PDFName to a JS string. */
|
||||
function decodeText(obj: unknown): string {
|
||||
if (obj instanceof PDFString || obj instanceof PDFHexString) {
|
||||
return obj.decodeText();
|
||||
}
|
||||
if (obj instanceof PDFName) {
|
||||
return obj.decodeText();
|
||||
}
|
||||
if (typeof obj === 'string') return obj;
|
||||
return String(obj ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a field is read-only.
|
||||
*/
|
||||
@ -431,17 +506,22 @@ function isFieldRequired(field: PDFField): boolean {
|
||||
|
||||
/**
|
||||
* Get field tooltip (TU entry).
|
||||
* Uses proper PDFString/PDFHexString decoding for correct Unicode support.
|
||||
*/
|
||||
function getFieldTooltip(acroField: PDFDict): string | null {
|
||||
const tu = acroField.lookup(PDFName.of('TU'));
|
||||
if (tu) {
|
||||
try {
|
||||
return tu.toString().replace(/^\(|\)$/g, '');
|
||||
} catch {
|
||||
// ignore
|
||||
if (!tu) return null;
|
||||
|
||||
try {
|
||||
// Prefer decodeText() for proper Unicode handling (UTF-16BE / PDFDocEncoding)
|
||||
if (tu instanceof PDFString || tu instanceof PDFHexString) {
|
||||
return tu.decodeText();
|
||||
}
|
||||
// Fallback: strip parentheses from raw toString() for other object types
|
||||
return tu.toString().replace(/^\(|\)$/g, '');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -471,49 +551,82 @@ export class PdfLibFormProvider implements IFormDataProvider {
|
||||
|
||||
async fetchFields(file: File | Blob): Promise<FormField[]> {
|
||||
const arrayBuffer = await readAsArrayBuffer(file);
|
||||
const doc = await PDFDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
updateMetadata: false,
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
let doc: PDFDocument;
|
||||
try {
|
||||
doc = await PDFDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
updateMetadata: false,
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
} catch (loadError) {
|
||||
console.warn('[PdfLibFormProvider] Failed to load PDF document:', loadError);
|
||||
return [];
|
||||
}
|
||||
|
||||
let form: PDFForm;
|
||||
try {
|
||||
form = doc.getForm();
|
||||
} catch {
|
||||
// No AcroForm — return empty
|
||||
} catch (formError) {
|
||||
// No AcroForm or broken catalog — return empty
|
||||
console.warn('[PdfLibFormProvider] Failed to access AcroForm:', formError);
|
||||
return [];
|
||||
}
|
||||
|
||||
const fields = form.getFields();
|
||||
let fields: PDFField[];
|
||||
try {
|
||||
fields = form.getFields();
|
||||
} catch (fieldsError) {
|
||||
console.warn('[PdfLibFormProvider] Failed to enumerate form fields:', fieldsError);
|
||||
return [];
|
||||
}
|
||||
if (fields.length === 0) return [];
|
||||
|
||||
const pages = doc.getPages();
|
||||
let pages: PDFPage[];
|
||||
try {
|
||||
pages = doc.getPages();
|
||||
} catch (pagesError) {
|
||||
// Pages tree is invalid (same issue as usePdfLibLinks "invalid catalog").
|
||||
// Without page references we can't place widgets, so return empty.
|
||||
// The viewer will fall back to native form rendering via withForms.
|
||||
console.warn(
|
||||
'[PdfLibFormProvider] PDF pages tree is invalid — cannot place form widgets.',
|
||||
'Native form rendering will be used as fallback.',
|
||||
pagesError,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: FormField[] = [];
|
||||
|
||||
for (const field of fields) {
|
||||
const type = getFieldType(field);
|
||||
const widgets = extractWidgets(field, pages, doc);
|
||||
const fieldName = field.getName();
|
||||
try {
|
||||
const type = getFieldType(field);
|
||||
const widgets = extractWidgets(field, pages, doc);
|
||||
|
||||
// Skip fields with no visible widgets
|
||||
if (widgets.length === 0) continue;
|
||||
// Skip fields with no visible widgets
|
||||
if (widgets.length === 0) continue;
|
||||
|
||||
const formField: FormField = {
|
||||
name: field.getName(),
|
||||
label: getFieldLabel(field),
|
||||
type,
|
||||
value: getFieldValue(field),
|
||||
options: getFieldOptions(field),
|
||||
displayOptions: null, // pdf-lib doesn't expose display vs export values separately
|
||||
required: isFieldRequired(field),
|
||||
readOnly: isFieldReadOnly(field),
|
||||
multiSelect: field instanceof PDFOptionList,
|
||||
multiline: isMultiline(field),
|
||||
tooltip: getFieldTooltip((field.acroField as any).dict as PDFDict),
|
||||
widgets,
|
||||
};
|
||||
const formField: FormField = {
|
||||
name: field.getName(),
|
||||
label: getFieldLabel(field),
|
||||
type,
|
||||
value: getFieldValue(field),
|
||||
options: getFieldOptions(field),
|
||||
displayOptions: getFieldDisplayOptions(field),
|
||||
required: isFieldRequired(field),
|
||||
readOnly: isFieldReadOnly(field),
|
||||
multiSelect: field instanceof PDFOptionList,
|
||||
multiline: isMultiline(field),
|
||||
tooltip: getFieldTooltip((field.acroField as any).dict as PDFDict),
|
||||
widgets,
|
||||
};
|
||||
|
||||
result.push(formField);
|
||||
result.push(formField);
|
||||
} catch (fieldError) {
|
||||
// Skip individual malformed fields but continue processing
|
||||
console.warn(`[PdfLibFormProvider] Skipping field "${fieldName}":`, fieldError);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user