diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14319294..df1825c1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,9 @@ jobs: node-version: ${{ matrix.node-version }} cache: "npm" + - name: Set up Poppler on ubuntu + run: sudo apt install -y poppler-utils + - name: Install Node.js dependencies run: npm ci diff --git a/.lintstagedrc.json b/.lintstagedrc.json deleted file mode 100644 index cfb3f2aa..00000000 --- a/.lintstagedrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "./src/**/*.ts": [ - "eslint --fix" - ] -} diff --git a/package-lock.json b/package-lock.json index 92f7ec03..3f80d37c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,15 @@ "version": "4.22.0", "license": "MIT", "dependencies": { + "canvas": "^3.0.1", "commander": "~9.4.1", "file-type": "~16.5.4", "form-data": "~3.0.1", - "pdf-lib": "~1.17.1" + "node-poppler": "^7.2.2", + "pdf-lib": "^1.17.1", + "pdf.js-extract": "^0.2.1", + "sharp": "^0.33.5", + "tmp": "^0.2.3" }, "bin": { "mindee": "bin/mindee.js" @@ -21,6 +26,7 @@ "@types/chai": "^4.3.4", "@types/mocha": "^10.0.9", "@types/node": "^18.15.11", + "@types/tmp": "^0.2.6", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", "chai": "^4.3.10", @@ -49,6 +55,16 @@ "node": ">=12" } }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.49.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz", @@ -289,6 +305,367 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -534,6 +911,13 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -909,6 +1293,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -922,6 +1326,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -952,6 +1367,30 @@ "dev": true, "license": "ISC" }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -966,7 +1405,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -975,6 +1413,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/canvas": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.0.1.tgz", + "integrity": "sha512-PcpVF4f8RubAeN/jCQQ/UymDKzOiLmRPph8fOTzDnlsUihkO/AUlxuhaa7wGRc3vMcCbV1fzuvyu5cWZlIcn1w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "simple-get": "^3.0.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -1095,6 +1548,12 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -1107,11 +1566,23 @@ "wrap-ansi": "^7.0.0" } }, + "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" @@ -1124,9 +1595,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", @@ -1229,6 +1709,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "license": "MIT", + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -1242,6 +1734,15 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1268,6 +1769,15 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -1292,6 +1802,12 @@ "node": ">=0.3.1" } }, + "node_modules/dommatrix": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/dommatrix/-/dommatrix-0.0.24.tgz", + "integrity": "sha512-PatEhAW5pIHr28MvFQGV5iiHNloqvecQZlxs7/8s/eulLqZI3uVqPkrO7YDuqsebovr/9mmcWDSWzVG4amEZgQ==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1306,6 +1822,15 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1579,6 +2104,15 @@ "node": ">=0.10.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1745,6 +2279,12 @@ "node": ">= 6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1772,6 +2312,12 @@ "node": "*" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -1983,6 +2529,18 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2404,6 +2962,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2420,6 +2990,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mocha": { "version": "10.8.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", @@ -2492,6 +3077,12 @@ "dev": true, "license": "MIT" }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2514,6 +3105,41 @@ "node": ">= 10.13" } }, + "node_modules/node-abi": { + "version": "3.71.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", + "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-poppler": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/node-poppler/-/node-poppler-7.2.2.tgz", + "integrity": "sha512-fbPnwe161tcWnh1XaWDiAiVWIyzVwKTF+GOAgVJRV6hLtCejdnqldYKdD+xBLFSWDVa4Foe0D1AIjXkk3bv22g==", + "license": "MIT", + "dependencies": { + "camelcase": "^6.3.0", + "semver": "^7.6.3", + "upath": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/Fdawgs" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2528,7 +3154,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -2671,6 +3296,25 @@ "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/pdf.js-extract": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/pdf.js-extract/-/pdf.js-extract-0.2.1.tgz", + "integrity": "sha512-oUs5KaTVCelIyiBajCx3zAZKurkN9oVwRdqbSeDqeofddxNuwJRur86fCETvKZ/tX5nZJUSZWq3ie76PsArz7A==", + "license": "MIT", + "dependencies": { + "dommatrix": "0.0.24", + "web-streams-polyfill": "3.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/peek-readable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", @@ -2697,6 +3341,84 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install/node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2728,6 +3450,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2779,6 +3511,30 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -2928,7 +3684,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2947,6 +3702,45 @@ "randombytes": "^2.1.0" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2985,6 +3779,46 @@ "@types/hast": "^3.0.4" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "license": "MIT", + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/slashes": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", @@ -3140,12 +3974,42 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/synckit/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -3256,11 +4120,24 @@ } }, "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true, "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3408,6 +4285,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/upath": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", + "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3461,6 +4348,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/web-streams-polyfill": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz", + "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3516,7 +4412,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/y18n": { diff --git a/package.json b/package.json index 705cedb2..8927dc08 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@types/chai": "^4.3.4", "@types/mocha": "^10.0.9", "@types/node": "^18.15.11", + "@types/tmp": "^0.2.6", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", "chai": "^4.3.10", @@ -55,10 +56,15 @@ "typescript": "^5.6.3" }, "dependencies": { + "canvas": "^3.0.1", "commander": "~9.4.1", "file-type": "~16.5.4", "form-data": "~3.0.1", - "pdf-lib": "~1.17.1" + "node-poppler": "^7.2.2", + "pdf-lib": "^1.17.1", + "pdf.js-extract": "^0.2.1", + "sharp": "^0.33.5", + "tmp": "^0.2.3" }, "keywords": [ "typescript", diff --git a/src/errors/mindeeError.ts b/src/errors/mindeeError.ts index 4abe8d38..1925c699 100644 --- a/src/errors/mindeeError.ts +++ b/src/errors/mindeeError.ts @@ -17,3 +17,20 @@ export class MindeeMimeTypeError extends MindeeError { this.name = "MindeeMimeTypeError"; } } + + +export class MindeeImageError extends MindeeError { + constructor(message: string) { + super(message); + this.name = "MindeeImageError"; + } +} + +export class MindeePdfError extends MindeeError { + constructor(message: string) { + super(message); + this.name = "MindeePdfError"; + } +} + + diff --git a/src/imageOperations/imageCompressor.ts b/src/imageOperations/imageCompressor.ts new file mode 100644 index 00000000..df23e82c --- /dev/null +++ b/src/imageOperations/imageCompressor.ts @@ -0,0 +1,39 @@ +import sharp from "sharp"; +import { Sharp, Metadata } from "sharp"; +import { MindeeImageError } from "../errors/mindeeError"; + +/** + * Compresses an image with the given parameters. + * + * @param imageBuffer Buffer representation of an image. + * @param quality Quality to apply to the image (JPEG). + * @param maxWidth Maximum bound for width. + * @param maxHeight Maximum bound for height. + */ +export async function compressImage( + imageBuffer: Buffer, + quality:number = 85, + maxWidth:number|null = null, + maxHeight:number|null = null, +) { + let sharpImage: Sharp = sharp(imageBuffer); + + const metadata: Metadata = await sharpImage.metadata(); + if (metadata.width === undefined || metadata.height === undefined){ + throw new MindeeImageError("Source image has invalid dimensions."); + } + maxWidth ??= metadata.width; + maxHeight ??= metadata.height; + if (maxWidth || maxHeight) { + sharpImage = sharpImage.resize({ + width: maxWidth, + height: maxHeight, + fit: "inside", + withoutEnlargement: true, + }); + } + + return await sharpImage + .jpeg({ quality: quality }) + .toBuffer(); +} diff --git a/src/imageOperations/index.ts b/src/imageOperations/index.ts index 95d5d121..573ec2ad 100644 --- a/src/imageOperations/index.ts +++ b/src/imageOperations/index.ts @@ -1,2 +1,3 @@ export { extractReceipts, ExtractedMultiReceiptImage } from "./multiReceiptsExtractor"; export { extractInvoices, ExtractedInvoiceSplitterImage } from "./invoiceSplitterExtractor"; +export { compressImage } from "./imageCompressor"; diff --git a/src/input/sources/localInputSource.ts b/src/input/sources/localInputSource.ts index 96366a19..e4862669 100644 --- a/src/input/sources/localInputSource.ts +++ b/src/input/sources/localInputSource.ts @@ -1,9 +1,11 @@ import { errorHandler } from "../../errors/handler"; import { logger } from "../../logger"; +import { compressImage } from "../../imageOperations"; +import { compressPdf } from "../../pdf"; import path from "path"; import * as fileType from "file-type"; import { PageOptions } from "../pageOptions"; -import { extractPages } from "../../pdf"; +import { extractPages, hasSourceText } from "../../pdf"; import { InputSource, InputConstructor, @@ -98,4 +100,48 @@ export abstract class LocalInputSource extends InputSource { const processedPdf = await extractPages(this.fileObject, pageOptions); this.fileObject = processedPdf.file; } + + /** + * Compresses the file object, either as a PDF or an image. + * + * @param quality Quality of the compression. For images, this is the JPEG quality. + * For PDFs, this affects image quality within the PDF. + * @param maxWidth Maximum width for image resizing. Ignored for PDFs. + * @param maxHeight Maximum height for image resizing. Ignored for PDFs. + * @param forceSourceText For PDFs, whether to force compression even if source text is present. + * @param disableSourceText For PDFs, whether to disable source text during compression. + * + * @returns A Promise that resolves when the compression is complete. + */ + async compress( + quality: number = 85, + maxWidth: number | null = null, + maxHeight: number | null = null, + forceSourceText: boolean = false, + disableSourceText: boolean = true + ) { + let buffer: Buffer; + if (typeof this.fileObject === "string") { + buffer = Buffer.from(this.fileObject); + } else { + buffer = this.fileObject; + } + if (this.isPdf()){ + this.fileObject = await compressPdf(buffer, quality, forceSourceText, disableSourceText); + } else { + this.fileObject = await compressImage(buffer, quality, maxWidth, maxHeight); + } + } + + /** + * Returns true if the object is a PDF and has source text. False otherwise. + * @return boolean + */ + async hasSourceText() { + if (!this.isPdf()){ + return false; + } + const buffer = typeof this.fileObject === "string" ? Buffer.from(this.fileObject) : this.fileObject; + return hasSourceText(buffer); + } } diff --git a/src/pdf/index.ts b/src/pdf/index.ts index a16d5d4f..9c57e895 100644 --- a/src/pdf/index.ts +++ b/src/pdf/index.ts @@ -1 +1,3 @@ export { extractPages, countPages, SplitPdf } from "./pdfOperation"; +export { compressPdf } from "./pdfCompressor"; +export { hasSourceText } from "./pdfUtils"; diff --git a/src/pdf/pdfCompressor.ts b/src/pdf/pdfCompressor.ts new file mode 100644 index 00000000..6a26972f --- /dev/null +++ b/src/pdf/pdfCompressor.ts @@ -0,0 +1,275 @@ +import { logger } from "../logger"; +import tmp from "tmp"; +import { ExtractedPdfInfo, extractTextFromPdf, hasSourceText } from "./pdfUtils"; +import * as fs from "node:fs"; +import { Poppler } from "node-poppler"; +import { PDFDocument, PDFFont, PDFPage, rgb, StandardFonts } from "pdf-lib"; +import { compressImage } from "../imageOperations"; + +/** + * Compresses each page of a provided PDF buffer. + * @param pdfData The input PDF as a Buffer. + * @param imageQuality Compression quality (70-100 for most JPG images). + * @param forceSourceTextCompression If true, attempts to re-write detected text. + * @param disableSourceText If true, doesn't re-apply source text to the output PDF. + * @returns A Promise resolving to the compressed PDF as a Buffer. + */ +export async function compressPdf( + pdfData: Buffer, + imageQuality: number = 85, + forceSourceTextCompression: boolean = false, + disableSourceText: boolean = true +): Promise { + handleCompressionWarnings(forceSourceTextCompression, disableSourceText); + if (await hasSourceText(pdfData)) { + if (forceSourceTextCompression) { + if (!disableSourceText) { + logger.warn("Re-writing PDF source-text is an EXPERIMENTAL feature."); + } else { + logger.warn("Source file contains text, but disable_source_text flag. " + + "is set to false. Resulting file will not contain any embedded text."); + } + } else { + logger.warn("Found text inside of the provided PDF file. Compression operation aborted since disableSourceText " + + "is set to 'true'." + ); + return pdfData; + } + } + + const extractedText = disableSourceText ? await extractTextFromPdf(pdfData) : null; + const extractedPdfInfo: ExtractedPdfInfo = await extractTextFromPdf(pdfData); + + const compressedPages = await compressPdfPages( + pdfData, + extractedPdfInfo, + imageQuality, + disableSourceText, + extractedText + ); + + if (!compressedPages) { + logger.warn("Could not compress PDF to a smaller size. Returning original PDF."); + return pdfData; + } + + return createNewPdfFromCompressedPages(compressedPages); +} + +/** + * Handles compression warnings based on the provided parameters. + * @param forceSourceTextCompression If true, attempts to re-write detected text. + * @param disableSourceText If true, doesn't re-apply source text to the output PDF. + */ +function handleCompressionWarnings(forceSourceTextCompression: boolean, disableSourceText: boolean): void { + if (forceSourceTextCompression) { + if (!disableSourceText) { + logger.warn("Re-writing PDF source-text is an EXPERIMENTAL feature."); + } else { + logger.warn("Source file contains text, but the disable_source_text is set to false. " + + "Resulting file will not contain any embedded text."); + } + } +} + +/** + * Compresses PDF pages and returns an array of compressed page buffers. + * @param pdfData The input PDF as a Buffer. + * @param extractedPdfInfo Extracted PDF information. + * @param imageQuality Initial compression quality. + * @param disableSourceText If true, doesn't re-apply source text to the output PDF. + * @param extractedText Extracted text from the PDF. + * @returns A Promise resolving to an array of compressed page buffers, or null if compression fails. + */ +async function compressPdfPages( + pdfData: Buffer, + extractedPdfInfo: ExtractedPdfInfo, + imageQuality: number, + disableSourceText: boolean, + extractedText: ExtractedPdfInfo | null +): Promise { + const originalSize = pdfData.length; + const MIN_QUALITY = 1; + let imageQualityLoop = imageQuality; + + while (imageQualityLoop >= MIN_QUALITY) { + const compressedPages = await compressPagesWithQuality( + pdfData, + extractedPdfInfo, + imageQualityLoop, + disableSourceText, + extractedText + ); + const totalCompressedSize = calculateTotalCompressedSize(compressedPages); + + if (isCompressionSuccessful(totalCompressedSize, originalSize, imageQuality)) { + return compressedPages; + } + + imageQualityLoop -= Math.round(lerp(1, 10, imageQualityLoop / 100)); + } + + return null; +} + +/** + * Compresses pages with a specific quality. + * @param pdfData The input PDF as a Buffer. + * @param extractedPdfInfo Extracted PDF information. + * @param imageQuality Compression quality. + * @param disableSourceText If true, doesn't re-apply source text to the output PDF. + * @param extractedText Extracted text from the PDF. + * @returns A Promise resolving to an array of compressed page buffers. + */ +async function compressPagesWithQuality( + pdfData: Buffer, + extractedPdfInfo: ExtractedPdfInfo, + imageQuality: number, + disableSourceText: boolean, + extractedText: ExtractedPdfInfo | null +): Promise { + const pdfDoc = await PDFDocument.load(pdfData); + const compressedPages: Buffer[] = []; + + for (let i = 0; i < extractedPdfInfo.pages.length; i++) { + const page = pdfDoc.getPages()[i]; + const rasterizedPage = await rasterizePage(pdfData, i + 1, imageQuality); + const compressedImage = await compressImage(Buffer.from(rasterizedPage, "binary"), imageQuality); + + if (!disableSourceText) { + await addTextToPdfPage(page, extractedText); + } + + compressedPages.push(compressedImage); + } + + return compressedPages; +} + +/** + * Calculates the total size of compressed pages. + * @param compressedPages Array of compressed page buffers. + * @returns The total size of compressed pages. + */ +function calculateTotalCompressedSize(compressedPages: Buffer[]): number { + return compressedPages.reduce((sum, page) => sum + page.length, 0); +} + +/** + * Checks if the compression was successful based on the compressed size and original size. + * Note: Not quite sure how or why the rasterization quality ratio is correlated with the overhead generated by the + * image's inclusion into the pdf data, but this makes the following lerp() necessary if we want consistency during + * compression. + * + * @param totalCompressedSize Total size of compressed pages. + * @param originalSize Original PDF size. + * @param imageQuality Compression quality. + * @returns True if compression was successful, false otherwise. + */ +function isCompressionSuccessful(totalCompressedSize: number, originalSize: number, imageQuality: number): boolean { + const overhead = lerp(0.54, 0.18, imageQuality / 100); + return totalCompressedSize + totalCompressedSize * overhead < originalSize; +} + +/** + * Creates a new PDF document from compressed page buffers. + * @param compressedPages Array of compressed page buffers. + * @returns A Promise resolving to the new PDF as a Buffer. + */ +async function createNewPdfFromCompressedPages(compressedPages: Buffer[]): Promise { + const newPdfDoc = await PDFDocument.create(); + + for (const compressedPage of compressedPages) { + const image = await newPdfDoc.embedJpg(compressedPage); + const newPage = newPdfDoc.addPage([image.width, image.height]); + newPage.drawImage(image, { + x: 0, + y: 0, + width: image.width, + height: image.height, + }); + } + + const compressedPdfBytes = await newPdfDoc.save(); + return Buffer.from(compressedPdfBytes); +} + +async function addTextToPdfPage( + page: PDFPage, + textInfo: ExtractedPdfInfo | null +): Promise { + if (textInfo === null) { + return; + } + for (const textPages of textInfo.pages) { + for (const textPage of textPages.content) { + page.drawText(textPage.str, { + x: textPage.x, + y: textPage.y, + size: textPage.height, + color: rgb(0, 0, 0), + font: await getFontFromName(textPage.fontName) + }); + } + } +} + +async function getFontFromName(fontName: string): Promise { + const pdfDoc = await PDFDocument.create(); + let font: PDFFont; + if (Object.values(StandardFonts).map(value => value.toString()).includes(fontName)) { + font = await pdfDoc.embedFont(fontName); + } else { + font = await pdfDoc.embedFont(StandardFonts.Helvetica); + } + + return font; +} + +/** + * Rasterizes a PDF page. + * + * @param pdfData Buffer representation of the entire PDF file. + * @param index Index of the page to rasterize. + * @param quality Quality to apply during rasterization. + */ +async function rasterizePage(pdfData: Buffer, index: number, quality = 85): Promise { + const poppler = new Poppler(); + const tmpPdf = tmp.fileSync(); + const tempPdfPath = tmpPdf.name; + const antialiasOption: "fast" | "best" | "default" | "good" | "gray" | "none" | "subpixel" = "best"; + try { + await fs.promises.writeFile(tempPdfPath, pdfData); + const options = { + antialias: antialiasOption, + firstPageToConvert: index, + lastPageToConvert: index, + jpegFile: true, + jpegOptions: `quality=${quality}`, + singleFile: true + }; + + const jpegBuffer = await poppler.pdfToCairo(tempPdfPath, undefined, options); + + await fs.promises.unlink(tempPdfPath); + + return jpegBuffer; + } catch (error) { + console.error("Error rasterizing PDF:", error); + throw error; + } finally { + tmpPdf.removeCallback(); + } +} + +/** + * Performs linear interpolation between two numbers. + * @param start The starting value. + * @param end The ending value. + * @param t The interpolation factor (0 to 1). + * @returns The interpolated value. + */ +function lerp(start: number, end: number, t: number): number { + return start * (1 - t) + end * t; +} + diff --git a/src/pdf/pdfUtils.ts b/src/pdf/pdfUtils.ts new file mode 100644 index 00000000..bf7e7d27 --- /dev/null +++ b/src/pdf/pdfUtils.ts @@ -0,0 +1,81 @@ +import { PDFExtract, PDFExtractOptions, PDFExtractResult } from "pdf.js-extract"; +import { MindeePdfError } from "../errors/mindeeError"; + + +export interface PageTextInfo { + pageNumber: number; + content: Array<{ + str: string; + x: number; + y: number; + width: number; + height: number; + fontName: string; + }>; +} + +export interface ExtractedPdfInfo { + pages: PageTextInfo[]; + getConcatenatedText: () => string; +} + + +function getConcatenatedText(pages: PageTextInfo[]): string { + return pages.flatMap( + page => page.content.map( + item => item.str) + ).join(" "); +} + +/** + * Extracts text from a full PDF document. + * + * @returns A Promise containing the extracted text as a string. + * @param pdfBuffer PDF handle, as a buffer. + */ +export async function extractTextFromPdf(pdfBuffer: Buffer): Promise { + const pdfExtract = new PDFExtract(); + const options: PDFExtractOptions = {}; + + const pdf = await new Promise((resolve, reject) => { + pdfExtract.extractBuffer(pdfBuffer, options, (err, result) => { + if (err) reject(err); + if (result === undefined) + reject(new MindeePdfError("Couldn't process result.")); + else resolve(result); + }); + }); + + const pages = pdf.pages.map((page, index) => ({ + pageNumber: index + 1, + content: page.content.map(item => ({ + str: item.str, + x: item.x, + y: item.y, + width: item.width, + height: item.height, + fontName: item.fontName, + })), + })); + + return { + pages, + getConcatenatedText: () => getConcatenatedText(pages), + }; +} + + + + + +/** + * Checks if a PDF contains source text. + * + * @param pdfData Buffer representing the content of the PDF file. + * + * @returns A Promise containing a boolean indicating if the PDF has source text. + */ +export async function hasSourceText(pdfData: Buffer): Promise { + const text = await extractTextFromPdf(pdfData); + return text.getConcatenatedText().trim().length > 0; +} diff --git a/tests/inputs/sources.spec.ts b/tests/inputs/sources.spec.ts index 80b059e0..aa5de4b8 100644 --- a/tests/inputs/sources.spec.ts +++ b/tests/inputs/sources.spec.ts @@ -1,21 +1,31 @@ import { Base64Input, - PathInput, - StreamInput, - BytesInput, BufferInput, - INPUT_TYPE_BYTES, - INPUT_TYPE_STREAM, - INPUT_TYPE_PATH, + BytesInput, INPUT_TYPE_BASE64, INPUT_TYPE_BUFFER, + INPUT_TYPE_BYTES, + INPUT_TYPE_PATH, + INPUT_TYPE_STREAM, + PathInput, + StreamInput, } from "../../src/input"; import * as fs from "fs"; import * as path from "path"; import { expect } from "chai"; +import { loadImage } from "canvas"; import { Buffer } from "node:buffer"; +import { compressImage } from "../../src/imageOperations"; +import { compressPdf } from "../../src/pdf"; +import { extractTextFromPdf } from "../../src/pdf/pdfUtils"; describe("Test different types of input", () => { + const resourcesPath = path.join(__dirname, "../data"); + const outputPath = path.join(resourcesPath, "output"); + + before(async () => { + await fs.promises.mkdir(outputPath, { recursive: true }); + }); it("should accept base64 inputs", async () => { const b64Input = await fs.promises.readFile( path.join(__dirname, "../data/file_types/receipt.txt") @@ -134,4 +144,208 @@ describe("Test different types of input", () => { expect(input.isPdf()).to.be.true; expect(input.fileObject).to.be.instanceOf(Buffer); }); + + + it("Image Quality Compress From Input Source", async () => { + const receiptInput = new PathInput({ inputPath: path.join(resourcesPath, "file_types/receipt.jpg") }); + await receiptInput.init(); + await receiptInput.compress(40); + await fs.promises.writeFile(path.join(outputPath, "compress_indirect.jpg"), receiptInput.fileObject); + + const initialFileStats = await fs.promises.stat(path.join(resourcesPath, "file_types/receipt.jpg")); + const renderedFileStats = await fs.promises.stat(path.join(outputPath, "compress_indirect.jpg")); + expect(renderedFileStats.size).to.be.lessThan(initialFileStats.size); + }); + + it("Image Quality Compresses From Compressor", async () => { + const receiptInput = new PathInput({ inputPath: path.join(resourcesPath, "file_types/receipt.jpg") }); + await receiptInput.init(); + const compresses = [ + await compressImage(receiptInput.fileObject, 100), + await compressImage(receiptInput.fileObject), + await compressImage(receiptInput.fileObject, 50), + await compressImage(receiptInput.fileObject, 10), + await compressImage(receiptInput.fileObject, 1) + ]; + + const fileNames = ["compress100.jpg", "compress75.jpg", "compress50.jpg", "compress10.jpg", "compress1.jpg"]; + for (let i = 0; i < compresses.length; i++) { + await fs.promises.writeFile(path.join(outputPath, fileNames[i]), compresses[i]); + } + + const initialFileStats = await fs.promises.stat(path.join(resourcesPath, "file_types/receipt.jpg")); + const renderedFileStats = await Promise.all( + fileNames.map(fileName => fs.promises.stat(path.join(outputPath, fileName))) + ); + + expect(initialFileStats.size).to.be.lessThan(renderedFileStats[0].size); + expect(initialFileStats.size).to.be.lessThan(renderedFileStats[1].size); + expect(renderedFileStats[1].size).to.be.greaterThan(renderedFileStats[2].size); + expect(renderedFileStats[2].size).to.be.greaterThan(renderedFileStats[3].size); + expect(renderedFileStats[3].size).to.be.greaterThan(renderedFileStats[4].size); + }); + + it("Image Resize From InputSource", async () => { + const imageResizeInput = new PathInput({ inputPath: path.join(resourcesPath, "file_types/receipt.jpg") }); + await imageResizeInput.init(); + + await imageResizeInput.compress(75, 250, 1000); + await fs.promises.writeFile(path.join(outputPath, "resize_indirect.jpg"), imageResizeInput.fileObject); + + const initialFileStats = await fs.promises.stat(path.join(resourcesPath, "file_types/receipt.jpg")); + const renderedFileStats = await fs.promises.stat(path.join(outputPath, "resize_indirect.jpg")); + expect(renderedFileStats.size).to.be.lessThan(initialFileStats.size); + const image = await loadImage(imageResizeInput.fileObject); + expect(image.width).to.be.equals(250); + expect(image.height).to.be.equals(333); + }); + + it("Image Resize From Compressor", async () => { + const imageResizeInput = new PathInput({ inputPath: path.join(resourcesPath, "file_types/receipt.jpg") }); + await imageResizeInput.init(); + + const resizes = [ + await compressImage(imageResizeInput.fileObject, 75, 500), + await compressImage(imageResizeInput.fileObject, 75, 250, 500), + await compressImage(imageResizeInput.fileObject, 75, 500, 250), + await compressImage(imageResizeInput.fileObject, 75, null, 250) + ]; + + const fileNames = ["resize500xnull.jpg", "resize250x500.jpg", "resize500x250.jpg", "resizenullx250.jpg"]; + for (let i = 0; i < resizes.length; i++) { + await fs.promises.writeFile(path.join(outputPath, fileNames[i]), resizes[i]); + } + + const initialFileStats = await fs.promises.stat(path.join(resourcesPath, "file_types/receipt.jpg")); + const renderedFileStats = await Promise.all( + fileNames.map(fileName => fs.promises.stat(path.join(outputPath, fileName))) + ); + + expect(initialFileStats.size).to.be.greaterThan(renderedFileStats[0].size); + expect(renderedFileStats[0].size).to.be.greaterThan(renderedFileStats[1].size); + expect(renderedFileStats[1].size).to.be.greaterThan(renderedFileStats[2].size); + expect(renderedFileStats[2].size).to.be.equals(renderedFileStats[3].size); + }); + + + it("PDF Input Has Text", async () => { + const hasSourceTextPath = path.join(resourcesPath, "file_types/pdf/multipage.pdf"); + const hasNoSourceTextPath = path.join(resourcesPath, "file_types/pdf/blank_1.pdf"); + const hasNoSourceTextSinceItsImagePath = path.join(resourcesPath, "file_types/receipt.jpg"); + + const hasSourceTextInput = new PathInput({ inputPath: hasSourceTextPath }); + const hasNoSourceTextInput = new PathInput({ inputPath: hasNoSourceTextPath }); + const hasNoSourceTextSinceItsImageInput = new PathInput({ inputPath: hasNoSourceTextSinceItsImagePath }); + + await hasSourceTextInput.init(); + await hasNoSourceTextInput.init(); + await hasNoSourceTextSinceItsImageInput.init(); + + expect(await hasSourceTextInput.hasSourceText()).to.be.true; + expect(await hasNoSourceTextInput.hasSourceText()).to.be.false; + expect(await hasNoSourceTextSinceItsImageInput.hasSourceText()).to.be.false; + }); + + it("PDF Compress From InputSource", async () => { + const pdfResizeInput = new PathInput( + { inputPath: path.join(resourcesPath, "products/invoice_splitter/default_sample.pdf") } + ); + await pdfResizeInput.init(); + + const compressedPdf = await compressPdf(pdfResizeInput.fileObject, 75, true); + await fs.promises.writeFile(path.join(outputPath, "resize_indirect.pdf"), compressedPdf); + + const initialFileStats = await fs.promises.stat( + path.join( + resourcesPath, + "products/invoice_splitter/default_sample.pdf" + ) + ); + const renderedFileStats = await fs.promises.stat(path.join(outputPath, "resize_indirect.pdf")); + + expect(renderedFileStats.size).to.be.lessThan(initialFileStats.size); + }).timeout(10000); + + it("PDF Compress From Compressor", async () => { + const pdfResizeInput = new PathInput( + { inputPath: path.join(resourcesPath, "products/invoice_splitter/default_sample.pdf") } + ); + await pdfResizeInput.init(); + + const resizes = [ + await compressPdf(pdfResizeInput.fileObject, 85), + await compressPdf(pdfResizeInput.fileObject, 75), + await compressPdf(pdfResizeInput.fileObject, 50), + await compressPdf(pdfResizeInput.fileObject, 10) + ]; + + const fileNames = ["compress85.pdf", "compress75.pdf", "compress50.pdf", "compress10.pdf"]; + for (let i = 0; i < resizes.length; i++) { + await fs.promises.writeFile(path.join(outputPath, fileNames[i]), resizes[i]); + } + + const initialFileStats = await fs.promises.stat( + path.join(resourcesPath, "products/invoice_splitter/default_sample.pdf") + ); + const renderedFileStats = await Promise.all( + fileNames.map(fileName => fs.promises.stat(path.join(outputPath, fileName))) + ); + + expect(initialFileStats.size).to.be.greaterThan(renderedFileStats[0].size); + expect(renderedFileStats[0].size).to.be.greaterThan(renderedFileStats[1].size); + expect(renderedFileStats[1].size).to.be.greaterThan(renderedFileStats[2].size); + expect(renderedFileStats[2].size).to.be.greaterThan(renderedFileStats[3].size); + }).timeout(20000); + + it("PDF Compress With Text Keeps Text", async () => { + const initialWithText = new PathInput({ inputPath: path.join(resourcesPath, "file_types/pdf/multipage.pdf") }); + await initialWithText.init(); + + const compressedWithText = await compressPdf(initialWithText.fileObject, 100, true, false); + + const originalText = (await extractTextFromPdf(initialWithText.fileObject)).getConcatenatedText(); + const compressedText = (await extractTextFromPdf(compressedWithText)).getConcatenatedText(); + + expect(compressedText).to.equal(originalText); + }).timeout(60000); + + it("PDF Compress With Text Does Not Compress", async () => { + const initialWithText = new PathInput({ inputPath: path.join(resourcesPath, "file_types/pdf/multipage.pdf") }); + await initialWithText.init(); + + const compressedWithText = await compressPdf(initialWithText.fileObject, 50); + + expect(compressedWithText).to.deep.equal(initialWithText.fileObject); + }).timeout(10000); + + after(async function () { + const createdFiles: string[] = [ + "compress10.pdf", + "compress50.pdf", + "compress75.pdf", + "compress85.pdf", + "resize_indirect.pdf", + "compress1.jpg", + "compress10.jpg", + "compress50.jpg", + "compress75.jpg", + "compress100.jpg", + "compress_indirect.jpg", + "resize250x500.jpg", + "resize500x250.jpg", + "resize500xnull.jpg", + "resize_indirect.jpg", + "resizenullx250.jpg", + ]; + + for (const filePath of createdFiles) { + try { + await fs.promises.unlink(path.join(resourcesPath, "output", filePath)); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + console.warn(`Could not delete file '${filePath}': ${(error as Error).message}`); + } + } + } + }); });