diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 266e9661e..88a6a7717 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index ba9fb5de3..54d64620d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/core/components/viewer/AnnotationAPIBridge.tsx b/frontend/src/core/components/viewer/AnnotationAPIBridge.tsx index 0f4e6b2e4..8ac6b3542 100644 --- a/frontend/src/core/components/viewer/AnnotationAPIBridge.tsx +++ b/frontend/src/core/components/viewer/AnnotationAPIBridge.tsx @@ -8,6 +8,7 @@ import type { AnnotationEvent, AnnotationPatch, } from '@app/components/viewer/viewerTypes'; +import { useDocumentReady } from '@app/components/viewer/hooks/useDocumentReady'; type NoteIcon = NonNullable; @@ -290,6 +291,7 @@ const TOOL_DEFAULT_BUILDERS: Record = { export const AnnotationAPIBridge = forwardRef(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(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; diff --git a/frontend/src/core/components/viewer/AttachmentAPIBridge.tsx b/frontend/src/core/components/viewer/AttachmentAPIBridge.tsx index ea456072e..26e722a3b 100644 --- a/frontend/src/core/components/viewer/AttachmentAPIBridge.tsx +++ b/frontend/src/core/components/viewer/AttachmentAPIBridge.tsx @@ -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(() => { - 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; diff --git a/frontend/src/core/components/viewer/BookmarkAPIBridge.tsx b/frontend/src/core/components/viewer/BookmarkAPIBridge.tsx index 10a012d04..900dcb809 100644 --- a/frontend/src/core/components/viewer/BookmarkAPIBridge.tsx +++ b/frontend/src/core/components/viewer/BookmarkAPIBridge.tsx @@ -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(() => { - 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; diff --git a/frontend/src/core/components/viewer/DocumentPermissionsAPIBridge.tsx b/frontend/src/core/components/viewer/DocumentPermissionsAPIBridge.tsx index b8684eb2e..c16b37b43 100644 --- a/frontend/src/core/components/viewer/DocumentPermissionsAPIBridge.tsx +++ b/frontend/src/core/components/viewer/DocumentPermissionsAPIBridge.tsx @@ -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(() => ({ isEncrypted, @@ -42,7 +44,7 @@ export function DocumentPermissionsAPIBridge({ const api = useMemo(() => ({ 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; } diff --git a/frontend/src/core/components/viewer/ExportAPIBridge.tsx b/frontend/src/core/components/viewer/ExportAPIBridge.tsx index 1bf5d5991..9ae2b1bb4 100644 --- a/frontend/src/core/components/viewer/ExportAPIBridge.tsx +++ b/frontend/src/core/components/viewer/ExportAPIBridge.tsx @@ -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; -} \ No newline at end of file +} diff --git a/frontend/src/core/components/viewer/HistoryAPIBridge.tsx b/frontend/src/core/components/viewer/HistoryAPIBridge.tsx index e590f9bb2..e6fed9471 100644 --- a/frontend/src/core/components/viewer/HistoryAPIBridge.tsx +++ b/frontend/src/core/components/viewer/HistoryAPIBridge.tsx @@ -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(function HistoryAPIBridge(_, ref) { const { provides: historyApi } = useHistoryCapability(); const { provides: annotationApi } = useAnnotationCapability(); const { getImageData, storeImageData } = useSignature(); + const documentReady = useDocumentReady(); const restoringIds = useRef>(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(function HistoryAPIBridge return historyApi ? historyApi.canRedo() : false; }, + isReady: () => !!historyApi && documentReady, + purgeByMetadata: (predicate: (metadata: T | undefined) => boolean, topic?: string) => { if (historyApi?.purgeByMetadata) { return historyApi.purgeByMetadata(predicate, topic); diff --git a/frontend/src/core/components/viewer/LinkLayer.tsx b/frontend/src/core/components/viewer/LinkLayer.tsx index 214dde257..d78abf260 100644 --- a/frontend/src/core/components/viewer/LinkLayer.tsx +++ b/frontend/src/core/components/viewer/LinkLayer.tsx @@ -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) diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx index e38708581..7f004095f 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx @@ -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({ diff --git a/frontend/src/core/components/viewer/PanAPIBridge.tsx b/frontend/src/core/components/viewer/PanAPIBridge.tsx index 111ef70e2..057a316cb 100644 --- a/frontend/src/core/components/viewer/PanAPIBridge.tsx +++ b/frontend/src/core/components/viewer/PanAPIBridge.tsx @@ -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 ; } 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(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; diff --git a/frontend/src/core/components/viewer/PrintAPIBridge.tsx b/frontend/src/core/components/viewer/PrintAPIBridge.tsx index 8ea48d163..ed6b12638 100644 --- a/frontend/src/core/components/viewer/PrintAPIBridge.tsx +++ b/frontend/src/core/components/viewer/PrintAPIBridge.tsx @@ -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; } diff --git a/frontend/src/core/components/viewer/RedactionAPIBridge.tsx b/frontend/src/core/components/viewer/RedactionAPIBridge.tsx index 4824d3434..4d78abce0 100644 --- a/frontend/src/core/components/viewer/RedactionAPIBridge.tsx +++ b/frontend/src/core/components/viewer/RedactionAPIBridge.tsx @@ -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(); }, diff --git a/frontend/src/core/components/viewer/RotateAPIBridge.tsx b/frontend/src/core/components/viewer/RotateAPIBridge.tsx index b746d5c59..762e80598 100644 --- a/frontend/src/core/components/viewer/RotateAPIBridge.tsx +++ b/frontend/src/core/components/viewer/RotateAPIBridge.tsx @@ -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 ; } @@ -42,7 +47,11 @@ function RotateAPIBridgeInner({ documentId }: { documentId: string }) { } }); } + + return () => { + registerBridge('rotation', null); + }; }, [rotation, registerBridge]); return null; -} \ No newline at end of file +} diff --git a/frontend/src/core/components/viewer/ScrollAPIBridge.tsx b/frontend/src/core/components/viewer/ScrollAPIBridge.tsx index 41feb2b3e..9e1add3d6 100644 --- a/frontend/src/core/components/viewer/ScrollAPIBridge.tsx +++ b/frontend/src/core/components/viewer/ScrollAPIBridge.tsx @@ -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 ; } @@ -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; diff --git a/frontend/src/core/components/viewer/SearchAPIBridge.tsx b/frontend/src/core/components/viewer/SearchAPIBridge.tsx index fd198ac89..af06221b7 100644 --- a/frontend/src/core/components/viewer/SearchAPIBridge.tsx +++ b/frontend/src/core/components/viewer/SearchAPIBridge.tsx @@ -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 ; } 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(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; diff --git a/frontend/src/core/components/viewer/SelectionAPIBridge.tsx b/frontend/src/core/components/viewer/SelectionAPIBridge.tsx index ae25fdecf..78eff1d1a 100644 --- a/frontend/src/core/components/viewer/SelectionAPIBridge.tsx +++ b/frontend/src/core/components/viewer/SelectionAPIBridge.tsx @@ -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; } diff --git a/frontend/src/core/components/viewer/SignatureAPIBridge.tsx b/frontend/src/core/components/viewer/SignatureAPIBridge.tsx index c5baa8254..1c38ceb73 100644 --- a/frontend/src/core/components/viewer/SignatureAPIBridge.tsx +++ b/frontend/src/core/components/viewer/SignatureAPIBridge.tsx @@ -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(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(null); @@ -211,7 +217,7 @@ export const SignatureAPIBridge = forwardRef(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(function SignatureAPI }), [annotationApi, signatureConfig, placementPreviewSize, applyStampDefaults]); useEffect(() => { - if (!annotationApi?.onAnnotationEvent) { + if (!annotationApi?.onAnnotationEvent || !documentReady) { return; } @@ -430,10 +436,10 @@ export const SignatureAPIBridge = forwardRef(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(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 diff --git a/frontend/src/core/components/viewer/SpreadAPIBridge.tsx b/frontend/src/core/components/viewer/SpreadAPIBridge.tsx index fa3660526..46f11c876 100644 --- a/frontend/src/core/components/viewer/SpreadAPIBridge.tsx +++ b/frontend/src/core/components/viewer/SpreadAPIBridge.tsx @@ -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 ; } @@ -52,6 +58,10 @@ function SpreadAPIBridgeInner({ documentId }: { documentId: string }) { }); triggerImmediateSpreadUpdate(spreadMode); + + return () => { + registerBridge('spread', null); + }; }, [spreadMode, registerBridge, triggerImmediateSpreadUpdate]); return null; diff --git a/frontend/src/core/components/viewer/ThumbnailAPIBridge.tsx b/frontend/src/core/components/viewer/ThumbnailAPIBridge.tsx index a5e3f13d5..b1786b4ee 100644 --- a/frontend/src/core/components/viewer/ThumbnailAPIBridge.tsx +++ b/frontend/src/core/components/viewer/ThumbnailAPIBridge.tsx @@ -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; } diff --git a/frontend/src/core/components/viewer/ZoomAPIBridge.tsx b/frontend/src/core/components/viewer/ZoomAPIBridge.tsx index a4cd74e18..1b0ea744d 100644 --- a/frontend/src/core/components/viewer/ZoomAPIBridge.tsx +++ b/frontend/src/core/components/viewer/ZoomAPIBridge.tsx @@ -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 ; } @@ -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; -} \ No newline at end of file +} diff --git a/frontend/src/core/components/viewer/hooks/useDocumentReady.ts b/frontend/src/core/components/viewer/hooks/useDocumentReady.ts new file mode 100644 index 000000000..ba52a7380 --- /dev/null +++ b/frontend/src/core/components/viewer/hooks/useDocumentReady.ts @@ -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; +} diff --git a/frontend/src/core/contexts/ViewerContext.tsx b/frontend/src/core/contexts/ViewerContext.tsx index 997afa6f4..9c4f494f7 100644 --- a/frontend/src/core/contexts/ViewerContext.tsx +++ b/frontend/src/core/contexts/ViewerContext.tsx @@ -151,7 +151,7 @@ interface ViewerContextType { // Bridge registration - internal use by bridges registerBridge: ( type: K, - ref: BridgeRef + ref: BridgeRef | null ) => void; // Save changes function - registered by EmbedPdfViewer @@ -242,7 +242,7 @@ export const ViewerProvider: React.FC = ({ children }) => { const registerBridge = useCallback( ( type: K, - ref: BridgeRef + ref: BridgeRef | null ) => { setBridgeRef(bridgeRefs.current, type, ref); }, diff --git a/frontend/src/core/contexts/viewer/viewerBridges.ts b/frontend/src/core/contexts/viewer/viewerBridges.ts index cca42ff62..30f465cfd 100644 --- a/frontend/src/core/contexts/viewer/viewerBridges.ts +++ b/frontend/src/core/contexts/viewer/viewerBridges.ts @@ -227,7 +227,7 @@ export const createBridgeRegistry = (): ViewerBridgeRegistry => ({ export function registerBridge( registry: ViewerBridgeRegistry, type: K, - ref: BridgeRef + ref: BridgeRef | null ): void { registry[type] = ref as ViewerBridgeRegistry[K]; } diff --git a/frontend/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts b/frontend/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts index 6524a2585..3cab7d89e 100644 --- a/frontend/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts +++ b/frontend/src/core/hooks/tools/adjustContrast/useAdjustContrastOperation.ts @@ -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'; diff --git a/frontend/src/core/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts b/frontend/src/core/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts index e4b176c8d..359f134b5 100644 --- a/frontend/src/core/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts +++ b/frontend/src/core/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation.ts @@ -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 => { const processedFiles: File[] = []; diff --git a/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/CenteredMessageSection.ts b/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/CenteredMessageSection.ts index 9ae2ab43e..baf4f12a1 100644 --- a/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/CenteredMessageSection.ts +++ b/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/CenteredMessageSection.ts @@ -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'; diff --git a/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/FieldBoxSection.ts b/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/FieldBoxSection.ts index 78378f6d6..0fe6384de 100644 --- a/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/FieldBoxSection.ts +++ b/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/FieldBoxSection.ts @@ -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'; diff --git a/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/SignatureSection.ts b/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/SignatureSection.ts index c055a80a7..bd3538223 100644 --- a/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/SignatureSection.ts +++ b/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/SignatureSection.ts @@ -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'; diff --git a/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/StatusBadgeSection.ts b/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/StatusBadgeSection.ts index 35832ec97..a931f81d3 100644 --- a/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/StatusBadgeSection.ts +++ b/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/StatusBadgeSection.ts @@ -1,4 +1,4 @@ -import { PDFFont, PDFPage, rgb } from 'pdf-lib'; +import { PDFFont, PDFPage, rgb } from '@cantoo/pdf-lib'; interface StatusBadgeOptions { page: PDFPage; diff --git a/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/SummarySection.ts b/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/SummarySection.ts index 5e5bf8fa3..b86f6f7b3 100644 --- a/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/SummarySection.ts +++ b/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/SummarySection.ts @@ -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'; diff --git a/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/ThumbnailSection.ts b/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/ThumbnailSection.ts index 4596a0fe7..932415db5 100644 --- a/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/ThumbnailSection.ts +++ b/frontend/src/core/hooks/tools/validateSignature/outputtedPDFSections/ThumbnailSection.ts @@ -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 = ( diff --git a/frontend/src/core/hooks/tools/validateSignature/signatureReportPdf.ts b/frontend/src/core/hooks/tools/validateSignature/signatureReportPdf.ts index b61e928d1..486c0ccd0 100644 --- a/frontend/src/core/hooks/tools/validateSignature/signatureReportPdf.ts +++ b/frontend/src/core/hooks/tools/validateSignature/signatureReportPdf.ts @@ -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'; diff --git a/frontend/src/core/hooks/tools/validateSignature/utils/pdfPageHelpers.ts b/frontend/src/core/hooks/tools/validateSignature/utils/pdfPageHelpers.ts index b348f506f..91b927ed1 100644 --- a/frontend/src/core/hooks/tools/validateSignature/utils/pdfPageHelpers.ts +++ b/frontend/src/core/hooks/tools/validateSignature/utils/pdfPageHelpers.ts @@ -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'; diff --git a/frontend/src/core/hooks/tools/validateSignature/utils/pdfPalette.ts b/frontend/src/core/hooks/tools/validateSignature/utils/pdfPalette.ts index 558a96b17..064063318 100644 --- a/frontend/src/core/hooks/tools/validateSignature/utils/pdfPalette.ts +++ b/frontend/src/core/hooks/tools/validateSignature/utils/pdfPalette.ts @@ -1,4 +1,4 @@ -import { rgb } from 'pdf-lib'; +import { rgb } from '@cantoo/pdf-lib'; type RgbTuple = [number, number, number]; diff --git a/frontend/src/core/hooks/tools/validateSignature/utils/pdfText.ts b/frontend/src/core/hooks/tools/validateSignature/utils/pdfText.ts index d592db70c..029758eba 100644 --- a/frontend/src/core/hooks/tools/validateSignature/utils/pdfText.ts +++ b/frontend/src/core/hooks/tools/validateSignature/utils/pdfText.ts @@ -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[] = []; diff --git a/frontend/src/core/hooks/usePdfLibLinks.ts b/frontend/src/core/hooks/usePdfLibLinks.ts index 008bc12a9..f46ecefae 100644 --- a/frontend/src/core/hooks/usePdfLibLinks.ts +++ b/frontend/src/core/hooks/usePdfLibLinks.ts @@ -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; + /** 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>(); @@ -54,11 +39,17 @@ async function acquireDocument(url: string): Promise { 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 }); } diff --git a/frontend/src/core/services/pdfExportService.ts b/frontend/src/core/services/pdfExportService.ts index 74708a177..f0b66c2cb 100644 --- a/frontend/src/core/services/pdfExportService.ts +++ b/frontend/src/core/services/pdfExportService.ts @@ -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'; diff --git a/frontend/src/core/utils/imageToPdfUtils.ts b/frontend/src/core/utils/imageToPdfUtils.ts index 42c71e2cb..26b3848ec 100644 --- a/frontend/src/core/utils/imageToPdfUtils.ts +++ b/frontend/src/core/utils/imageToPdfUtils.ts @@ -1,4 +1,4 @@ -import { PDFDocument, PageSizes } from 'pdf-lib'; +import { PDFDocument, PageSizes } from '@cantoo/pdf-lib'; export interface ImageToPdfOptions { imageResolution?: 'full' | 'reduced'; diff --git a/frontend/src/core/utils/pdfLinkUtils.ts b/frontend/src/core/utils/pdfLinkUtils.ts new file mode 100644 index 000000000..61ba0ef58 --- /dev/null +++ b/frontend/src/core/utils/pdfLinkUtils.ts @@ -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 = { + 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; +} diff --git a/frontend/src/core/utils/signatureFlattening.ts b/frontend/src/core/utils/signatureFlattening.ts index 540779ec3..1779de956 100644 --- a/frontend/src/core/utils/signatureFlattening.ts +++ b/frontend/src/core/utils/signatureFlattening.ts @@ -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 { + 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; + }); +} diff --git a/frontend/src/proprietary/tools/formFill/FormFieldOverlay.tsx b/frontend/src/proprietary/tools/formFill/FormFieldOverlay.tsx index 0db191f20..d48de82e5 100644 --- a/frontend/src/proprietary/tools/formFill/FormFieldOverlay.tsx +++ b/frontend/src/proprietary/tools/formFill/FormFieldOverlay.tsx @@ -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 ( -
+
{field.multiline ? (