qpdf-compress 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +3 -2
- package/THIRD-PARTY-NOTICES.md +44 -0
- package/binding.gyp +7 -4
- package/dist/concurrency.js +1 -1
- package/dist/concurrency.js.map +1 -1
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +50 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -5
- package/dist/index.js.map +1 -1
- package/lib/errors.ts +54 -0
- package/lib/index.ts +16 -8
- package/package.json +15 -8
- package/scripts/download-harfbuzz.mjs +10 -19
- package/scripts/download-mozjpeg.mjs +10 -19
- package/scripts/download-qpdf.mjs +39 -25
- package/scripts/install.mjs +5 -4
- package/scripts/lib/integrity.mjs +109 -0
- package/scripts/native-deps.json +23 -0
- package/scripts/verify-checksums.mjs +58 -0
- package/src/qpdf_addon.cc +32 -9
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.7.0] - 2026-06-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Typed error hierarchy: `QpdfError` with `QpdfFileError`, `QpdfFormatError`, and `QpdfPasswordError` subclasses for actionable compression failures
|
|
14
|
+
- `.nvmrc`, `.editorconfig`, and a `packageManager` field for reproducible toolchains
|
|
15
|
+
- `typecheck` CI step and a weekly `windows-latest` canary job that re-tests the `windows-2022` pin
|
|
16
|
+
- `./package.json` to the package `exports` map
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Raised the Node.js `engines` floor to `>=22.0.0`, dropping end-of-life Node 20
|
|
21
|
+
- Modernized `tsconfig`: `nodenext` module resolution, `es2023` target, and stricter type-checking (`verbatimModuleSyntax`, `exactOptionalPropertyTypes`, `noUnusedLocals`/`noUnusedParameters`, `noImplicitOverride`, `noFallthroughCasesInSwitch`)
|
|
22
|
+
- Reworked ESLint to type-aware linting for `lib/`, adopted the `globals` package, and dropped `eslint-plugin-prettier`; switched coverage to Vitest's v8 provider
|
|
23
|
+
- Bumped the native build to C++20 and Node-API 9 (C++ standard defined once via a gyp variable)
|
|
24
|
+
- Hardened and sped up CI/CD: pinned all GitHub Actions to commit SHAs, added npm and native-dependency caching, a concurrency group, job timeouts, and top-level least-privilege permissions
|
|
25
|
+
- Migrated npm publish to OIDC trusted publishing (dropped `NPM_TOKEN`)
|
|
26
|
+
- Switched QPDF's source download to the official byte-stable release tarball; HarfBuzz uses the SHA-256-pinned GitHub git-archive `.tar.gz` (its only release asset is `.tar.xz`, which hangs Windows `tar` during extraction)
|
|
27
|
+
|
|
28
|
+
### Security
|
|
29
|
+
|
|
30
|
+
- Pin and verify the SHA-256 of every downloaded native dependency (`scripts/native-deps.json`) via a hardened downloader (timeout, retry, atomic writes, origin pinning), with an `npm run verify:checksums` tripwire
|
|
31
|
+
- Added OpenSSF Scorecard scanning, Dependabot updates, and a `SECURITY.md` policy (CodeQL code scanning runs via GitHub's default setup)
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **Windows build**: upgraded node-gyp to 13 and dropped Node 20 from the CI matrix; pinned Windows CI to `windows-2022` to avoid the VS 2026 MSVC internal compiler error (C1001); fixed Windows zlib detection and addon linking
|
|
36
|
+
|
|
37
|
+
## [0.6.4] - 2026-06-26
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **Double-free on teardown**: fixed a double free of the addon instance data during Node.js environment teardown
|
|
42
|
+
|
|
7
43
|
## [0.6.3] - 2026-04-26
|
|
8
44
|
|
|
9
45
|
### Changed
|
|
@@ -156,3 +192,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|
|
156
192
|
- Release pipeline with prebuild generation and npm provenance
|
|
157
193
|
- QEMU-based cross-compilation for Linux arm64/arm and musl variants
|
|
158
194
|
- vcpkg integration for Windows static linking
|
|
195
|
+
|
|
196
|
+
[Unreleased]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.7.0...HEAD
|
|
197
|
+
[0.7.0]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.6.4...v0.7.0
|
|
198
|
+
[0.6.4]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.6.3...v0.6.4
|
|
199
|
+
[0.6.3]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.6.2...v0.6.3
|
|
200
|
+
[0.6.2]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.6.1...v0.6.2
|
|
201
|
+
[0.6.1]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.6.0...v0.6.1
|
|
202
|
+
[0.6.0]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.5.0...v0.6.0
|
|
203
|
+
[0.5.0]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.4.0...v0.5.0
|
|
204
|
+
[0.4.0]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.3.0...v0.4.0
|
|
205
|
+
[0.3.0]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.2.0...v0.3.0
|
|
206
|
+
[0.2.0]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.1.3...v0.2.0
|
|
207
|
+
[0.1.3]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.1.2...v0.1.3
|
|
208
|
+
[0.1.2]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.1.1...v0.1.2
|
|
209
|
+
[0.1.1]: https://github.com/xonaman/nodejs-qpdf-compress/compare/v0.1.0...v0.1.1
|
|
210
|
+
[0.1.0]: https://github.com/xonaman/nodejs-qpdf-compress/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# qpdf-compress
|
|
2
2
|
|
|
3
|
+
[](https://github.com/xonaman/nodejs-qpdf-compress/actions/workflows/ci.yml)
|
|
3
4
|
[](https://www.npmjs.com/package/qpdf-compress)
|
|
4
5
|
[](https://nodejs.org)
|
|
5
6
|
[](https://github.com/xonaman/nodejs-qpdf-compress/blob/main/LICENSE)
|
|
6
|
-
[]()
|
|
7
|
+
[](#-supported-platforms)
|
|
7
8
|
|
|
8
9
|
Native PDF compression for Node.js — powered by [QPDF](https://qpdf.sourceforge.io/), the industry-standard PDF transformation library. Built as a C++ addon with N-API for ABI stability across Node.js versions.
|
|
9
10
|
|
|
@@ -40,7 +41,7 @@ const smaller = await compress(pdfBuffer, { lossy: true });
|
|
|
40
41
|
**🔒 Reliability**
|
|
41
42
|
|
|
42
43
|
- Built on QPDF — the most widely used PDF structural transformation library
|
|
43
|
-
- ABI-stable via N-API — works across Node.js
|
|
44
|
+
- ABI-stable via N-API — works across Node.js 22+ without recompilation
|
|
44
45
|
- Automatic repair — damaged PDFs are reconstructed during compression
|
|
45
46
|
- Apache-2.0 licensed — no AGPL/copyleft concerns
|
|
46
47
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Third-Party Notices
|
|
2
|
+
|
|
3
|
+
`qpdf-compress` is distributed under the [Apache-2.0](./LICENSE) license. The
|
|
4
|
+
published package and its prebuilt native binaries statically link the
|
|
5
|
+
third-party libraries listed below. Each remains under its own license, and the
|
|
6
|
+
full license text of each is available from the upstream project linked.
|
|
7
|
+
|
|
8
|
+
| Library | Version | License | Source |
|
|
9
|
+
| -------- | ------------------ | --------------------------------------------------------- | -------------------------------------- |
|
|
10
|
+
| QPDF | 12.3.2 | Apache-2.0 | <https://github.com/qpdf/qpdf> |
|
|
11
|
+
| mozjpeg | 4.1.1 | BSD-3-Clause and IJG (with zlib-licensed SIMD components) | <https://github.com/mozilla/mozjpeg> |
|
|
12
|
+
| HarfBuzz | 14.1.0 | MIT ("Old MIT") | <https://github.com/harfbuzz/harfbuzz> |
|
|
13
|
+
| zlib | bundled on Windows | zlib License | <https://github.com/madler/zlib> |
|
|
14
|
+
|
|
15
|
+
## Details
|
|
16
|
+
|
|
17
|
+
### QPDF
|
|
18
|
+
|
|
19
|
+
Copyright Jay Berkenbilt and QPDF contributors. Licensed under the Apache
|
|
20
|
+
License, Version 2.0. QPDF performs the structural PDF transformation and
|
|
21
|
+
compression. See <https://github.com/qpdf/qpdf/blob/main/LICENSE.txt>.
|
|
22
|
+
|
|
23
|
+
### mozjpeg
|
|
24
|
+
|
|
25
|
+
Copyright the Mozilla Foundation and contributors; derived from libjpeg-turbo
|
|
26
|
+
and the Independent JPEG Group (IJG) library. mozjpeg is covered by three
|
|
27
|
+
licenses: the IJG License, a BSD-3-Clause-style license for libjpeg-turbo
|
|
28
|
+
modifications, and the zlib License for the SIMD extensions. mozjpeg is used for
|
|
29
|
+
lossless JPEG Huffman optimization and lossy JPEG re-encoding. See
|
|
30
|
+
<https://github.com/mozilla/mozjpeg/blob/master/LICENSE.md>.
|
|
31
|
+
|
|
32
|
+
### HarfBuzz
|
|
33
|
+
|
|
34
|
+
Copyright the HarfBuzz contributors. Licensed under the "Old MIT" license. Used
|
|
35
|
+
for TrueType/OpenType font handling and subsetting (`harfbuzz` and
|
|
36
|
+
`harfbuzz-subset`). See
|
|
37
|
+
<https://github.com/harfbuzz/harfbuzz/blob/main/COPYING>.
|
|
38
|
+
|
|
39
|
+
### zlib
|
|
40
|
+
|
|
41
|
+
Copyright Jean-loup Gailly and Mark Adler. Licensed under the zlib License.
|
|
42
|
+
zlib provides DEFLATE compression and is statically linked into the Windows
|
|
43
|
+
build (via vcpkg); on macOS and Linux the system zlib is used at build time.
|
|
44
|
+
See <https://github.com/madler/zlib/blob/master/LICENSE>.
|
package/binding.gyp
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
"targets": [
|
|
3
3
|
{
|
|
4
4
|
"target_name": "qpdf_compress",
|
|
5
|
+
"variables": {
|
|
6
|
+
"cpp_std%": "c++20"
|
|
7
|
+
},
|
|
5
8
|
"sources": [
|
|
6
9
|
"src/qpdf_addon.cc",
|
|
7
10
|
"src/jpeg.cc",
|
|
@@ -20,7 +23,7 @@
|
|
|
20
23
|
"src"
|
|
21
24
|
],
|
|
22
25
|
"defines": [
|
|
23
|
-
"NAPI_VERSION=
|
|
26
|
+
"NAPI_VERSION=9"
|
|
24
27
|
],
|
|
25
28
|
"cflags!": [
|
|
26
29
|
"-fno-exceptions"
|
|
@@ -35,7 +38,7 @@
|
|
|
35
38
|
"-fno-exceptions"
|
|
36
39
|
],
|
|
37
40
|
"cflags_cc": [
|
|
38
|
-
"-std
|
|
41
|
+
"-std=<(cpp_std)",
|
|
39
42
|
"-fvisibility=hidden"
|
|
40
43
|
],
|
|
41
44
|
"conditions": [
|
|
@@ -49,7 +52,7 @@
|
|
|
49
52
|
],
|
|
50
53
|
"xcode_settings": {
|
|
51
54
|
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
|
52
|
-
"CLANG_CXX_LANGUAGE_STANDARD": "
|
|
55
|
+
"CLANG_CXX_LANGUAGE_STANDARD": "<(cpp_std)",
|
|
53
56
|
"GCC_SYMBOLS_PRIVATE_EXTERN": "YES",
|
|
54
57
|
"DEAD_CODE_STRIPPING": "YES",
|
|
55
58
|
"LLVM_LTO": "YES",
|
|
@@ -106,7 +109,7 @@
|
|
|
106
109
|
"WholeProgramOptimization": "true",
|
|
107
110
|
"RuntimeLibrary": 0,
|
|
108
111
|
"AdditionalOptions": [
|
|
109
|
-
"/std
|
|
112
|
+
"/std:<(cpp_std)"
|
|
110
113
|
]
|
|
111
114
|
},
|
|
112
115
|
"VCLinkerTool": {
|
package/dist/concurrency.js
CHANGED
package/dist/concurrency.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../lib/concurrency.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,
|
|
1
|
+
{"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../lib/concurrency.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,MAAM,EAAE,EAAsB,MAAM,SAAS,CAAC;AAErD,IAAI,KAAK,GAAkB,MAAM,CAAC,oBAAoB,EAAE,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,SAAS,CAAC,4CAA4C,CAAC,CAAC;QACpE,CAAC;QACD,KAAK,GAAG,MAAM,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,oBAAoB,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,KAAK,CAAC,WAAW,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,eAAe,CAAI,EAAoB;IACrD,OAAO,KAAK,CAAC,EAAE,CAAC,CAAC;AACnB,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Base error for all qpdf-compress operations. */
|
|
2
|
+
export declare class QpdfError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
constructor(code: string, message: string);
|
|
5
|
+
}
|
|
6
|
+
/** Thrown when an input or output file cannot be read or written. */
|
|
7
|
+
export declare class QpdfFileError extends QpdfError {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
/** Thrown when the input is not a valid PDF or is too damaged to recover. */
|
|
11
|
+
export declare class QpdfFormatError extends QpdfError {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
/** Thrown when the PDF is encrypted and cannot be opened without a password. */
|
|
15
|
+
export declare class QpdfPasswordError extends QpdfError {
|
|
16
|
+
constructor(message: string);
|
|
17
|
+
}
|
|
18
|
+
/** Decode the native "CODE:message" convention into a typed error. */
|
|
19
|
+
export declare function parseNativeError(err: unknown): QpdfError;
|
|
20
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../lib/errors.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,qBAAa,SAAU,SAAQ,KAAK;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBACV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK1C;AAED,qEAAqE;AACrE,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED,6EAA6E;AAC7E,qBAAa,eAAgB,SAAQ,SAAS;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED,gFAAgF;AAChF,qBAAa,iBAAkB,SAAQ,SAAS;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAkBxD"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Base error for all qpdf-compress operations. */
|
|
2
|
+
export class QpdfError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'QpdfError';
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** Thrown when an input or output file cannot be read or written. */
|
|
11
|
+
export class QpdfFileError extends QpdfError {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super('FILE', message);
|
|
14
|
+
this.name = 'QpdfFileError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Thrown when the input is not a valid PDF or is too damaged to recover. */
|
|
18
|
+
export class QpdfFormatError extends QpdfError {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super('FORMAT', message);
|
|
21
|
+
this.name = 'QpdfFormatError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Thrown when the PDF is encrypted and cannot be opened without a password. */
|
|
25
|
+
export class QpdfPasswordError extends QpdfError {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super('PASSWORD', message);
|
|
28
|
+
this.name = 'QpdfPasswordError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Decode the native "CODE:message" convention into a typed error. */
|
|
32
|
+
export function parseNativeError(err) {
|
|
33
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
34
|
+
const colonIdx = msg.indexOf(':');
|
|
35
|
+
if (colonIdx === -1)
|
|
36
|
+
return new QpdfError('UNKNOWN', msg);
|
|
37
|
+
const code = msg.slice(0, colonIdx);
|
|
38
|
+
const text = msg.slice(colonIdx + 1);
|
|
39
|
+
switch (code) {
|
|
40
|
+
case 'FILE':
|
|
41
|
+
return new QpdfFileError(text);
|
|
42
|
+
case 'FORMAT':
|
|
43
|
+
return new QpdfFormatError(text);
|
|
44
|
+
case 'PASSWORD':
|
|
45
|
+
return new QpdfPasswordError(text);
|
|
46
|
+
default:
|
|
47
|
+
return new QpdfError(code, text);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../lib/errors.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,MAAM,OAAO,SAAU,SAAQ,KAAK;IACzB,IAAI,CAAS;IACtB,YAAY,IAAY,EAAE,OAAe;QACvC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,qEAAqE;AACrE,MAAM,OAAO,aAAc,SAAQ,SAAS;IAC1C,YAAY,OAAe;QACzB,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED,6EAA6E;AAC7E,MAAM,OAAO,eAAgB,SAAQ,SAAS;IAC5C,YAAY,OAAe;QACzB,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED,gFAAgF;AAChF,MAAM,OAAO,iBAAkB,SAAQ,SAAS;IAC9C,YAAY,OAAe;QACzB,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED,sEAAsE;AACtE,MAAM,UAAU,gBAAgB,CAAC,GAAY;IAC3C,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,SAAS,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAE1D,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;IAErC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,MAAM;YACT,OAAO,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC;QACjC,KAAK,QAAQ;YACX,OAAO,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC;QACnC,KAAK,UAAU;YACb,OAAO,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACrC;YACE,OAAO,IAAI,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACrC,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,5 +17,6 @@ export declare function compress(input: PdfInput, options: CompressOptions & {
|
|
|
17
17
|
}): Promise<void>;
|
|
18
18
|
export declare function compress(input: PdfInput, options?: CompressOptions): Promise<Buffer>;
|
|
19
19
|
export { concurrency } from './concurrency.js';
|
|
20
|
+
export { QpdfError, QpdfFileError, QpdfFormatError, QpdfPasswordError } from './errors.js';
|
|
20
21
|
export type { CompressOptions } from './types.js';
|
|
21
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAe,MAAM,YAAY,CAAC;AAc/D,KAAK,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhC;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,QAAQ,EACf,OAAO,EAAE,eAAe,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,IAAI,CAAC,CAAC;AACjB,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AA2BtF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC3F,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,15 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { withConcurrency } from './concurrency.js';
|
|
5
|
+
import { parseNativeError } from './errors.js';
|
|
5
6
|
const require = createRequire(import.meta.url);
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const addonDir = resolve(__dirname, '..', 'build', 'Release');
|
|
8
9
|
if (process.platform === 'win32') {
|
|
9
10
|
process.env.PATH = `${addonDir};${process.env.PATH ?? ''}`;
|
|
10
11
|
}
|
|
12
|
+
// The native addon is loaded through createRequire, so its export is `any` at
|
|
13
|
+
// the module boundary; assert it to the typed surface once, here.
|
|
11
14
|
const addon = require('../build/Release/qpdf_compress.node');
|
|
12
15
|
export async function compress(input, options) {
|
|
13
16
|
if (Buffer.isBuffer(input)) {
|
|
@@ -24,11 +27,17 @@ export async function compress(input, options) {
|
|
|
24
27
|
throw new TypeError('Input must be a Buffer or file path string');
|
|
25
28
|
}
|
|
26
29
|
const stripMetadata = options?.stripMetadata ?? true;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
try {
|
|
31
|
+
return await withConcurrency(() => addon.compress(input, {
|
|
32
|
+
...(options?.lossy ? { lossy: true } : {}),
|
|
33
|
+
...(stripMetadata ? { stripMetadata: true } : {}),
|
|
34
|
+
...(options?.output ? { output: options.output } : {}),
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
throw parseNativeError(err);
|
|
39
|
+
}
|
|
32
40
|
}
|
|
33
41
|
export { concurrency } from './concurrency.js';
|
|
42
|
+
export { QpdfError, QpdfFileError, QpdfFormatError, QpdfPasswordError } from './errors.js';
|
|
34
43
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;AAC9D,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;AAC7D,CAAC;AAED,8EAA8E;AAC9E,kEAAkE;AAClE,MAAM,KAAK,GAAG,OAAO,CAAC,qCAAqC,CAAgB,CAAC;AAqB5E,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,KAAe,EAAE,OAAyB;IACvE,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC,8BAA8B,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC,4BAA4B,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,SAAS,CAAC,4CAA4C,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,aAAa,GAAG,OAAO,EAAE,aAAa,IAAI,IAAI,CAAC;IACrD,IAAI,CAAC;QACH,OAAO,MAAM,eAAe,CAAC,GAAG,EAAE,CAChC,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE;YACpB,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1C,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvD,CAAC,CACH,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC;AAED,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
package/lib/errors.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Base error for all qpdf-compress operations. */
|
|
2
|
+
export class QpdfError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
constructor(code: string, message: string) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'QpdfError';
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Thrown when an input or output file cannot be read or written. */
|
|
12
|
+
export class QpdfFileError extends QpdfError {
|
|
13
|
+
constructor(message: string) {
|
|
14
|
+
super('FILE', message);
|
|
15
|
+
this.name = 'QpdfFileError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Thrown when the input is not a valid PDF or is too damaged to recover. */
|
|
20
|
+
export class QpdfFormatError extends QpdfError {
|
|
21
|
+
constructor(message: string) {
|
|
22
|
+
super('FORMAT', message);
|
|
23
|
+
this.name = 'QpdfFormatError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Thrown when the PDF is encrypted and cannot be opened without a password. */
|
|
28
|
+
export class QpdfPasswordError extends QpdfError {
|
|
29
|
+
constructor(message: string) {
|
|
30
|
+
super('PASSWORD', message);
|
|
31
|
+
this.name = 'QpdfPasswordError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Decode the native "CODE:message" convention into a typed error. */
|
|
36
|
+
export function parseNativeError(err: unknown): QpdfError {
|
|
37
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
38
|
+
const colonIdx = msg.indexOf(':');
|
|
39
|
+
if (colonIdx === -1) return new QpdfError('UNKNOWN', msg);
|
|
40
|
+
|
|
41
|
+
const code = msg.slice(0, colonIdx);
|
|
42
|
+
const text = msg.slice(colonIdx + 1);
|
|
43
|
+
|
|
44
|
+
switch (code) {
|
|
45
|
+
case 'FILE':
|
|
46
|
+
return new QpdfFileError(text);
|
|
47
|
+
case 'FORMAT':
|
|
48
|
+
return new QpdfFormatError(text);
|
|
49
|
+
case 'PASSWORD':
|
|
50
|
+
return new QpdfPasswordError(text);
|
|
51
|
+
default:
|
|
52
|
+
return new QpdfError(code, text);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/lib/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { withConcurrency } from './concurrency.js';
|
|
5
|
+
import { parseNativeError } from './errors.js';
|
|
5
6
|
import type { CompressOptions, NativeAddon } from './types.js';
|
|
6
7
|
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
@@ -12,7 +13,9 @@ if (process.platform === 'win32') {
|
|
|
12
13
|
process.env.PATH = `${addonDir};${process.env.PATH ?? ''}`;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
// The native addon is loaded through createRequire, so its export is `any` at
|
|
17
|
+
// the module boundary; assert it to the typed surface once, here.
|
|
18
|
+
const addon = require('../build/Release/qpdf_compress.node') as NativeAddon;
|
|
16
19
|
|
|
17
20
|
type PdfInput = Buffer | string;
|
|
18
21
|
|
|
@@ -46,14 +49,19 @@ export async function compress(input: PdfInput, options?: CompressOptions): Prom
|
|
|
46
49
|
throw new TypeError('Input must be a Buffer or file path string');
|
|
47
50
|
}
|
|
48
51
|
const stripMetadata = options?.stripMetadata ?? true;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
try {
|
|
53
|
+
return await withConcurrency(() =>
|
|
54
|
+
addon.compress(input, {
|
|
55
|
+
...(options?.lossy ? { lossy: true } : {}),
|
|
56
|
+
...(stripMetadata ? { stripMetadata: true } : {}),
|
|
57
|
+
...(options?.output ? { output: options.output } : {}),
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
throw parseNativeError(err);
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
export { concurrency } from './concurrency.js';
|
|
66
|
+
export { QpdfError, QpdfFileError, QpdfFormatError, QpdfPasswordError } from './errors.js';
|
|
59
67
|
export type { CompressOptions } from './types.js';
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qpdf-compress",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Native PDF compression for Node.js, powered by QPDF",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"packageManager": "npm@11.17.0",
|
|
7
8
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
9
|
+
"node": ">=22.0.0"
|
|
9
10
|
},
|
|
10
11
|
"keywords": [
|
|
11
12
|
"pdf",
|
|
@@ -31,7 +32,8 @@
|
|
|
31
32
|
".": {
|
|
32
33
|
"types": "./dist/index.d.ts",
|
|
33
34
|
"default": "./dist/index.js"
|
|
34
|
-
}
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
35
37
|
},
|
|
36
38
|
"files": [
|
|
37
39
|
"dist",
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
"binding.gyp",
|
|
42
44
|
"README.md",
|
|
43
45
|
"CHANGELOG.md",
|
|
46
|
+
"THIRD-PARTY-NOTICES.md",
|
|
44
47
|
"LICENSE"
|
|
45
48
|
],
|
|
46
49
|
"repository": {
|
|
@@ -73,7 +76,10 @@
|
|
|
73
76
|
"download:mozjpeg": "node scripts/download-mozjpeg.mjs",
|
|
74
77
|
"download:qpdf": "node scripts/download-qpdf.mjs",
|
|
75
78
|
"download:harfbuzz": "node scripts/download-harfbuzz.mjs",
|
|
79
|
+
"verify:checksums": "node scripts/verify-checksums.mjs",
|
|
76
80
|
"test": "vitest run",
|
|
81
|
+
"test:coverage": "vitest run --coverage",
|
|
82
|
+
"typecheck": "tsc --noEmit",
|
|
77
83
|
"lint": "eslint --fix .",
|
|
78
84
|
"lint:check": "eslint .",
|
|
79
85
|
"format": "prettier --write .",
|
|
@@ -81,8 +87,8 @@
|
|
|
81
87
|
"prepare": "husky && tsc"
|
|
82
88
|
},
|
|
83
89
|
"lint-staged": {
|
|
84
|
-
"*.{ts,js,json,md,yaml,yml}": "prettier --write",
|
|
85
|
-
"*.{ts,js}": "eslint --max-warnings 0"
|
|
90
|
+
"*.{ts,js,mjs,cjs,json,md,yaml,yml}": "prettier --write",
|
|
91
|
+
"*.{ts,js,mjs,cjs}": "eslint --max-warnings 0"
|
|
86
92
|
},
|
|
87
93
|
"dependencies": {
|
|
88
94
|
"node-addon-api": "^8.0.0",
|
|
@@ -91,15 +97,16 @@
|
|
|
91
97
|
"devDependencies": {
|
|
92
98
|
"@eslint/js": "^10.0.1",
|
|
93
99
|
"@types/node": "^25.5.0",
|
|
100
|
+
"@vitest/coverage-v8": "^4.1.4",
|
|
94
101
|
"eslint": "^10.1.0",
|
|
95
102
|
"eslint-config-prettier": "^10.1.8",
|
|
96
|
-
"
|
|
103
|
+
"globals": "^17.0.0",
|
|
97
104
|
"husky": "^9.1.7",
|
|
98
105
|
"lint-staged": "^16.4.0",
|
|
99
|
-
"node-gyp": "^
|
|
106
|
+
"node-gyp": "^13.0.0",
|
|
100
107
|
"prettier": "^3.8.1",
|
|
101
108
|
"typescript": "^5.9.3",
|
|
102
109
|
"typescript-eslint": "^8.57.2",
|
|
103
|
-
"vitest": "^4.1.
|
|
110
|
+
"vitest": "^4.1.4"
|
|
104
111
|
}
|
|
105
112
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import { pipeline } from 'node:stream/promises';
|
|
4
|
-
import { Readable } from 'node:stream';
|
|
2
|
+
import { mkdirSync, existsSync, rmSync, cpSync, readdirSync } from 'node:fs';
|
|
5
3
|
import { join } from 'node:path';
|
|
4
|
+
import { downloadFile, extractTarball, loadManifest } from './lib/integrity.mjs';
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
6
|
+
const { harfbuzz: dep } = loadManifest();
|
|
7
|
+
const HARFBUZZ_VERSION = dep.version;
|
|
9
8
|
|
|
10
9
|
const root = join(import.meta.dirname, '..');
|
|
11
10
|
const depsDir = join(root, 'deps', 'harfbuzz');
|
|
@@ -21,27 +20,19 @@ if (!/^\d+\.\d+\.\d+$/.test(HARFBUZZ_VERSION)) {
|
|
|
21
20
|
process.exit(1);
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
const url = `${BASE_URL}/${HARFBUZZ_VERSION}.tar.gz`;
|
|
25
23
|
const tarball = join(root, `harfbuzz-${HARFBUZZ_VERSION}.tar.gz`);
|
|
26
24
|
const srcDir = join(root, `harfbuzz-${HARFBUZZ_VERSION}`);
|
|
27
25
|
const buildDir = join(root, 'build-harfbuzz');
|
|
28
26
|
|
|
29
|
-
// step 1: download
|
|
27
|
+
// step 1: download (origin- and SHA-256-pinned via native-deps.json)
|
|
30
28
|
console.log(`Downloading HarfBuzz ${HARFBUZZ_VERSION}...`);
|
|
31
|
-
console.log(`URL: ${url}`);
|
|
32
|
-
|
|
33
|
-
const response = await fetch(url, { redirect: 'follow' });
|
|
34
|
-
if (!response.ok) {
|
|
35
|
-
console.error(`Download failed: ${response.status} ${response.statusText}`);
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
29
|
+
console.log(`URL: ${dep.url}`);
|
|
38
30
|
|
|
39
31
|
mkdirSync(join(root, 'deps'), { recursive: true });
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
rmSync(tarball);
|
|
32
|
+
await downloadFile(dep.url, tarball, { expectedSha256: dep.sha256, allowedOrigin: dep.origin });
|
|
33
|
+
console.log('Checksum verified. Extracting...');
|
|
34
|
+
extractTarball(tarball, root);
|
|
35
|
+
rmSync(tarball, { force: true });
|
|
45
36
|
|
|
46
37
|
// GitHub archive tarballs extract to `harfbuzz-{tag}` — find it
|
|
47
38
|
if (!existsSync(srcDir)) {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import { pipeline } from 'node:stream/promises';
|
|
4
|
-
import { Readable } from 'node:stream';
|
|
2
|
+
import { mkdirSync, existsSync, rmSync, readdirSync } from 'node:fs';
|
|
5
3
|
import { join } from 'node:path';
|
|
4
|
+
import { downloadFile, extractTarball, loadManifest } from './lib/integrity.mjs';
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
6
|
+
const { mozjpeg: dep } = loadManifest();
|
|
7
|
+
const MOZJPEG_VERSION = dep.version;
|
|
9
8
|
|
|
10
9
|
const root = join(import.meta.dirname, '..');
|
|
11
10
|
const depsDir = join(root, 'deps', 'mozjpeg');
|
|
@@ -21,27 +20,19 @@ if (!/^\d+\.\d+\.\d+$/.test(MOZJPEG_VERSION)) {
|
|
|
21
20
|
process.exit(1);
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
const url = `${BASE_URL}/v${MOZJPEG_VERSION}.tar.gz`;
|
|
25
23
|
const tarball = join(root, `mozjpeg-${MOZJPEG_VERSION}.tar.gz`);
|
|
26
24
|
const srcDir = join(root, `mozjpeg-${MOZJPEG_VERSION}`);
|
|
27
25
|
const buildDir = join(root, 'build-mozjpeg');
|
|
28
26
|
|
|
29
|
-
// step 1: download
|
|
27
|
+
// step 1: download (origin- and SHA-256-pinned via native-deps.json)
|
|
30
28
|
console.log(`Downloading mozjpeg ${MOZJPEG_VERSION}...`);
|
|
31
|
-
console.log(`URL: ${url}`);
|
|
32
|
-
|
|
33
|
-
const response = await fetch(url, { redirect: 'follow' });
|
|
34
|
-
if (!response.ok) {
|
|
35
|
-
console.error(`Download failed: ${response.status} ${response.statusText}`);
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
29
|
+
console.log(`URL: ${dep.url}`);
|
|
38
30
|
|
|
39
31
|
mkdirSync(join(root, 'deps'), { recursive: true });
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
rmSync(tarball);
|
|
32
|
+
await downloadFile(dep.url, tarball, { expectedSha256: dep.sha256, allowedOrigin: dep.origin });
|
|
33
|
+
console.log('Checksum verified. Extracting...');
|
|
34
|
+
extractTarball(tarball, root);
|
|
35
|
+
rmSync(tarball, { force: true });
|
|
45
36
|
|
|
46
37
|
// GitHub archive tarballs extract to `mozjpeg-{tag}` — find the directory
|
|
47
38
|
if (!existsSync(srcDir)) {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import { pipeline } from 'node:stream/promises';
|
|
4
|
-
import { Readable } from 'node:stream';
|
|
2
|
+
import { mkdirSync, existsSync, rmSync, cpSync, readdirSync } from 'node:fs';
|
|
5
3
|
import { join } from 'node:path';
|
|
4
|
+
import { downloadFile, extractTarball, loadManifest } from './lib/integrity.mjs';
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
6
|
+
const { qpdf: dep } = loadManifest();
|
|
7
|
+
const QPDF_VERSION = dep.version;
|
|
9
8
|
|
|
10
9
|
const root = join(import.meta.dirname, '..');
|
|
11
10
|
const depsDir = join(root, 'deps', 'qpdf');
|
|
@@ -21,27 +20,19 @@ if (!/^\d+\.\d+\.\d+$/.test(QPDF_VERSION)) {
|
|
|
21
20
|
process.exit(1);
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
const url = `${BASE_URL}/v${QPDF_VERSION}.tar.gz`;
|
|
25
23
|
const tarball = join(root, `qpdf-${QPDF_VERSION}.tar.gz`);
|
|
26
24
|
const srcDir = join(root, `qpdf-${QPDF_VERSION}`);
|
|
27
25
|
const buildDir = join(root, 'build-qpdf');
|
|
28
26
|
|
|
29
|
-
// step 1: download
|
|
27
|
+
// step 1: download (origin- and SHA-256-pinned via native-deps.json)
|
|
30
28
|
console.log(`Downloading QPDF ${QPDF_VERSION}...`);
|
|
31
|
-
console.log(`URL: ${url}`);
|
|
32
|
-
|
|
33
|
-
const response = await fetch(url, { redirect: 'follow' });
|
|
34
|
-
if (!response.ok) {
|
|
35
|
-
console.error(`Download failed: ${response.status} ${response.statusText}`);
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
29
|
+
console.log(`URL: ${dep.url}`);
|
|
38
30
|
|
|
39
31
|
mkdirSync(join(root, 'deps'), { recursive: true });
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
rmSync(tarball);
|
|
32
|
+
await downloadFile(dep.url, tarball, { expectedSha256: dep.sha256, allowedOrigin: dep.origin });
|
|
33
|
+
console.log('Checksum verified. Extracting...');
|
|
34
|
+
extractTarball(tarball, root);
|
|
35
|
+
rmSync(tarball, { force: true });
|
|
45
36
|
|
|
46
37
|
// GitHub archive tarballs extract to `qpdf-{tag}` (without 'v' prefix)
|
|
47
38
|
// but may also be `qpdf-qpdf-{ver}` depending on repo naming — find it
|
|
@@ -132,6 +123,19 @@ if (process.platform === 'win32') {
|
|
|
132
123
|
`-DVCPKG_TARGET_TRIPLET=${triplet}`,
|
|
133
124
|
);
|
|
134
125
|
|
|
126
|
+
// qpdf locates zlib via pkg-config first, then falls back to find_path/
|
|
127
|
+
// find_library. The GitHub Windows image no longer ships pkg-config and the
|
|
128
|
+
// fallback misses the vcpkg tree, so point qpdf's ZLIB_*_PATH cache vars
|
|
129
|
+
// straight at the vcpkg-installed static zlib (mirrors LIBJPEG_*_PATH).
|
|
130
|
+
const vcpkgInstalled = join(vcpkgRoot, 'installed', triplet);
|
|
131
|
+
const zlibLib = ['zlib.lib', 'zs.lib', 'zlibstatic.lib']
|
|
132
|
+
.map((n) => join(vcpkgInstalled, 'lib', n))
|
|
133
|
+
.find((p) => existsSync(p));
|
|
134
|
+
cmakeArgs.push(`-DZLIB_H_PATH:PATH=${join(vcpkgInstalled, 'include')}`);
|
|
135
|
+
if (zlibLib) {
|
|
136
|
+
cmakeArgs.push(`-DZLIB_LIB_PATH:FILEPATH=${zlibLib}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
135
139
|
// cross-compile for ARM64 when triplet indicates it
|
|
136
140
|
if (triplet.startsWith('arm64')) {
|
|
137
141
|
cmakeArgs.push('-A', 'arm64');
|
|
@@ -200,13 +204,23 @@ if (process.platform === 'win32') {
|
|
|
200
204
|
];
|
|
201
205
|
|
|
202
206
|
const vcpkgLibDir = candidateLibDirs.find((d) => existsSync(d));
|
|
203
|
-
if (vcpkgLibDir) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
if (!vcpkgLibDir) {
|
|
208
|
+
console.error('vcpkg lib dir not found; checked:', candidateLibDirs);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
// vcpkg's static zlib lib name varies across versions (zlib.lib / zs.lib /
|
|
212
|
+
// zlibstatic.lib); copy whichever exists to the zlib.lib name binding.gyp links.
|
|
213
|
+
const zlibNames = ['zlib.lib', 'zs.lib', 'zlibstatic.lib'];
|
|
214
|
+
const zlibSrc = zlibNames.map((n) => join(vcpkgLibDir, n)).find((p) => existsSync(p));
|
|
215
|
+
if (!zlibSrc) {
|
|
216
|
+
console.error(
|
|
217
|
+
`zlib static lib not found in ${vcpkgLibDir}. Contents:`,
|
|
218
|
+
readdirSync(vcpkgLibDir),
|
|
219
|
+
);
|
|
220
|
+
process.exit(1);
|
|
209
221
|
}
|
|
222
|
+
cpSync(zlibSrc, join(depsDir, 'lib', 'zlib.lib'));
|
|
223
|
+
console.log(`Copied ${zlibSrc} -> deps/qpdf/lib/zlib.lib`);
|
|
210
224
|
}
|
|
211
225
|
|
|
212
226
|
// step 5: clean up source and build dirs
|
package/scripts/install.mjs
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* Musl (Alpine) naming: qpdf-compress-v{version}-linux-musl-{arch}.tar.gz
|
|
7
7
|
* Contents: build/Release/qpdf_compress.node
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
10
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, unlinkSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { Readable } from 'node:stream';
|
|
13
13
|
import { pipeline } from 'node:stream/promises';
|
|
14
|
+
import { extractTarball } from './lib/integrity.mjs';
|
|
14
15
|
|
|
15
16
|
const root = join(import.meta.dirname, '..');
|
|
16
17
|
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
@@ -57,7 +58,7 @@ async function tryDownload() {
|
|
|
57
58
|
if (releaseUrl.origin !== releaseOrigin) {
|
|
58
59
|
throw new Error(`Unexpected URL origin: ${releaseUrl.origin}`);
|
|
59
60
|
}
|
|
60
|
-
const res = await fetch(releaseUrl, { redirect: 'follow' });
|
|
61
|
+
const res = await fetch(releaseUrl, { redirect: 'follow', signal: AbortSignal.timeout(60000) });
|
|
61
62
|
if (!res.ok) {
|
|
62
63
|
console.log(`No prebuilt binary found (HTTP ${res.status}), will compile from source.`);
|
|
63
64
|
return false;
|
|
@@ -73,8 +74,8 @@ async function tryDownload() {
|
|
|
73
74
|
|
|
74
75
|
await pipeline(Readable.fromWeb(body), fileStream);
|
|
75
76
|
|
|
76
|
-
// extract tar.gz into project root
|
|
77
|
-
|
|
77
|
+
// extract tar.gz into project root (hardened: no -P, no archived ownership)
|
|
78
|
+
extractTarball(tmpTar, root);
|
|
78
79
|
unlinkSync(tmpTar);
|
|
79
80
|
|
|
80
81
|
// verify the .node file exists
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared download/verify helpers for the install and dependency-build scripts.
|
|
3
|
+
*
|
|
4
|
+
* Goals:
|
|
5
|
+
* - never trust the network blindly: verify a pinned SHA-256 before use,
|
|
6
|
+
* - survive flaky connections: timeout + exponential-backoff retry,
|
|
7
|
+
* - never leave a half-written file behind: download to `<dest>.part` and
|
|
8
|
+
* rename atomically only after the checksum passes,
|
|
9
|
+
* - extract archives without restoring archived ownership or honouring
|
|
10
|
+
* absolute/`..` paths (tar's default, kept by NOT passing -P).
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import {
|
|
15
|
+
createReadStream,
|
|
16
|
+
createWriteStream,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
rmSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
} from 'node:fs';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { Readable } from 'node:stream';
|
|
24
|
+
import { pipeline } from 'node:stream/promises';
|
|
25
|
+
|
|
26
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
+
|
|
28
|
+
/** Compute the SHA-256 of a file as a lowercase hex string. */
|
|
29
|
+
export async function sha256File(path) {
|
|
30
|
+
const hash = createHash('sha256');
|
|
31
|
+
await pipeline(createReadStream(path), hash);
|
|
32
|
+
return hash.digest('hex');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Read the native-dependency manifest (scripts/native-deps.json). */
|
|
36
|
+
export function loadManifest() {
|
|
37
|
+
return JSON.parse(readFileSync(join(import.meta.dirname, '..', 'native-deps.json'), 'utf8'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Download `url` to `dest` with a per-attempt timeout and exponential-backoff
|
|
42
|
+
* retry. Writes to `<dest>.part` (same filesystem, so the rename is atomic) and
|
|
43
|
+
* only promotes it once the optional `expectedSha256` matches. A checksum
|
|
44
|
+
* mismatch or unexpected origin fails immediately (not retried).
|
|
45
|
+
*
|
|
46
|
+
* @param {string} url
|
|
47
|
+
* @param {string} dest
|
|
48
|
+
* @param {{ expectedSha256?: string, allowedOrigin?: string, timeoutMs?: number, retries?: number }} [opts]
|
|
49
|
+
*/
|
|
50
|
+
export async function downloadFile(url, dest, opts = {}) {
|
|
51
|
+
const { expectedSha256, allowedOrigin, timeoutMs = 60000, retries = 4 } = opts;
|
|
52
|
+
|
|
53
|
+
const u = new URL(url);
|
|
54
|
+
if (allowedOrigin && u.origin !== allowedOrigin) {
|
|
55
|
+
throw new Error(`Refusing to download from unexpected origin: ${u.origin}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
59
|
+
const tmp = `${dest}.part`;
|
|
60
|
+
|
|
61
|
+
let lastErr;
|
|
62
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(u, { redirect: 'follow', signal: AbortSignal.timeout(timeoutMs) });
|
|
65
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
66
|
+
if (!res.body) throw new Error('empty response body');
|
|
67
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(tmp));
|
|
68
|
+
|
|
69
|
+
if (expectedSha256) {
|
|
70
|
+
const actual = await sha256File(tmp);
|
|
71
|
+
if (actual !== expectedSha256.toLowerCase()) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`checksum mismatch for ${u.href}\n expected ${expectedSha256}\n actual ${actual}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
renameSync(tmp, dest);
|
|
79
|
+
return dest;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
lastErr = err;
|
|
82
|
+
rmSync(tmp, { force: true });
|
|
83
|
+
const msg = String(err?.message ?? err);
|
|
84
|
+
// integrity failures are not transient — surface them immediately
|
|
85
|
+
if (msg.includes('checksum mismatch') || msg.includes('unexpected origin')) throw err;
|
|
86
|
+
if (attempt < retries) {
|
|
87
|
+
const backoff = Math.min(1000 * 2 ** (attempt - 1), 8000);
|
|
88
|
+
console.warn(` attempt ${attempt}/${retries} failed (${msg}); retrying in ${backoff}ms`);
|
|
89
|
+
await sleep(backoff);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Failed to download ${u.href} after ${retries} attempts: ${lastErr?.message}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extract a .tar.gz / .tar.xz into `destDir`. Compression is auto-detected.
|
|
98
|
+
* Not passing `-P` keeps tar's default refusal of absolute and `..` paths;
|
|
99
|
+
* `--no-same-owner` avoids restoring archived uid/gid.
|
|
100
|
+
*
|
|
101
|
+
* stdin is redirected from /dev/null: tar never reads it for `-xf`, and on
|
|
102
|
+
* Windows an inherited console stdin can deadlock a spawned decompressor.
|
|
103
|
+
*/
|
|
104
|
+
export function extractTarball(tarball, destDir, { strip } = {}) {
|
|
105
|
+
mkdirSync(destDir, { recursive: true });
|
|
106
|
+
const args = ['-xf', tarball, '-C', destDir, '--no-same-owner'];
|
|
107
|
+
if (strip) args.push('--strip-components', String(strip));
|
|
108
|
+
execFileSync('tar', args, { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
109
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"qpdf": {
|
|
3
|
+
"version": "12.3.2",
|
|
4
|
+
"origin": "https://github.com",
|
|
5
|
+
"url": "https://github.com/qpdf/qpdf/releases/download/v12.3.2/qpdf-12.3.2.tar.gz",
|
|
6
|
+
"sha256": "6cba2f9f2cd887d905faeb99e0e51a307b217920d1bbf3e9cfbb2e8178a2deda",
|
|
7
|
+
"checksumSource": "official PGP-signed release manifest (qpdf-12.3.2.sha256)"
|
|
8
|
+
},
|
|
9
|
+
"harfbuzz": {
|
|
10
|
+
"version": "14.1.0",
|
|
11
|
+
"origin": "https://github.com",
|
|
12
|
+
"url": "https://github.com/harfbuzz/harfbuzz/archive/refs/tags/14.1.0.tar.gz",
|
|
13
|
+
"sha256": "9deffe9dded3c367c7d08265fd0a64193fad505a362a4889d898568c1cd46812",
|
|
14
|
+
"checksumSource": "self-computed from the GitHub git-archive tarball; HarfBuzz's only release asset is .tar.xz, whose extraction hangs Windows bsdtar — the gzip git-archive builds identically"
|
|
15
|
+
},
|
|
16
|
+
"mozjpeg": {
|
|
17
|
+
"version": "4.1.1",
|
|
18
|
+
"origin": "https://github.com",
|
|
19
|
+
"url": "https://github.com/mozilla/mozjpeg/archive/refs/tags/v4.1.1.tar.gz",
|
|
20
|
+
"sha256": "66b1b8d6b55d263f35f27f55acaaa3234df2a401232de99b6d099e2bb0a9d196",
|
|
21
|
+
"checksumSource": "self-computed from the GitHub archive tarball (mozilla/mozjpeg publishes no release asset or checksum)"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify that the SHA-256 hashes pinned in native-deps.json still match what
|
|
3
|
+
* the upstream URLs serve. Use after a version bump, or in CI as a supply-chain
|
|
4
|
+
* tripwire. By default only this platform's artifacts are checked; pass --all
|
|
5
|
+
* to verify every pinned target (large download for prebuilt-binary deps).
|
|
6
|
+
*/
|
|
7
|
+
import { rmSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { downloadFile, loadManifest } from './lib/integrity.mjs';
|
|
11
|
+
|
|
12
|
+
const all = process.argv.includes('--all');
|
|
13
|
+
const manifest = loadManifest();
|
|
14
|
+
const platform = process.platform;
|
|
15
|
+
const arch = process.arch;
|
|
16
|
+
|
|
17
|
+
const checks = [];
|
|
18
|
+
for (const [name, dep] of Object.entries(manifest)) {
|
|
19
|
+
if (dep.targets) {
|
|
20
|
+
for (const [key, t] of Object.entries(dep.targets)) {
|
|
21
|
+
if (!all && key !== `${platform}-${arch}` && key !== `${platform}-musl-${arch}`) continue;
|
|
22
|
+
checks.push({
|
|
23
|
+
label: `${name} ${key}`,
|
|
24
|
+
url: `${dep.baseUrl}/${encodeURIComponent(dep.version)}/${t.asset}`,
|
|
25
|
+
sha256: t.sha256,
|
|
26
|
+
origin: dep.origin,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
checks.push({
|
|
31
|
+
label: `${name} ${dep.version}`,
|
|
32
|
+
url: dep.url,
|
|
33
|
+
sha256: dep.sha256,
|
|
34
|
+
origin: dep.origin,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let failed = 0;
|
|
40
|
+
for (const c of checks) {
|
|
41
|
+
const tmp = join(tmpdir(), `verify-${c.label.replace(/\W+/g, '-')}`);
|
|
42
|
+
try {
|
|
43
|
+
process.stdout.write(`Verifying ${c.label} ... `);
|
|
44
|
+
await downloadFile(c.url, tmp, { expectedSha256: c.sha256, allowedOrigin: c.origin });
|
|
45
|
+
console.log('OK');
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.log(`FAIL\n ${err.message}`);
|
|
48
|
+
failed++;
|
|
49
|
+
} finally {
|
|
50
|
+
rmSync(tmp, { force: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (failed) {
|
|
55
|
+
console.error(`\n${failed} checksum(s) did not match.`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
console.log(`\nAll ${checks.length} checksum(s) verified.`);
|
package/src/qpdf_addon.cc
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#include <napi.h>
|
|
2
2
|
|
|
3
|
+
#include <algorithm>
|
|
3
4
|
#include <atomic>
|
|
5
|
+
#include <cctype>
|
|
4
6
|
#include <cerrno>
|
|
5
7
|
#include <cstdio>
|
|
6
8
|
#include <cstdlib>
|
|
@@ -52,6 +54,28 @@ static std::string writeToFile(const std::string &path, const uint8_t *data,
|
|
|
52
54
|
return {};
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Error classification — prefix a native error message with a stable CODE so
|
|
59
|
+
// the JS layer (parseNativeError) can map it to a typed QpdfError subclass.
|
|
60
|
+
// Mirrors the "CODE:message" convention used by pdfium-native.
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
static std::string classifyError(const std::string &what) {
|
|
64
|
+
std::string lower = what;
|
|
65
|
+
std::transform(lower.begin(), lower.end(), lower.begin(),
|
|
66
|
+
[](unsigned char c) { return std::tolower(c); });
|
|
67
|
+
auto has = [&](const char *needle) {
|
|
68
|
+
return lower.find(needle) != std::string::npos;
|
|
69
|
+
};
|
|
70
|
+
if (has("password") || has("encrypted"))
|
|
71
|
+
return "PASSWORD:" + what;
|
|
72
|
+
if (has("damaged") || has("not a pdf") || has("not well-formed") ||
|
|
73
|
+
has("invalid pdf") || has("corrupt") || has("xref") ||
|
|
74
|
+
has("unable to find") || has("/root"))
|
|
75
|
+
return "FORMAT:" + what;
|
|
76
|
+
return "COMPRESS:" + what;
|
|
77
|
+
}
|
|
78
|
+
|
|
55
79
|
// ---------------------------------------------------------------------------
|
|
56
80
|
// Per-environment alive flag for worker thread safety.
|
|
57
81
|
// When a Node.js worker thread is terminated, the V8 isolate tears down but
|
|
@@ -125,7 +149,7 @@ protected:
|
|
|
125
149
|
|
|
126
150
|
if (useFile_) {
|
|
127
151
|
if (access(filePath_.c_str(), F_OK) != 0) {
|
|
128
|
-
SetError("Input file not found: " + filePath_);
|
|
152
|
+
SetError("FILE:Input file not found: " + filePath_);
|
|
129
153
|
return;
|
|
130
154
|
}
|
|
131
155
|
qpdf->processFile(filePath_.c_str());
|
|
@@ -192,7 +216,7 @@ protected:
|
|
|
192
216
|
auto err = writeToFile(outputPath_, writerBuf_->getBuffer(),
|
|
193
217
|
writerBuf_->getSize());
|
|
194
218
|
if (!err.empty()) {
|
|
195
|
-
SetError(err);
|
|
219
|
+
SetError("FILE:" + err);
|
|
196
220
|
return;
|
|
197
221
|
}
|
|
198
222
|
writerBuf_.reset();
|
|
@@ -204,7 +228,7 @@ protected:
|
|
|
204
228
|
qpdf.reset();
|
|
205
229
|
} catch (...) {
|
|
206
230
|
}
|
|
207
|
-
SetError(e.what());
|
|
231
|
+
SetError(classifyError(e.what()));
|
|
208
232
|
} catch (...) {
|
|
209
233
|
// on macOS/libc++, catch(std::exception&) may fail to match QPDFExc
|
|
210
234
|
// due to typeinfo visibility mismatch between the static library and
|
|
@@ -213,7 +237,7 @@ protected:
|
|
|
213
237
|
qpdf.reset();
|
|
214
238
|
} catch (...) {
|
|
215
239
|
}
|
|
216
|
-
SetError("PDF compression failed");
|
|
240
|
+
SetError("UNKNOWN:PDF compression failed");
|
|
217
241
|
}
|
|
218
242
|
}
|
|
219
243
|
|
|
@@ -306,12 +330,11 @@ static Napi::Value Compress(const Napi::CallbackInfo &info) {
|
|
|
306
330
|
|
|
307
331
|
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
308
332
|
auto *addonData = new AddonData();
|
|
333
|
+
// SetInstanceData installs a finalizer that deletes addonData on env teardown,
|
|
334
|
+
// so the cleanup hook must NOT delete it again (double free); it only flips the
|
|
335
|
+
// alive flag so in-flight workers bail out.
|
|
309
336
|
env.SetInstanceData(addonData);
|
|
310
|
-
|
|
311
|
-
env.AddCleanupHook([addonData]() {
|
|
312
|
-
addonData->envAlive->store(false);
|
|
313
|
-
delete addonData;
|
|
314
|
-
});
|
|
337
|
+
env.AddCleanupHook([addonData]() { addonData->envAlive->store(false); });
|
|
315
338
|
|
|
316
339
|
exports.Set("compress", Napi::Function::New(env, Compress));
|
|
317
340
|
return exports;
|