From 84d73f1b8689e393b354248503ce5de1132c608d Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Fri, 30 Oct 2026 17:26:09 +0800 Subject: [PATCH] feat: pdf encryption image extraction rendering and table reading --- electron-builder.config.mjs | 3 +- package-lock.json | 382 ++++++++++++++++++++++--- package.json | 2 + src/main/capabilities/pdf-high.test.ts | 54 ++++ src/main/capabilities/pdf.ts | 204 ++++++++++++- 5 files changed, 598 insertions(+), 47 deletions(-) create mode 100644 src/main/capabilities/pdf-high.test.ts diff --git a/electron-builder.config.mjs b/electron-builder.config.mjs index 45f9e2f..ffdaa52 100644 --- a/electron-builder.config.mjs +++ b/electron-builder.config.mjs @@ -28,7 +28,8 @@ export default { asarUnpack: [ "node_modules/onnxruntime-node/**", "node_modules/sharp/**", - "node_modules/@gutenye/**" + "node_modules/@gutenye/**", + "node_modules/@napi-rs/**" ], compression: "maximum", npmRebuild: false, diff --git a/package-lock.json b/package-lock.json index c39f455..fa46cc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,10 @@ "name": "rpa-studio", "version": "0.1.0", "dependencies": { + "@cantoo/pdf-lib": "^2.11.1", "@gutenye/ocr-models": "^1.4.2", "@gutenye/ocr-node": "^1.4.9", + "@napi-rs/canvas": "^1.1.0", "@xyflow/react": "^12.12.0", "adm-zip": "^0.5.16", "basic-ftp": "^6.2.3", @@ -360,6 +362,27 @@ "node": ">=6.9.0" } }, + "node_modules/@cantoo/pdf-lib": { + "version": "2.11.1", + "resolved": "https://registry.npmmirror.com/@cantoo/pdf-lib/-/pdf-lib-2.11.1.tgz", + "integrity": "sha512-lYuRJFfXgoP8WVE8HYSeoLfDcnSXmjMLmdZS7YvmYHngbcZfiqDOruHrZBqjYizkrNa4ad1yW4WCrPYyb26T3Q==", + "license": "MIT", + "dependencies": { + "culori": ">=4", + "fflate": "^0.8.2", + "node-html-better-parser": ">=1.5.9", + "tslib": ">=2" + }, + "peerDependencies": { + "html-entities": ">=2.3.2" + } + }, + "node_modules/@cantoo/pdf-lib/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@electron/asar": { "version": "3.4.1", "resolved": "https://registry.npmmirror.com/@electron/asar/-/asar-3.4.1.tgz", @@ -1745,11 +1768,10 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.100", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-0.1.100.tgz", - "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-1.1.0.tgz", + "integrity": "sha512-k+ph/D8H9GS8ZZ48okvHrKeJynkqAA1kMUZRuzRWNlQUDHXngq7oRaWMZBdmSfd9ZhVy9bwNKei/7wMboI/uMg==", "license": "MIT", - "optional": true, "workspaces": [ "e2e/*" ], @@ -1761,23 +1783,23 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.100", - "@napi-rs/canvas-darwin-arm64": "0.1.100", - "@napi-rs/canvas-darwin-x64": "0.1.100", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", - "@napi-rs/canvas-linux-arm64-musl": "0.1.100", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", - "@napi-rs/canvas-linux-x64-gnu": "0.1.100", - "@napi-rs/canvas-linux-x64-musl": "0.1.100", - "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", - "@napi-rs/canvas-win32-x64-msvc": "0.1.100" + "@napi-rs/canvas-android-arm64": "1.1.0", + "@napi-rs/canvas-darwin-arm64": "1.1.0", + "@napi-rs/canvas-darwin-x64": "1.1.0", + "@napi-rs/canvas-linux-arm-gnueabihf": "1.1.0", + "@napi-rs/canvas-linux-arm64-gnu": "1.1.0", + "@napi-rs/canvas-linux-arm64-musl": "1.1.0", + "@napi-rs/canvas-linux-riscv64-gnu": "1.1.0", + "@napi-rs/canvas-linux-x64-gnu": "1.1.0", + "@napi-rs/canvas-linux-x64-musl": "1.1.0", + "@napi-rs/canvas-win32-arm64-msvc": "1.1.0", + "@napi-rs/canvas-win32-x64-msvc": "1.1.0" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.100", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", - "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.1.0.tgz", + "integrity": "sha512-mKrSmRyhWK4q699Q1xthbZIZi7JBvLtsRNnOZAeFaLPOFT1yo8y82aRC2nNZbXTGJXU7fVDONgGQPr0vo3kerw==", "cpu": [ "arm64" ], @@ -1795,9 +1817,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.100", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", - "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-iqwST4g0oH+x8VCpBIYh+MYokv/mVvkM+v9oJGB0d9A3P2qBh42ogJlVyWq9SpPCrAjS7tplMb1D3OIfxl+utw==", "cpu": [ "arm64" ], @@ -1815,9 +1837,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.100", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", - "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.1.0.tgz", + "integrity": "sha512-jkeA8eVTxDum03LTvc32YJ28F6KB1U0gYwnWMfOAUQZvWWEh/kvONby4iWGJx3+Tw45UqMGMXjtw2GKNVZDhVg==", "cpu": [ "x64" ], @@ -1835,9 +1857,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.100", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", - "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.1.0.tgz", + "integrity": "sha512-Y4YkKG1ijoqkgUdTEEBNG4k+2BK5bR5jGL6S56YxCXYPwGfUd5nED8/74xyc7z+bpixPasCjSYoP3xYPOIGNxw==", "cpu": [ "arm" ], @@ -1855,9 +1877,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.100", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", - "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.1.0.tgz", + "integrity": "sha512-dE/XthqYkiWcBVtKFlZbUXwAYfDuQGqDWixznipJNmevLGpprNnTVQZyftm1RZJudkIOn2EkzYZDMMETM08Row==", "cpu": [ "arm64" ], @@ -1875,9 +1897,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.100", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", - "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.1.0.tgz", + "integrity": "sha512-DqZ/CMcHlhU56pPjv9AP0h3U7MFQ5LzC1HzMyRMotL+PFsJmZJA/ziApPYVyyz78DaVLhI8t3So3ztqtijbYpg==", "cpu": [ "arm64" ], @@ -1915,9 +1937,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.100", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", - "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.1.0.tgz", + "integrity": "sha512-uQ9JuJlRhQdHyttcMWOiwLkfIqkqaB0YFUVe835LUe12THt2b51HXkTItq0fxWT1jdhUPX2ylWZ1walkJyD6GQ==", "cpu": [ "x64" ], @@ -1935,9 +1957,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.100", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", - "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.1.0.tgz", + "integrity": "sha512-has4CPPAkkCac/LZ13VAw5p+MJLdaKVoyrhumN/A7uJd6960xYkJNDZgUebyxiha82AFzXx6RV+QJg5qGG079g==", "cpu": [ "x64" ], @@ -1955,9 +1977,9 @@ } }, "node_modules/@napi-rs/canvas-win32-arm64-msvc": { - "version": "0.1.100", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", - "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.1.0.tgz", + "integrity": "sha512-MJnktLKNijmXqdjTughHDZ9TDLZ9uUm2hvFXqaOqn0+w7lphXFnft6bpSgQAc9U1exc20UGIBlzGu9+kiCj9Ag==", "cpu": [ "arm64" ], @@ -1975,9 +1997,9 @@ } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.100", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", - "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.1.0.tgz", + "integrity": "sha512-08575bFk7iiadxap4h+JxgGut9xDCHAGum6wh0eInN53zEMImkmEHi5MushX2FOmNO7qRNX7BPiDC5ZYBwHeEg==", "cpu": [ "x64" ], @@ -4461,6 +4483,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/culori": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/culori/-/culori-4.0.2.tgz", + "integrity": "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", @@ -5570,6 +5601,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -6021,6 +6058,22 @@ "node": ">= 0.4" } }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/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/http-cache-semantics": { "version": "4.3.0", "resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", @@ -7153,6 +7206,15 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/node-html-better-parser": { + "version": "1.5.9", + "resolved": "https://registry.npmmirror.com/node-html-better-parser/-/node-html-better-parser-1.5.9.tgz", + "integrity": "sha512-z1I5UINMezJXYL9cH3h0a9KBth2G978gSLlfkpQ+CQzzVHVQy9gpARgm9eDsz1O4gn1HtgUqjdAIYxKFZm6uHQ==", + "license": "MIT", + "dependencies": { + "html-entities": "^2.3.2" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7475,6 +7537,236 @@ "@napi-rs/canvas": "^0.1.65" } }, + "node_modules/pdfjs-dist/node_modules/@napi-rs/canvas": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" + } + }, + "node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/pdfjs-dist/node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", diff --git a/package.json b/package.json index 67323bf..3bbdfaf 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,10 @@ "test:e2e": "node scripts/demo-e2e.mjs" }, "dependencies": { + "@cantoo/pdf-lib": "^2.11.1", "@gutenye/ocr-models": "^1.4.2", "@gutenye/ocr-node": "^1.4.9", + "@napi-rs/canvas": "^1.1.0", "@xyflow/react": "^12.12.0", "adm-zip": "^0.5.16", "basic-ftp": "^6.2.3", diff --git a/src/main/capabilities/pdf-high.test.ts b/src/main/capabilities/pdf-high.test.ts new file mode 100644 index 0000000..9e9343f --- /dev/null +++ b/src/main/capabilities/pdf-high.test.ts @@ -0,0 +1,54 @@ +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { PDFDocument, StandardFonts } from "pdf-lib"; +import { PNG } from "pngjs"; +import { getHandler } from "../engine/registry"; +import type { CatalogComponent, FlowNode } from "../../shared/types"; +import type { RunContext } from "../engine/types"; +import "./pdf"; + +const directories: string[] = []; +const context = {} as RunContext; +const component = {} as CatalogComponent; +const node = (id: string): FlowNode => ({ id, componentId: id, position: { x: 0, y: 0 }, values: {}, options: {} }); +async function run(id: string, args: Record) { + const handler = getHandler(id); + expect(handler, id).toBeTruthy(); + return handler!(context, node(id), args, component); +} +afterEach(async () => Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })))); + +describe("PDF high-level capabilities", () => { + it("creates a pdf fixture", async () => { + const directory = await mkdtempSafe(); + const pdf = await PDFDocument.create(); + const page = pdf.addPage([220, 120]); + const font = await pdf.embedFont(StandardFonts.Helvetica); + const png = new PNG({ width: 8, height: 8 }); + png.data.fill(255); png.data[0] = 20; png.data[1] = 90; png.data[2] = 200; png.data[3] = 255; + page.drawImage(await pdf.embedPng(PNG.sync.write(png)), { x: 140, y: 70, width: 40, height: 40 }); + for (const [value, x, y] of [["Name", 24, 90], ["Age", 120, 90], ["Alice", 24, 72], ["30", 120, 72]] as Array<[string, number, number]>) page.drawText(value, { x, y, size: 9, font }); + await writeFile(path.join(directory, "source.pdf"), await pdf.save()); + const filePath = path.join(directory, "source.pdf"); + expect((await readFile(path.join(directory, "source.pdf"))).byteLength).toBeGreaterThan(0); + const images = await run("PDF_GetPdfImg", { fileInPath: filePath, pdfRange: "all", pdfNum: "1", imgSavePath: directory, imageType: "png" }) as string[]; + expect(images.length).toBeGreaterThan(0); + expect((await readFile(images[0])).subarray(0, 4)).toEqual(Buffer.from([0x89, 0x50, 0x4e, 0x47])); + + await expect(run("PDF_GetPdfTable", { fileInPath: filePath, pdfRange: "all", pdfNum: "1" })).resolves.toEqual([["Name", "Age"], ["Alice", "30"]]); + + const encryptedPath = path.join(directory, "encrypted.pdf"); + await writeFile(encryptedPath, await readFile(filePath)); + await expect(run("PDF_PdfEncryption", { fileInPath: encryptedPath, passWord: "secret" })).resolves.toBe(encryptedPath); + const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs"); + const opened = await pdfjs.getDocument({ data: new Uint8Array(await readFile(encryptedPath)), password: "secret" }).promise; + expect(opened.numPages).toBe(1); + }); +}); +async function mkdtempSafe(): Promise { + const directory = await (await import("node:fs/promises")).mkdtemp(path.join(os.tmpdir(), "rpa-pdf-high-")); + directories.push(directory); + return directory; +} diff --git a/src/main/capabilities/pdf.ts b/src/main/capabilities/pdf.ts index 08d123f..11af737 100644 --- a/src/main/capabilities/pdf.ts +++ b/src/main/capabilities/pdf.ts @@ -1,6 +1,8 @@ import { promises as fs } from "node:fs"; -import path from "node:path"; +import fsSync from "node:fs"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { PDFDocument } from "pdf-lib"; +import path from "node:path"; import { register } from "../engine/registry"; import { RuntimeError, type CapabilityHandler } from "../engine/types"; @@ -60,3 +62,203 @@ register("PDF_GetPdfText", async (_ctx, node, args) => { } return chunks.join("\n"); }); + +type PdfJsDocument = Awaited["promise"]>; + +function standardFontUrl(): string | undefined { + const candidates = [ + process.env.RPA_PDFJS_STANDARD_FONTS, + path.join(process.cwd(), "node_modules", "pdfjs-dist", "standard_fonts"), + path.join(process.execPath, "resources", "app.asar", "node_modules", "pdfjs-dist", "standard_fonts"), + path.join(process.execPath, "..", "resources", "app.asar", "node_modules", "pdfjs-dist", "standard_fonts") + ].filter(Boolean) as string[]; + const found = candidates.find((candidate) => fsSync.existsSync(candidate)); + return found ? pathToFileURL(path.join(found, path.sep)).href : undefined; +} + +class LocalStandardFontFactory { + baseUrl: string; + constructor(options: { baseUrl: string }) { this.baseUrl = options.baseUrl; } + async fetch({ filename }: { filename: string }): Promise { + return new Uint8Array(await fs.readFile(path.join(fileURLToPath(this.baseUrl), filename))); + } +} + +async function loadPdf(filePath: string, password: string) { + const canvasModule = await import("@napi-rs/canvas"); + const globals = globalThis as unknown as Record; + globals.Path2D = canvasModule.Path2D; + globals.DOMMatrix = canvasModule.DOMMatrix; + globals.ImageData = canvasModule.ImageData; + const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs"); + return { pdfjs, document: await pdfjs.getDocument({ data: new Uint8Array(await fs.readFile(filePath)), password: password || undefined, isEvalSupported: false, useSystemFonts: false, standardFontDataUrl: standardFontUrl(), StandardFontDataFactory: LocalStandardFontFactory }).promise }; +} + +function pageIndices(document: PdfJsDocument, raw: unknown): number[] { + const range = String(raw ?? "all").trim().toLowerCase(); + if (!range || range === "all") return Array.from({ length: document.numPages }, (_, index) => index + 1); + const result = new Set(); + for (const part of range.split(/[,,;;\s]+/).filter(Boolean)) { + const match = part.match(/^(\d+)-(\d+)$/); + if (match) { + const start = Math.max(1, Number(match[1])); const end = Math.min(document.numPages, Number(match[2])); + for (let index = start; index <= end; index += 1) result.add(index); + } else { + const index = Number(part); + if (index >= 1 && index <= document.numPages) result.add(Math.floor(index)); + } + } + const values = [...result].sort((left, right) => left - right); + if (!values.length) throw new Error(`PDF 页码范围无效:${range}`); + return values; +} + +async function renderPage(page: Awaited>, scale = 2): Promise { + const canvasModule = await import("@napi-rs/canvas"); + const viewport = page.getViewport({ scale }); + const canvas = canvasModule.createCanvas(Math.ceil(viewport.width), Math.ceil(viewport.height)); + const context = canvas.getContext("2d"); + context.fillStyle = "white"; context.fillRect(0, 0, canvas.width, canvas.height); + await page.render({ canvasContext: context as unknown as CanvasRenderingContext2D, viewport }).promise; + return canvas.toBuffer("image/png"); +} + +function saveImage(folder: string, prefix: string, suffix: string, data: Buffer, type: string): string { + const ext = /jpe?g/i.test(type) ? "jpg" : "png"; + const output = path.join(path.resolve(folder), `${prefix}_${suffix}_${Date.now()}.${ext}`); + fsSync.mkdirSync(path.dirname(output), { recursive: true }); + fsSync.writeFileSync(output, data); + return output; +} + +async function rawImageToBuffer(image: Record, type: string): Promise { + const width = Number(image.width || 0); const height = Number(image.height || 0); + const data = image.data; + if (!width || !height || !(data instanceof Uint8Array) || data.length === 0) throw new Error("PDF 图片数据无效"); + if (data[0] === 0xff && data[1] === 0xd8) return Buffer.from(data); + const canvasModule = await import("@napi-rs/canvas"); + const canvas = canvasModule.createCanvas(width, height); const context = canvas.getContext("2d"); + const target = context.createImageData(width, height); const kind = Number(image.kind || 0); + if (kind === 1) { + const bytesPerRow = Math.ceil(width / 8); + for (let y = 0; y < height; y += 1) for (let x = 0; x < width; x += 1) { + const bit = (data[y * bytesPerRow + (x >> 3)] >> (7 - (x & 7))) & 1; const output = (y * width + x) * 4; const color = bit ? 255 : 0; + target.data[output] = color; target.data[output + 1] = color; target.data[output + 2] = color; target.data[output + 3] = 255; + } + } else if (kind === 3) { target.data.set(data.subarray(0, Math.min(data.length, target.data.length))); } + else { + for (let source = 0, targetIndex = 0; source < data.length && targetIndex < target.data.length; source += 3, targetIndex += 4) { + target.data[targetIndex] = data[source]; target.data[targetIndex + 1] = data[source + 1] ?? 0; target.data[targetIndex + 2] = data[source + 2] ?? 0; target.data[targetIndex + 3] = 255; + } + } + context.putImageData(target, 0, 0); + return /jpe?g/i.test(type) ? canvas.toBuffer("image/jpeg") : canvas.toBuffer("image/png"); +} + +async function objectImage(page: Awaited>, objId: unknown): Promise | null> { + if (typeof objId !== "string") return null; + const objects = objId.startsWith("g_") ? page.commonObjs : page.objs; + try { if (objects.has(objId)) return objects.get(objId) as Record; } catch {} + return await new Promise((resolve) => { try { objects.get(objId, (value: unknown) => resolve(value as Record)); } catch { resolve(null); } }); +} + +const encryption: CapabilityHandler = async (_ctx, node, args) => { + const filePath = required(text(args, ["fileInPath", "path", "file"]), "PDF文件路径", node.id, node.componentId); + const password = String(args.passWord ?? args.password ?? ""); + if (!password) throw new RuntimeError("validation_error", "PDF密码为空", { nodeId: node.id, componentId: node.componentId }); + const { PDFDocument } = await import("@cantoo/pdf-lib"); + const document = await PDFDocument.load(await fs.readFile(filePath), { ignoreEncryption: true }); + document.encrypt({ userPassword: password, ownerPassword: password, permissions: { printing: "highResolution", copying: true, modifying: false } }); + const temporary = `${filePath}.${Date.now()}.enc`; + await fs.writeFile(temporary, await document.save()); + await fs.rename(temporary, filePath); + return filePath; +}; + +const extractImages: CapabilityHandler = async (_ctx, node, args) => { + const filePath = required(text(args, ["fileInPath", "path", "file"]), "PDF文件路径", node.id, node.componentId); + const folder = required(text(args, ["imgSavePath", "pdfSavePath", "folder", "path"]), "图片保存路径", node.id, node.componentId); + const type = text(args, ["imageType", "type"], "png"); + const prefix = text(args, ["imageName", "name"], "pdf-image"); + const { pdfjs, document } = await loadPdf(filePath, String(args.pdfPassWord ?? args.password ?? "")); + const outputs: string[] = []; + for (const pageNumber of pageIndices(document, args.pdfNum)) { + const page = await document.getPage(pageNumber); + const opList = await page.getOperatorList(); + let sequence = 0; + for (let index = 0; index < opList.fnArray.length; index += 1) { + const op = opList.fnArray[index]; const commandArgs = opList.argsArray[index] as unknown[]; + let image: Record | null = null; + if (op === pdfjs.OPS.paintImageXObject || op === pdfjs.OPS.paintImageXObjectRepeat) image = await objectImage(page, commandArgs[0]); + else if (op === pdfjs.OPS.paintInlineImageXObject) image = commandArgs[0] as Record; + else if (op === pdfjs.OPS.paintInlineImageXObjectGroup) image = commandArgs[0] as Record; + if (!image) continue; + sequence += 1; + outputs.push(saveImage(folder, `${prefix}-p${pageNumber}`, String(sequence), await rawImageToBuffer(image, type), type)); + } + } + return outputs; +}; + +const pdfToImages: CapabilityHandler = async (_ctx, node, args) => { + const filePath = required(text(args, ["fileInPath", "path", "file"]), "PDF文件路径", node.id, node.componentId); + const folder = required(text(args, ["pdfSavePath", "imgSavePath", "folder", "path"]), "图片保存路径", node.id, node.componentId); + const type = text(args, ["imageType", "type"], "png"); const prefix = text(args, ["imageName", "name"], "pdf-page"); + const { document } = await loadPdf(filePath, String(args.pdfPassWord ?? args.password ?? "")); + const outputs: string[] = []; + for (const pageNumber of pageIndices(document, args.pdfNum)) { + const buffer = await renderPage(await document.getPage(pageNumber)); + outputs.push(saveImage(folder, prefix, String(pageNumber), buffer, type)); + } + return outputs; +}; + +interface TextToken { x: number; y: number; value: string; } + +async function extractTableRows(page: Awaited>): Promise { + const content = await page.getTextContent(); + const tokens = content.items.flatMap((item) => { + if (!("str" in item) || !item.str.trim()) return []; + const transform = (item as { transform?: number[] }).transform ?? [1,0,0,1,0,0]; + return [{ x: Number(transform[4] || 0), y: Number(transform[5] || 0), value: item.str.trim() }]; + }); + if (tokens.length < 2) return []; + const lines = new Map(); + for (const token of [...tokens].sort((left, right) => right.y - left.y || left.x - right.x)) { + const lineKey = [...lines.keys()].find((key) => Math.abs(key - token.y) <= 3.5) ?? token.y; + if (!lines.has(lineKey)) lines.set(lineKey, []); + lines.get(lineKey)!.push(token); + } + const lineList = [...lines.entries()].map(([y, values]) => ({ y, values: values.sort((left, right) => left.x - right.x) })).sort((left, right) => right.y - left.y); + const counts = new Map(); + for (const line of lineList) for (const token of line.values) { + const key = Math.round(token.x / 5) * 5; + counts.set(key, (counts.get(key) || 0) + 1); + } + const columns = [...counts.entries()].filter(([, count]) => count >= Math.min(2, lineList.length)).sort((left, right) => left[0] - right[0]).map(([key]) => key); + if (columns.length < 2) return []; + const rows: string[][] = []; + for (const line of lineList) { + const cells = columns.map(() => ""); + for (const token of line.values) { + let nearest = 0; + for (let index = 1; index < columns.length; index += 1) if (Math.abs(token.x - columns[index]) < Math.abs(token.x - columns[nearest])) nearest = index; + cells[nearest] = cells[nearest] ? `${cells[nearest]} ${token.value}` : token.value; + } + if (cells.filter(Boolean).length >= 2) rows.push(cells); + } + return rows.length >= 2 ? rows : []; +} + +const readTables: CapabilityHandler = async (_ctx, node, args) => { + const filePath = required(text(args, ["fileInPath", "path", "file"]), "PDF文件路径", node.id, node.componentId); + const { document } = await loadPdf(filePath, String(args.pdfPassWord ?? args.password ?? "")); + const rows: string[][] = []; + for (const pageNumber of pageIndices(document, args.pdfNum)) rows.push(...await extractTableRows(await document.getPage(pageNumber))); + return rows; +}; + +register("PDF_PdfEncryption", encryption); +register("PDF_GetPdfImg", extractImages); +register("PDF_PdfToImg", pdfToImages); +register("PDF_GetPdfTable", readTables);