Add client-side PDF conversion tools

This commit is contained in:
achraf
2026-07-14 15:16:52 +02:00
parent 1cebdffda0
commit f75e6a19c4
9 changed files with 823 additions and 4 deletions

View File

@@ -20,13 +20,16 @@
"dependencies": { "dependencies": {
"@imgly/background-removal": "^1.7.0", "@imgly/background-removal": "^1.7.0",
"@tailwindcss/vite": "^4.3.0", "@tailwindcss/vite": "^4.3.0",
"docx": "^9.7.1",
"fabric": "^7.4.0", "fabric": "^7.4.0",
"onnxruntime-web": "1.21.0", "onnxruntime-web": "1.21.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^6.1.200",
"react": "^19.2.6", "react": "^19.2.6",
"react-dom": "^19.2.6", "react-dom": "^19.2.6",
"react-router-dom": "^7.15.0", "react-router-dom": "^7.15.0",
"tailwindcss": "^4.3.0", "tailwindcss": "^4.3.0",
"tesseract.js": "^7.0.0",
"zxing-wasm": "^3.1.0" "zxing-wasm": "^3.1.0"
}, },
"devDependencies": { "devDependencies": {

366
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.3.0 specifier: ^4.3.0
version: 4.3.0(vite@8.0.12(@types/node@24.12.4)(jiti@2.7.0)) version: 4.3.0(vite@8.0.12(@types/node@24.12.4)(jiti@2.7.0))
docx:
specifier: ^9.7.1
version: 9.7.1
fabric: fabric:
specifier: ^7.4.0 specifier: ^7.4.0
version: 7.4.0 version: 7.4.0
@@ -23,6 +26,9 @@ importers:
pdf-lib: pdf-lib:
specifier: ^1.17.1 specifier: ^1.17.1
version: 1.17.1 version: 1.17.1
pdfjs-dist:
specifier: ^6.1.200
version: 6.1.200
react: react:
specifier: ^19.2.6 specifier: ^19.2.6
version: 19.2.6 version: 19.2.6
@@ -35,6 +41,9 @@ importers:
tailwindcss: tailwindcss:
specifier: ^4.3.0 specifier: ^4.3.0
version: 4.3.0 version: 4.3.0
tesseract.js:
specifier: ^7.0.0
version: 7.0.0
zxing-wasm: zxing-wasm:
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.0(@types/emscripten@1.41.5) version: 3.1.0(@types/emscripten@1.41.5)
@@ -274,6 +283,81 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@napi-rs/canvas-android-arm64@1.0.2':
resolution: {integrity: sha512-IMXKVQod0ol4vt3gmClUfXz4JAgHYESGPCUqmH3lQxBoL0K/2greJaQE1HVBVxWWFKfLc4OLZVdxg7kXVyXv+g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@napi-rs/canvas-darwin-arm64@1.0.2':
resolution: {integrity: sha512-Sc8tPi6cF+5lqOzCCKFALJHhDiRwyMzTPYm3bbhdXsOunU0lQO5f05ucyOzN2r55I23Hg5bsjH63uSCvWp3EgQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@napi-rs/canvas-darwin-x64@1.0.2':
resolution: {integrity: sha512-niDXZ9LhKB1zLrUdYB64RHQFDGz9rr0eGx061qtJJU3U20EMMIx28ADF5fVYbhtOgkWQrBjFicfaye1yM0U62A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@1.0.2':
resolution: {integrity: sha512-sgatQL9JxGRH/Amzcvu0P3t8Am3duou74CisfuJ41Dwt8cWy723z/9KZ8LlgmxfypEwEZxSTNFJtU8d281lmhQ==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@1.0.2':
resolution: {integrity: sha512-dgKuX0peF3xwY6ZF5QxGS4wbfDqpoFAJYXiLSp+guZKARQUKMkRqZSDrXKj7nfrec3UCMzC0PFCPte0ES98AiA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-arm64-musl@1.0.2':
resolution: {integrity: sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-linux-riscv64-gnu@1.0.2':
resolution: {integrity: sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-gnu@1.0.2':
resolution: {integrity: sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-musl@1.0.2':
resolution: {integrity: sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-win32-arm64-msvc@1.0.2':
resolution: {integrity: sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@napi-rs/canvas-win32-x64-msvc@1.0.2':
resolution: {integrity: sha512-okU8/t2foV6C31n0GtvEMbfD5rOFc70+/6xUNME9Guld29sgSOIGUEDScAWFlcP3k5TYQRl9TNkwJEEjh15w8A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@napi-rs/canvas@1.0.2':
resolution: {integrity: sha512-EYEqlMYaCbpZDz+IgDH5xp9MTd3ui4dmGqbQYryhMLnSRxrhHKq5KQWHHKxFUcEP4Hp8/BWgvqXocX4j7iSbOQ==}
engines: {node: '>= 10'}
'@napi-rs/wasm-runtime@1.1.4': '@napi-rs/wasm-runtime@1.1.4':
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
peerDependencies: peerDependencies:
@@ -546,6 +630,9 @@ packages:
'@types/node@24.12.4': '@types/node@24.12.4':
resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==}
'@types/node@25.9.5':
resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==}
'@types/react-dom@19.2.3': '@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies: peerDependencies:
@@ -691,6 +778,9 @@ packages:
bl@4.1.0: bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
bmp-js@0.1.0:
resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==}
brace-expansion@5.0.6: brace-expansion@5.0.6:
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
@@ -724,6 +814,9 @@ packages:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
cross-spawn@7.0.6: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -766,6 +859,10 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
docx@9.7.1:
resolution: {integrity: sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg==}
engines: {node: '>=10'}
electron-to-chromium@1.5.353: electron-to-chromium@1.5.353:
resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==}
@@ -929,6 +1026,9 @@ packages:
guid-typescript@1.0.9: guid-typescript@1.0.9:
resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==}
hash.js@1.1.7:
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
hermes-estree@0.25.1: hermes-estree@0.25.1:
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
@@ -951,6 +1051,9 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
idb-keyval@6.3.0:
resolution: {integrity: sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==}
ieee754@1.2.1: ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@@ -962,6 +1065,9 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
imurmurhash@0.1.4: imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'} engines: {node: '>=0.8.19'}
@@ -989,6 +1095,12 @@ packages:
is-potential-custom-element-name@1.0.1: is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
is-url@1.2.4:
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isexe@2.0.0: isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -1027,6 +1139,9 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -1034,6 +1149,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lightningcss-android-arm64@1.32.0: lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
@@ -1131,6 +1249,9 @@ packages:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
minimalistic-assert@1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
minimatch@10.2.5: minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
@@ -1149,6 +1270,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
nanoid@5.1.16:
resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==}
engines: {node: ^18 || >=20}
hasBin: true
napi-build-utils@2.0.0: napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
@@ -1165,6 +1291,15 @@ packages:
node-addon-api@7.1.1: node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-releases@2.0.44: node-releases@2.0.44:
resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==}
@@ -1183,6 +1318,10 @@ packages:
onnxruntime-web@1.21.0: onnxruntime-web@1.21.0:
resolution: {integrity: sha512-adzOe+7uI7lKz6pQNbAsLMQd2Fq5Jhmoxd8LZjJr8m3KvbFyiYyRxRiC57/XXD+jb18voppjeGAjoZmskXG+7A==} resolution: {integrity: sha512-adzOe+7uI7lKz6pQNbAsLMQd2Fq5Jhmoxd8LZjJr8m3KvbFyiYyRxRiC57/XXD+jb18voppjeGAjoZmskXG+7A==}
opencollective-postinstall@2.0.3:
resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==}
hasBin: true
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -1215,6 +1354,10 @@ packages:
pdf-lib@1.17.1: pdf-lib@1.17.1:
resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==} resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==}
pdfjs-dist@6.1.200:
resolution: {integrity: sha512-o8MolyzirkkLrcdsae/HEOiIcXWI7DS5zGpvqW8xTC2YUsW30rltFw2bDGvw/fskUdEMrQm2br68jzDS5BH2vw==}
engines: {node: '>=22.13.0 || >=24'}
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -1254,6 +1397,9 @@ packages:
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
protobufjs@7.6.2: protobufjs@7.6.2:
resolution: {integrity: sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==} resolution: {integrity: sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -1295,10 +1441,16 @@ packages:
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readable-stream@3.6.2: readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
rolldown@1.0.0: rolldown@1.0.0:
resolution: {integrity: sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==} resolution: {integrity: sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -1307,12 +1459,19 @@ packages:
rrweb-cssom@0.8.0: rrweb-cssom@0.8.0:
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
safe-buffer@5.2.1: safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safer-buffer@2.1.2: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
saxes@6.0.0: saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'} engines: {node: '>=v12.22.7'}
@@ -1332,6 +1491,9 @@ packages:
set-cookie-parser@2.7.2: set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
shebang-command@2.0.0: shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1359,6 +1521,9 @@ packages:
std-env@4.1.0: std-env@4.1.0:
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
string_decoder@1.3.0: string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@@ -1387,6 +1552,12 @@ packages:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
tesseract.js-core@7.0.0:
resolution: {integrity: sha512-WnNH518NzmbSq9zgTPeoF8c+xmilS8rFIl1YKbk/ptuuc7p6cLNELNuPAzcmsYw450ca6bLa8j3t0VAtq435Vw==}
tesseract.js@7.0.0:
resolution: {integrity: sha512-exPBkd+z+wM1BuMkx/Bjv43OeLBxhL5kKWsz/9JY+DXcXdiBjiAch0V49QR3oAJqCaL5qURE0vx9Eo+G5YE7mA==}
tinybench@2.9.0: tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -1413,6 +1584,9 @@ packages:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
engines: {node: '>=16'} engines: {node: '>=16'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tr46@5.1.1: tr46@5.1.1:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1455,6 +1629,9 @@ packages:
undici-types@7.16.0: undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
update-browserslist-db@1.2.3: update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true hasBin: true
@@ -1555,6 +1732,12 @@ packages:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'} engines: {node: '>=18'}
wasm-feature-detect@1.8.0:
resolution: {integrity: sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webidl-conversions@7.0.0: webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -1572,6 +1755,9 @@ packages:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'} engines: {node: '>=18'}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which@2.0.2: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -1601,10 +1787,17 @@ packages:
utf-8-validate: utf-8-validate:
optional: true optional: true
xml-js@1.6.11:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true
xml-name-validator@5.0.0: xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'} engines: {node: '>=18'}
xml@1.0.1:
resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
xmlchars@2.2.0: xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
@@ -1615,6 +1808,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
zlibjs@0.3.1:
resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==}
zod-validation-error@4.0.2: zod-validation-error@4.0.2:
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -1860,6 +2056,54 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@napi-rs/canvas-android-arm64@1.0.2':
optional: true
'@napi-rs/canvas-darwin-arm64@1.0.2':
optional: true
'@napi-rs/canvas-darwin-x64@1.0.2':
optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@1.0.2':
optional: true
'@napi-rs/canvas-linux-arm64-gnu@1.0.2':
optional: true
'@napi-rs/canvas-linux-arm64-musl@1.0.2':
optional: true
'@napi-rs/canvas-linux-riscv64-gnu@1.0.2':
optional: true
'@napi-rs/canvas-linux-x64-gnu@1.0.2':
optional: true
'@napi-rs/canvas-linux-x64-musl@1.0.2':
optional: true
'@napi-rs/canvas-win32-arm64-msvc@1.0.2':
optional: true
'@napi-rs/canvas-win32-x64-msvc@1.0.2':
optional: true
'@napi-rs/canvas@1.0.2':
optionalDependencies:
'@napi-rs/canvas-android-arm64': 1.0.2
'@napi-rs/canvas-darwin-arm64': 1.0.2
'@napi-rs/canvas-darwin-x64': 1.0.2
'@napi-rs/canvas-linux-arm-gnueabihf': 1.0.2
'@napi-rs/canvas-linux-arm64-gnu': 1.0.2
'@napi-rs/canvas-linux-arm64-musl': 1.0.2
'@napi-rs/canvas-linux-riscv64-gnu': 1.0.2
'@napi-rs/canvas-linux-x64-gnu': 1.0.2
'@napi-rs/canvas-linux-x64-musl': 1.0.2
'@napi-rs/canvas-win32-arm64-msvc': 1.0.2
'@napi-rs/canvas-win32-x64-msvc': 1.0.2
optional: true
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies: dependencies:
'@emnapi/core': 1.10.0 '@emnapi/core': 1.10.0
@@ -2050,6 +2294,10 @@ snapshots:
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.16.0
'@types/node@25.9.5':
dependencies:
undici-types: 7.24.6
'@types/react-dom@19.2.3(@types/react@19.2.14)': '@types/react-dom@19.2.3(@types/react@19.2.14)':
dependencies: dependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
@@ -2227,6 +2475,8 @@ snapshots:
readable-stream: 3.6.2 readable-stream: 3.6.2
optional: true optional: true
bmp-js@0.1.0: {}
brace-expansion@5.0.6: brace-expansion@5.0.6:
dependencies: dependencies:
balanced-match: 4.0.4 balanced-match: 4.0.4
@@ -2262,6 +2512,8 @@ snapshots:
cookie@1.1.1: {} cookie@1.1.1: {}
core-util-is@1.0.3: {}
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@@ -2301,6 +2553,15 @@ snapshots:
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
docx@9.7.1:
dependencies:
'@types/node': 25.9.5
hash.js: 1.1.7
jszip: 3.10.1
nanoid: 5.1.16
xml: 1.0.1
xml-js: 1.6.11
electron-to-chromium@1.5.353: {} electron-to-chromium@1.5.353: {}
end-of-stream@1.4.5: end-of-stream@1.4.5:
@@ -2473,6 +2734,11 @@ snapshots:
guid-typescript@1.0.9: {} guid-typescript@1.0.9: {}
hash.js@1.1.7:
dependencies:
inherits: 2.0.4
minimalistic-assert: 1.0.1
hermes-estree@0.25.1: {} hermes-estree@0.25.1: {}
hermes-parser@0.25.1: hermes-parser@0.25.1:
@@ -2505,6 +2771,8 @@ snapshots:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
optional: true optional: true
idb-keyval@6.3.0: {}
ieee754@1.2.1: ieee754@1.2.1:
optional: true optional: true
@@ -2512,10 +2780,11 @@ snapshots:
ignore@7.0.5: {} ignore@7.0.5: {}
immediate@3.0.6: {}
imurmurhash@0.1.4: {} imurmurhash@0.1.4: {}
inherits@2.0.4: inherits@2.0.4: {}
optional: true
ini@1.3.8: ini@1.3.8:
optional: true optional: true
@@ -2533,6 +2802,10 @@ snapshots:
is-potential-custom-element-name@1.0.1: is-potential-custom-element-name@1.0.1:
optional: true optional: true
is-url@1.2.4: {}
isarray@1.0.0: {}
isexe@2.0.0: {} isexe@2.0.0: {}
jiti@2.7.0: {} jiti@2.7.0: {}
@@ -2579,6 +2852,13 @@ snapshots:
json5@2.2.3: {} json5@2.2.3: {}
jszip@3.10.1:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
keyv@4.5.4: keyv@4.5.4:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
@@ -2588,6 +2868,10 @@ snapshots:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
type-check: 0.4.0 type-check: 0.4.0
lie@3.3.0:
dependencies:
immediate: 3.0.6
lightningcss-android-arm64@1.32.0: lightningcss-android-arm64@1.32.0:
optional: true optional: true
@@ -2659,6 +2943,8 @@ snapshots:
mimic-response@3.1.0: mimic-response@3.1.0:
optional: true optional: true
minimalistic-assert@1.0.1: {}
minimatch@10.2.5: minimatch@10.2.5:
dependencies: dependencies:
brace-expansion: 5.0.6 brace-expansion: 5.0.6
@@ -2673,6 +2959,8 @@ snapshots:
nanoid@3.3.12: {} nanoid@3.3.12: {}
nanoid@5.1.16: {}
napi-build-utils@2.0.0: napi-build-utils@2.0.0:
optional: true optional: true
@@ -2691,6 +2979,10 @@ snapshots:
node-addon-api@7.1.1: node-addon-api@7.1.1:
optional: true optional: true
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-releases@2.0.44: {} node-releases@2.0.44: {}
nwsapi@2.2.23: nwsapi@2.2.23:
@@ -2714,6 +3006,8 @@ snapshots:
platform: 1.3.6 platform: 1.3.6
protobufjs: 7.6.2 protobufjs: 7.6.2
opencollective-postinstall@2.0.3: {}
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4
@@ -2751,6 +3045,10 @@ snapshots:
pako: 1.0.11 pako: 1.0.11
tslib: 1.14.1 tslib: 1.14.1
pdfjs-dist@6.1.200:
optionalDependencies:
'@napi-rs/canvas': 1.0.2
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@4.0.4: {} picomatch@4.0.4: {}
@@ -2791,6 +3089,8 @@ snapshots:
prettier@3.8.3: {} prettier@3.8.3: {}
process-nextick-args@2.0.1: {}
protobufjs@7.6.2: protobufjs@7.6.2:
dependencies: dependencies:
'@protobufjs/aspromise': 1.1.2 '@protobufjs/aspromise': 1.1.2
@@ -2843,6 +3143,16 @@ snapshots:
react@19.2.6: {} react@19.2.6: {}
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readable-stream@3.6.2: readable-stream@3.6.2:
dependencies: dependencies:
inherits: 2.0.4 inherits: 2.0.4
@@ -2850,6 +3160,8 @@ snapshots:
util-deprecate: 1.0.2 util-deprecate: 1.0.2
optional: true optional: true
regenerator-runtime@0.13.11: {}
rolldown@1.0.0: rolldown@1.0.0:
dependencies: dependencies:
'@oxc-project/types': 0.129.0 '@oxc-project/types': 0.129.0
@@ -2874,12 +3186,16 @@ snapshots:
rrweb-cssom@0.8.0: rrweb-cssom@0.8.0:
optional: true optional: true
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: safe-buffer@5.2.1:
optional: true optional: true
safer-buffer@2.1.2: safer-buffer@2.1.2:
optional: true optional: true
sax@1.6.0: {}
saxes@6.0.0: saxes@6.0.0:
dependencies: dependencies:
xmlchars: 2.2.0 xmlchars: 2.2.0
@@ -2893,6 +3209,8 @@ snapshots:
set-cookie-parser@2.7.2: {} set-cookie-parser@2.7.2: {}
setimmediate@1.0.5: {}
shebang-command@2.0.0: shebang-command@2.0.0:
dependencies: dependencies:
shebang-regex: 3.0.0 shebang-regex: 3.0.0
@@ -2917,6 +3235,10 @@ snapshots:
std-env@4.1.0: {} std-env@4.1.0: {}
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
string_decoder@1.3.0: string_decoder@1.3.0:
dependencies: dependencies:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
@@ -2951,6 +3273,22 @@ snapshots:
readable-stream: 3.6.2 readable-stream: 3.6.2
optional: true optional: true
tesseract.js-core@7.0.0: {}
tesseract.js@7.0.0:
dependencies:
bmp-js: 0.1.0
idb-keyval: 6.3.0
is-url: 1.2.4
node-fetch: 2.7.0
opencollective-postinstall: 2.0.3
regenerator-runtime: 0.13.11
tesseract.js-core: 7.0.0
wasm-feature-detect: 1.8.0
zlibjs: 0.3.1
transitivePeerDependencies:
- encoding
tinybench@2.9.0: {} tinybench@2.9.0: {}
tinyexec@1.1.2: {} tinyexec@1.1.2: {}
@@ -2975,6 +3313,8 @@ snapshots:
tldts: 6.1.86 tldts: 6.1.86
optional: true optional: true
tr46@0.0.3: {}
tr46@5.1.1: tr46@5.1.1:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
@@ -3017,6 +3357,8 @@ snapshots:
undici-types@7.16.0: {} undici-types@7.16.0: {}
undici-types@7.24.6: {}
update-browserslist-db@1.2.3(browserslist@4.28.2): update-browserslist-db@1.2.3(browserslist@4.28.2):
dependencies: dependencies:
browserslist: 4.28.2 browserslist: 4.28.2
@@ -3027,8 +3369,7 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
util-deprecate@1.0.2: util-deprecate@1.0.2: {}
optional: true
vite@8.0.12(@types/node@24.12.4)(jiti@2.7.0): vite@8.0.12(@types/node@24.12.4)(jiti@2.7.0):
dependencies: dependencies:
@@ -3075,6 +3416,10 @@ snapshots:
xml-name-validator: 5.0.0 xml-name-validator: 5.0.0
optional: true optional: true
wasm-feature-detect@1.8.0: {}
webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: webidl-conversions@7.0.0:
optional: true optional: true
@@ -3092,6 +3437,11 @@ snapshots:
webidl-conversions: 7.0.0 webidl-conversions: 7.0.0
optional: true optional: true
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which@2.0.2: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
@@ -3109,9 +3459,15 @@ snapshots:
ws@8.21.0: ws@8.21.0:
optional: true optional: true
xml-js@1.6.11:
dependencies:
sax: 1.6.0
xml-name-validator@5.0.0: xml-name-validator@5.0.0:
optional: true optional: true
xml@1.0.1: {}
xmlchars@2.2.0: xmlchars@2.2.0:
optional: true optional: true
@@ -3119,6 +3475,8 @@ snapshots:
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zlibjs@0.3.1: {}
zod-validation-error@4.0.2(zod@4.4.3): zod-validation-error@4.0.2(zod@4.4.3):
dependencies: dependencies:
zod: 4.4.3 zod: 4.4.3

View File

@@ -28,6 +28,8 @@ import { passwordGeneratorPlugin } from "../../tools/password-generator";
import { fileChecksumVerifierPlugin } from "../../tools/file-checksum-verifier"; import { fileChecksumVerifierPlugin } from "../../tools/file-checksum-verifier";
import { svgViewerPlugin } from "../../tools/svg-viewer"; import { svgViewerPlugin } from "../../tools/svg-viewer";
import { backgroundRemoverPlugin } from "../../tools/background-remover"; import { backgroundRemoverPlugin } from "../../tools/background-remover";
import { imageToPdfPlugin } from "../../tools/image-to-pdf";
import { pdfToDocxOcrPlugin } from "../../tools/pdf-to-docx-ocr";
function erasePlugin<TOptions>(plugin: PlimiPlugin<TOptions>): UnknownPlimiPlugin { function erasePlugin<TOptions>(plugin: PlimiPlugin<TOptions>): UnknownPlimiPlugin {
return plugin as unknown as UnknownPlimiPlugin; return plugin as unknown as UnknownPlimiPlugin;
@@ -54,6 +56,8 @@ export const pluginRegistry: UnknownPlimiPlugin[] = [
erasePlugin(pdfMergerPlugin), erasePlugin(pdfMergerPlugin),
erasePlugin(pdfSplitterPlugin), erasePlugin(pdfSplitterPlugin),
erasePlugin(pdfWatermarkPlugin), erasePlugin(pdfWatermarkPlugin),
erasePlugin(imageToPdfPlugin),
erasePlugin(pdfToDocxOcrPlugin),
erasePlugin(imageRedactorPlugin), erasePlugin(imageRedactorPlugin),
erasePlugin(exifScrubberPlugin), erasePlugin(exifScrubberPlugin),
erasePlugin(jwtDecoderPlugin), erasePlugin(jwtDecoderPlugin),

View File

@@ -0,0 +1,71 @@
import type { PlimiPlugin } from "../../core/plugins/plugin-types";
import { runImageToPdf } from "./run";
export interface ImageToPdfOptions {
pageSize: "fit" | "a4" | "letter";
orientation: "auto" | "portrait" | "landscape";
margin: number;
}
export const imageToPdfPlugin: PlimiPlugin<ImageToPdfOptions> = {
manifest: {
id: "image-to-pdf",
name: "Images to PDF",
description:
"Turn one or more JPG, PNG, or WebP images into a single PDF without uploading them.",
category: "pdf",
version: "1.0.0",
tags: ["image", "pdf", "jpg", "png", "webp", "convert"],
input: {
type: "files",
label: "Images, in page order",
description: "Each image becomes one page. Select files in the order you want them to appear.",
accept: ["image/jpeg", "image/png", "image/webp"],
multiple: true,
maxSizeMb: 30,
maxFiles: 100,
},
output: { type: "files" },
offlineReady: true,
},
optionsSchema: {
fields: [
{
type: "select",
key: "pageSize",
label: "Page size",
defaultValue: "fit",
options: [
{ label: "Fit each image", value: "fit" },
{ label: "A4", value: "a4" },
{ label: "US Letter", value: "letter" },
],
},
{
type: "select",
key: "orientation",
label: "Orientation",
defaultValue: "auto",
options: [
{ label: "Automatic", value: "auto" },
{ label: "Portrait", value: "portrait" },
{ label: "Landscape", value: "landscape" },
],
},
{
type: "number",
key: "margin",
label: "Margin (mm)",
defaultValue: 10,
min: 0,
max: 40,
step: 1,
},
],
},
capabilities: { cancelable: true },
permissions: { network: "none", fileSystem: "read-write" },
run: runImageToPdf,
};

View File

@@ -0,0 +1,47 @@
import { describe, expect, it, vi } from "vitest";
import { PDFDocument } from "pdf-lib";
import type { ToolContext } from "../../core/plugins/plugin-types";
import { imageToPdfPlugin, type ImageToPdfOptions } from "./index";
import { runImageToPdf } from "./run";
const context: ToolContext = {
signal: new AbortController().signal,
reportProgress: vi.fn(),
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
};
const options: ImageToPdfOptions = {
pageSize: "fit",
orientation: "auto",
margin: 10,
};
const onePixelPng = Uint8Array.from(atob(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
), (char) => char.charCodeAt(0));
describe("Images to PDF", () => {
it("has a local PDF plugin manifest", () => {
expect(imageToPdfPlugin.manifest.id).toBe("image-to-pdf");
expect(imageToPdfPlugin.manifest.category).toBe("pdf");
expect(imageToPdfPlugin.permissions?.network).toBe("none");
});
it("requires at least one image", async () => {
await expect(runImageToPdf({ files: [] }, options, context)).rejects.toThrow(
"Add at least one image."
);
});
it("creates one PDF page per image", async () => {
const files = ["first.png", "second.png"].map(
(name) => new File([onePixelPng], name, { type: "image/png" })
);
const result = await runImageToPdf({ files }, options, context);
expect(result.type).toBe("files");
if (result.type !== "files") throw new Error("Unexpected result");
expect(result.files[0].name).toBe("images.pdf");
const pdf = await PDFDocument.load(await result.files[0].blob.arrayBuffer());
expect(pdf.getPageCount()).toBe(2);
});
});

View File

@@ -0,0 +1,113 @@
import { PDFDocument } from "pdf-lib";
import type { ToolInput } from "../../core/io/input-types";
import type { ToolResult } from "../../core/io/output-types";
import type { ToolContext } from "../../core/plugins/plugin-types";
import type { ImageToPdfOptions } from "./index";
const MM_TO_POINTS = 72 / 25.4;
const PAGE_SIZES = {
a4: [595.28, 841.89],
letter: [612, 792],
} as const;
function abortIfNeeded(signal: AbortSignal) {
if (signal.aborted) throw new DOMException("Operation cancelled", "AbortError");
}
async function convertWebpToPng(file: File): Promise<Uint8Array> {
const bitmap = await createImageBitmap(file);
try {
const canvas = document.createElement("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const context = canvas.getContext("2d");
if (!context) throw new Error("Your browser could not prepare this WebP image.");
context.drawImage(bitmap, 0, 0);
const blob = await new Promise<Blob>((resolve, reject) =>
canvas.toBlob(
(value) => (value ? resolve(value) : reject(new Error("Could not convert WebP image."))),
"image/png"
)
);
return new Uint8Array(await blob.arrayBuffer());
} finally {
bitmap.close();
}
}
export async function runImageToPdf(
input: ToolInput,
options: ImageToPdfOptions,
context: ToolContext
): Promise<ToolResult> {
const files = input.files ?? [];
if (files.length === 0) throw new Error("Add at least one image.");
const pdf = await PDFDocument.create();
const margin = Math.max(0, Math.min(40, Number(options.margin) || 0)) * MM_TO_POINTS;
for (let index = 0; index < files.length; index += 1) {
abortIfNeeded(context.signal);
const file = files[index];
context.reportProgress({
percentage: (index / files.length) * 95,
message: `Adding ${file.name} (${index + 1}/${files.length})...`,
});
let image;
if (file.type === "image/jpeg") {
image = await pdf.embedJpg(new Uint8Array(await file.arrayBuffer()));
} else if (file.type === "image/png") {
image = await pdf.embedPng(new Uint8Array(await file.arrayBuffer()));
} else if (file.type === "image/webp") {
image = await pdf.embedPng(await convertWebpToPng(file));
} else {
throw new Error(`${file.name} is not a supported JPG, PNG, or WebP image.`);
}
let pageWidth: number;
let pageHeight: number;
if (options.pageSize === "fit") {
pageWidth = image.width + margin * 2;
pageHeight = image.height + margin * 2;
} else {
[pageWidth, pageHeight] = PAGE_SIZES[options.pageSize];
const landscape =
options.orientation === "landscape" ||
(options.orientation === "auto" && image.width > image.height);
if (landscape) [pageWidth, pageHeight] = [pageHeight, pageWidth];
}
const usableWidth = Math.max(1, pageWidth - margin * 2);
const usableHeight = Math.max(1, pageHeight - margin * 2);
const scale = Math.min(usableWidth / image.width, usableHeight / image.height);
const width = image.width * scale;
const height = image.height * scale;
const page = pdf.addPage([pageWidth, pageHeight]);
page.drawImage(image, {
x: (pageWidth - width) / 2,
y: (pageHeight - height) / 2,
width,
height,
});
}
abortIfNeeded(context.signal);
context.reportProgress({ percentage: 98, message: "Finishing PDF..." });
const bytes = await pdf.save();
const blob = new Blob([bytes as unknown as BlobPart], { type: "application/pdf" });
context.reportProgress({ percentage: 100, message: "PDF ready." });
return {
type: "files",
files: [{
name: files.length === 1
? `${files[0].name.replace(/\.[^.]+$/, "")}.pdf`
: "images.pdf",
mimeType: "application/pdf",
blob,
sizeBefore: files.reduce((total, file) => total + file.size, 0),
sizeAfter: blob.size,
}],
};
}

View File

@@ -0,0 +1,76 @@
import type { PlimiPlugin } from "../../core/plugins/plugin-types";
import { runPdfToDocxOcr } from "./run";
export interface PdfToDocxOcrOptions {
language: "eng" | "fra" | "deu" | "spa" | "ita" | "por" | "nld";
quality: "standard" | "high";
pageBreaks: boolean;
}
const LANGUAGE_NAMES: Record<PdfToDocxOcrOptions["language"], string> = {
eng: "English",
fra: "French",
deu: "German",
spa: "Spanish",
ita: "Italian",
por: "Portuguese",
nld: "Dutch",
};
export const pdfToDocxOcrPlugin: PlimiPlugin<PdfToDocxOcrOptions> = {
manifest: {
id: "pdf-to-docx-ocr",
name: "PDF to DOCX with OCR",
description:
"Extract text from scanned or digital PDFs and create an editable Word document entirely on your device.",
category: "pdf",
version: "1.0.0",
tags: ["pdf", "docx", "word", "ocr", "scan", "convert"],
input: {
type: "files",
label: "PDF document",
description: "Best for typed documents and clear scans. Handwriting and complex layouts may need cleanup.",
accept: ["application/pdf"],
multiple: false,
maxSizeMb: 100,
maxFiles: 1,
},
output: { type: "files" },
offlineReady: false,
},
optionsSchema: {
fields: [
{
type: "select",
key: "language",
label: "Document language",
defaultValue: "eng",
options: Object.entries(LANGUAGE_NAMES).map(([value, label]) => ({ value, label })),
},
{
type: "select",
key: "quality",
label: "OCR quality",
defaultValue: "standard",
options: [
{ label: "Standard (faster)", value: "standard" },
{ label: "High (clearer small text)", value: "high" },
],
},
{
type: "boolean",
key: "pageBreaks",
label: "Keep PDF page breaks",
defaultValue: true,
},
],
},
capabilities: { cancelable: true, wasm: true },
permissions: { network: "optional", fileSystem: "read-write" },
getRunWarning: (_input, options) =>
`The ${LANGUAGE_NAMES[options.language]} OCR model may be downloaded to your browser and cached. ` +
"Your PDF is never uploaded; page rendering, OCR, and DOCX creation all happen locally.",
run: runPdfToDocxOcr,
};

View File

@@ -0,0 +1,37 @@
import { describe, expect, it, vi } from "vitest";
import type { ToolContext } from "../../core/plugins/plugin-types";
import { pdfToDocxOcrPlugin, type PdfToDocxOcrOptions } from "./index";
import { runPdfToDocxOcr } from "./run";
const context: ToolContext = {
signal: new AbortController().signal,
reportProgress: vi.fn(),
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
};
const options: PdfToDocxOcrOptions = {
language: "eng",
quality: "standard",
pageBreaks: true,
};
describe("PDF to DOCX with OCR", () => {
it("declares local processing with optional model access", () => {
expect(pdfToDocxOcrPlugin.manifest.id).toBe("pdf-to-docx-ocr");
expect(pdfToDocxOcrPlugin.capabilities?.wasm).toBe(true);
expect(pdfToDocxOcrPlugin.permissions?.network).toBe("optional");
});
it("requires a PDF", async () => {
await expect(runPdfToDocxOcr({ files: [] }, options, context)).rejects.toThrow(
"Add a PDF document."
);
});
it("rejects a non-PDF before starting OCR", async () => {
const file = new File(["hello"], "notes.txt", { type: "text/plain" });
await expect(runPdfToDocxOcr({ files: [file] }, options, context)).rejects.toThrow(
"The selected file is not a PDF."
);
});
});

View File

@@ -0,0 +1,110 @@
import type { ToolInput } from "../../core/io/input-types";
import type { ToolResult } from "../../core/io/output-types";
import type { ToolContext } from "../../core/plugins/plugin-types";
import type { PdfToDocxOcrOptions } from "./index";
function abortIfNeeded(signal: AbortSignal) {
if (signal.aborted) throw new DOMException("Operation cancelled", "AbortError");
}
export async function runPdfToDocxOcr(
input: ToolInput,
options: PdfToDocxOcrOptions,
context: ToolContext
): Promise<ToolResult> {
const file = input.files?.[0];
if (!file) throw new Error("Add a PDF document.");
if (file.type && file.type !== "application/pdf") throw new Error("The selected file is not a PDF.");
abortIfNeeded(context.signal);
context.reportProgress({ percentage: 1, message: "Opening PDF locally..." });
const [pdfjs, { createWorker }] = await Promise.all([
import("pdfjs-dist"),
import("tesseract.js"),
]);
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
import.meta.url
).toString();
const loadingTask = pdfjs.getDocument({ data: new Uint8Array(await file.arrayBuffer()) });
const pdf = await loadingTask.promise;
const pageTexts: string[] = [];
const scale = options.quality === "high" ? 2.5 : 1.75;
const worker = await createWorker(options.language, undefined, {
logger: (message) => {
if (message.status === "recognizing text" && typeof message.progress === "number") {
const completedPages = pageTexts.length;
context.reportProgress({
percentage: 8 + ((completedPages + message.progress) / pdf.numPages) * 88,
message: `Recognizing page ${completedPages + 1} of ${pdf.numPages}...`,
});
}
},
});
const cancelWorker = () => void worker.terminate();
context.signal.addEventListener("abort", cancelWorker, { once: true });
try {
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber += 1) {
abortIfNeeded(context.signal);
context.reportProgress({
percentage: 5 + ((pageNumber - 1) / pdf.numPages) * 88,
message: `Rendering page ${pageNumber} of ${pdf.numPages} locally...`,
});
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale });
const canvas = document.createElement("canvas");
canvas.width = Math.ceil(viewport.width);
canvas.height = Math.ceil(viewport.height);
const canvasContext = canvas.getContext("2d", { alpha: false });
if (!canvasContext) throw new Error("Your browser could not render this PDF page.");
await page.render({ canvas, canvasContext, viewport }).promise;
abortIfNeeded(context.signal);
const result = await worker.recognize(canvas);
pageTexts.push(result.data.text.trim());
page.cleanup();
canvas.width = 1;
canvas.height = 1;
}
} finally {
context.signal.removeEventListener("abort", cancelWorker);
await worker.terminate().catch(() => undefined);
await loadingTask.destroy();
}
abortIfNeeded(context.signal);
context.reportProgress({ percentage: 97, message: "Building editable DOCX..." });
const { Document, Packer, Paragraph } = await import("docx");
const children: Array<InstanceType<typeof Paragraph>> = [];
pageTexts.forEach((text, pageIndex) => {
const lines = text.split(/\r?\n/);
lines.forEach((line, lineIndex) => {
children.push(new Paragraph({
text: line,
pageBreakBefore: options.pageBreaks && pageIndex > 0 && lineIndex === 0,
spacing: { after: line.trim() ? 120 : 0 },
}));
});
});
const docxDocument = new Document({ sections: [{ children }] });
const blob = await Packer.toBlob(docxDocument);
context.reportProgress({ percentage: 100, message: "DOCX ready." });
return {
type: "files",
files: [{
name: `${file.name.replace(/\.pdf$/i, "")}_ocr.docx`,
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
blob,
sizeBefore: file.size,
sizeAfter: blob.size,
}],
};
}