qpdf-compress 0.7.1 → 0.8.1
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 +21 -0
- package/README.md +19 -11
- package/THIRD-PARTY-NOTICES.md +12 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/lib/index.ts +7 -0
- package/lib/types.ts +3 -0
- package/package.json +2 -1
- package/scripts/pdfa-baseline.json +23 -0
- package/scripts/prebuilds.json +9 -9
- package/scripts/verify-pdfa.mjs +136 -0
- package/src/qpdf_addon.cc +14 -8
- package/src/strip.cc +57 -11
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.1] - 2026-09-23
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **The release workflow published releases without their prebuilt binaries.** `gh release create <tag> <assets>` publishes the release and then uploads, and GitHub's immutable releases reject an upload to a published release — with a 422 that `gh` does not surface, so the step went green with nothing attached. `install.mjs` reads the prebuilts off the release, so every consumer of such a version is sent to a source build. The release is now created as a draft with the tarballs attached and published afterwards, and a following step fails the job unless every artifact made it onto the release. **0.8.0 is affected and should be skipped** — it installs only where a C++ toolchain is present. It is deprecated on npm in favour of this release.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `npm run verify:pdfa` — a PDF/A conformance harness. It validates `compress()` output against veraPDF, the reference validator, over a real ZUGFeRD PDF/A-3a invoice, and compares the failing clauses to a recorded baseline (`scripts/pdfa-baseline.json`), failing on any difference in either direction so that neither a regression nor an improvement passes unnoticed. CI runs it on Linux / Node 22. veraPDF is GPL-3.0 / MPL-2.0 and is run as an external container pinned by digest — never vendored or linked. Groundwork for [#35](https://github.com/xonaman/nodejs-qpdf-compress/issues/35).
|
|
18
|
+
|
|
19
|
+
## [0.8.0] - 2026-09-23
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Embedded file stripping removed the lookup path but not the file.** Only the `/EmbeddedFiles` name tree was cleared, while the `/AF` associated-files array (PDF/A-3, PDF 2.0) and `/FileAttachment` annotations kept their own reference to the same file specification. The attachment therefore stayed in the output and was recoverable in full, while readers that resolve attachments by name — pdfium among them — no longer found it: the removal did not remove, and a legitimate consumer lost access. All three paths are now cleared, so the writer drops the file specification and its stream as unreferenced. ([#34](https://github.com/xonaman/nodejs-qpdf-compress/issues/34))
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `stripAttachments` option (default `true`, matching previous behaviour) to keep embedded file attachments. Hybrid invoices (ZUGFeRD / Factur-X) carry the invoice itself as an attachment, so `compress(pdf, { stripAttachments: false })` is what keeps such a document machine-readable. Note that the output is still not a conforming PDF/A-3: output intents and structure information are dropped in every mode.
|
|
28
|
+
- A `with-attachment.pdf` fixture whose attachment is reachable through all three paths, and tests that search decompressed stream contents rather than raw bytes — with object streams on, a byte search can miss an attachment that is entirely intact.
|
|
29
|
+
|
|
9
30
|
## [0.7.1] - 2026-07-15
|
|
10
31
|
|
|
11
32
|
### Security
|
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ const smaller = await compress(pdfBuffer, { lossy: true });
|
|
|
71
71
|
| Form flattening | ✅ Automatic | ❌ | ❌ |
|
|
72
72
|
| Stream deduplication | ✅ Automatic | ❌ | ❌ |
|
|
73
73
|
| Content minification | ✅ Automatic | ❌ | ❌ |
|
|
74
|
-
| JS/embedded file removal | ✅
|
|
74
|
+
| JS/embedded file removal | ✅ Default on | ❌ | ❌ |
|
|
75
75
|
| Metadata stripping | ✅ Default on | ✅ Manual flag | ✅ |
|
|
76
76
|
| PDF repair | ✅ Automatic | ✅ Manual flag | ⚠️ Partial |
|
|
77
77
|
| License | Apache-2.0 | Apache-2.0 | AGPL-3.0 ⚠️ |
|
|
@@ -131,6 +131,9 @@ const smaller = await compress(pdfBuffer, { lossy: true });
|
|
|
131
131
|
// keep metadata (stripped by default)
|
|
132
132
|
const withMeta = await compress(pdfBuffer, { stripMetadata: false });
|
|
133
133
|
|
|
134
|
+
// keep embedded file attachments (stripped by default)
|
|
135
|
+
const withAttachments = await compress(pdfBuffer, { stripAttachments: false });
|
|
136
|
+
|
|
134
137
|
// file path input (avoids copying into memory twice)
|
|
135
138
|
const result = await compress('/path/to/file.pdf');
|
|
136
139
|
|
|
@@ -149,12 +152,15 @@ const fixed = await compress(damagedBuffer);
|
|
|
149
152
|
|
|
150
153
|
Compresses a PDF document. Automatically repairs damaged PDFs.
|
|
151
154
|
|
|
152
|
-
| Parameter
|
|
153
|
-
|
|
|
154
|
-
| `input`
|
|
155
|
-
| `options.lossy`
|
|
156
|
-
| `options.stripMetadata`
|
|
157
|
-
| `options.
|
|
155
|
+
| Parameter | Type | Description |
|
|
156
|
+
| -------------------------- | ------------------ | ------------------------------------------------------------------- |
|
|
157
|
+
| `input` | `Buffer \| string` | PDF data or file path |
|
|
158
|
+
| `options.lossy` | `boolean` | Enable lossy compression. Default: `false` |
|
|
159
|
+
| `options.stripMetadata` | `boolean` | Remove XMP metadata, document info, and thumbnails. Default: `true` |
|
|
160
|
+
| `options.stripAttachments` | `boolean` | Remove embedded file attachments. Default: `true` |
|
|
161
|
+
| `options.output` | `string` | Write to file path instead of returning a `Buffer` |
|
|
162
|
+
|
|
163
|
+
> **Hybrid invoices (ZUGFeRD / Factur-X)**: the invoice XML rides along as an attachment, so the defaults remove it. Pass `stripAttachments: false` to keep the attachment and every path readers look it up through. The result is still not a conforming PDF/A-3 — output intents and structure information are dropped in every mode, and XMP metadata too unless `stripMetadata: false` — so a file that has to stay conformant should not be compressed at all. That is measured, not assumed: `npm run verify:pdfa` validates the output against veraPDF over a real PDF/A-3a invoice, and `scripts/pdfa-baseline.json` records exactly which clauses fail.
|
|
158
164
|
|
|
159
165
|
**Both modes:**
|
|
160
166
|
|
|
@@ -170,7 +176,8 @@ Compresses a PDF document. Automatically repairs damaged PDFs.
|
|
|
170
176
|
- Flattens page tree (pushes inherited attributes to pages)
|
|
171
177
|
- Coalesces multiple content streams per page into one
|
|
172
178
|
- Minifies content streams (whitespace normalization, numeric formatting)
|
|
173
|
-
- Strips embedded
|
|
179
|
+
- Strips embedded file attachments and every path to them — the `/EmbeddedFiles` name tree, `/AF` associated-file arrays, and `/FileAttachment` annotations (default: on)
|
|
180
|
+
- Strips JavaScript actions
|
|
174
181
|
- Recompresses all decodable streams with Flate level 9
|
|
175
182
|
- Generates object streams for smaller metadata overhead
|
|
176
183
|
- Removes unreferenced objects
|
|
@@ -231,9 +238,10 @@ All operations run in a background thread via `Napi::AsyncWorker`, so the event
|
|
|
231
238
|
13. Coalesce multiple content streams per page
|
|
232
239
|
14. Minify content streams
|
|
233
240
|
15. Deduplicate identical non-image streams
|
|
234
|
-
16. Strip embedded
|
|
235
|
-
17.
|
|
236
|
-
18.
|
|
241
|
+
16. _(optional)_ Strip embedded file attachments
|
|
242
|
+
17. Strip JavaScript
|
|
243
|
+
18. _(optional)_ Strip metadata
|
|
244
|
+
19. QPDFWriter: Flate 9, object streams, unreferenced object removal
|
|
237
245
|
|
|
238
246
|
## License
|
|
239
247
|
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -42,3 +42,15 @@ Copyright Jean-loup Gailly and Mark Adler. Licensed under the zlib License.
|
|
|
42
42
|
zlib provides DEFLATE compression and is statically linked into the Windows
|
|
43
43
|
build (via vcpkg); on macOS and Linux the system zlib is used at build time.
|
|
44
44
|
See <https://github.com/madler/zlib/blob/master/LICENSE>.
|
|
45
|
+
|
|
46
|
+
## Development and test material
|
|
47
|
+
|
|
48
|
+
Neither of the following is distributed in the npm package: the fixture lives under `test/`, which is excluded from the package `files` list, and veraPDF is invoked as an external tool in its own container rather than vendored or linked.
|
|
49
|
+
|
|
50
|
+
### `test/fixtures/pdfa3-invoice.pdf`
|
|
51
|
+
|
|
52
|
+
A real ZUGFeRD 2.1 (EN 16931) PDF/A-3a invoice, used by `npm run verify:pdfa` to measure the PDF/A conformance of compression output. Taken unmodified from the [ZUGFeRD/corpus](https://github.com/ZUGFeRD/corpus) project (Apache-2.0), path `ZUGFeRDv2/correct/symtrax/Beispiele/EN16931/zugferd_2p1_EN16931_AbweichenderZahlungsempf.pdf`, SHA-256 `a4b903d4e508a80d65276f030d0fbeab5081d62c2c2b19ffd2f13bcdae37c564`.
|
|
53
|
+
|
|
54
|
+
### veraPDF
|
|
55
|
+
|
|
56
|
+
Copyright the veraPDF Consortium. Licensed under the GNU General Public License v3 or the Mozilla Public License v2. The reference PDF/A validator, run by `npm run verify:pdfa` from the `verapdf/cli` container image, pinned by digest in `scripts/pdfa-baseline.json`. See <https://github.com/veraPDF/veraPDF-apps>.
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ type PdfInput = Buffer | string;
|
|
|
8
8
|
* JPEG Huffman tables, recompresses all streams with Flate level 9,
|
|
9
9
|
* generates object streams, and removes unreferenced objects.
|
|
10
10
|
*
|
|
11
|
+
* Embedded file attachments are removed by default, along with every path
|
|
12
|
+
* they are reachable through. Pass `stripAttachments: false` to keep them —
|
|
13
|
+
* needed for hybrid invoices (ZUGFeRD / Factur-X), where the attachment is
|
|
14
|
+
* the document's payload rather than a rider on it.
|
|
15
|
+
*
|
|
11
16
|
* With `lossy: true`, uses more aggressive image re-encoding (skips JPEGs
|
|
12
17
|
* at q65 or below, re-encodes the rest at q75) and downscales to 72 DPI.
|
|
13
18
|
* Text, vectors, and fonts are preserved.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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
|
|
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;;;;;;;;;;;;;;;;GAgBG;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;AA6BtF,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
|
@@ -27,10 +27,12 @@ export async function compress(input, options) {
|
|
|
27
27
|
throw new TypeError('Input must be a Buffer or file path string');
|
|
28
28
|
}
|
|
29
29
|
const stripMetadata = options?.stripMetadata ?? true;
|
|
30
|
+
const stripAttachments = options?.stripAttachments ?? true;
|
|
30
31
|
try {
|
|
31
32
|
return await withConcurrency(() => addon.compress(input, {
|
|
32
33
|
...(options?.lossy ? { lossy: true } : {}),
|
|
33
34
|
...(stripMetadata ? { stripMetadata: true } : {}),
|
|
35
|
+
...(stripAttachments ? { stripAttachments: true } : {}),
|
|
34
36
|
...(options?.output ? { output: options.output } : {}),
|
|
35
37
|
}));
|
|
36
38
|
}
|
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;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;
|
|
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;AA0B5E,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,MAAM,gBAAgB,GAAG,OAAO,EAAE,gBAAgB,IAAI,IAAI,CAAC;IAC3D,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,gBAAgB,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvD,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/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface CompressOptions {
|
|
|
3
3
|
readonly lossy?: boolean;
|
|
4
4
|
/** Remove XMP metadata, document info, and thumbnails. Default: true. */
|
|
5
5
|
readonly stripMetadata?: boolean;
|
|
6
|
+
/** Remove embedded file attachments. Default: true. */
|
|
7
|
+
readonly stripAttachments?: boolean;
|
|
6
8
|
/** Write to this file path instead of returning a Buffer. */
|
|
7
9
|
readonly output?: string;
|
|
8
10
|
}
|
|
@@ -10,6 +12,7 @@ export interface NativeAddon {
|
|
|
10
12
|
compress(input: Buffer | string, options: {
|
|
11
13
|
lossy?: boolean;
|
|
12
14
|
stripMetadata?: boolean;
|
|
15
|
+
stripAttachments?: boolean;
|
|
13
16
|
output?: string;
|
|
14
17
|
}): Promise<Buffer | undefined>;
|
|
15
18
|
}
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CACN,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,OAAO,EAAE;QACP,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GACA,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAChC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,uDAAuD;IACvD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CACN,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,OAAO,EAAE;QACP,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GACA,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAChC"}
|
package/lib/index.ts
CHANGED
|
@@ -27,6 +27,11 @@ type PdfInput = Buffer | string;
|
|
|
27
27
|
* JPEG Huffman tables, recompresses all streams with Flate level 9,
|
|
28
28
|
* generates object streams, and removes unreferenced objects.
|
|
29
29
|
*
|
|
30
|
+
* Embedded file attachments are removed by default, along with every path
|
|
31
|
+
* they are reachable through. Pass `stripAttachments: false` to keep them —
|
|
32
|
+
* needed for hybrid invoices (ZUGFeRD / Factur-X), where the attachment is
|
|
33
|
+
* the document's payload rather than a rider on it.
|
|
34
|
+
*
|
|
30
35
|
* With `lossy: true`, uses more aggressive image re-encoding (skips JPEGs
|
|
31
36
|
* at q65 or below, re-encodes the rest at q75) and downscales to 72 DPI.
|
|
32
37
|
* Text, vectors, and fonts are preserved.
|
|
@@ -49,11 +54,13 @@ export async function compress(input: PdfInput, options?: CompressOptions): Prom
|
|
|
49
54
|
throw new TypeError('Input must be a Buffer or file path string');
|
|
50
55
|
}
|
|
51
56
|
const stripMetadata = options?.stripMetadata ?? true;
|
|
57
|
+
const stripAttachments = options?.stripAttachments ?? true;
|
|
52
58
|
try {
|
|
53
59
|
return await withConcurrency(() =>
|
|
54
60
|
addon.compress(input, {
|
|
55
61
|
...(options?.lossy ? { lossy: true } : {}),
|
|
56
62
|
...(stripMetadata ? { stripMetadata: true } : {}),
|
|
63
|
+
...(stripAttachments ? { stripAttachments: true } : {}),
|
|
57
64
|
...(options?.output ? { output: options.output } : {}),
|
|
58
65
|
}),
|
|
59
66
|
);
|
package/lib/types.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface CompressOptions {
|
|
|
3
3
|
readonly lossy?: boolean;
|
|
4
4
|
/** Remove XMP metadata, document info, and thumbnails. Default: true. */
|
|
5
5
|
readonly stripMetadata?: boolean;
|
|
6
|
+
/** Remove embedded file attachments. Default: true. */
|
|
7
|
+
readonly stripAttachments?: boolean;
|
|
6
8
|
/** Write to this file path instead of returning a Buffer. */
|
|
7
9
|
readonly output?: string;
|
|
8
10
|
}
|
|
@@ -13,6 +15,7 @@ export interface NativeAddon {
|
|
|
13
15
|
options: {
|
|
14
16
|
lossy?: boolean;
|
|
15
17
|
stripMetadata?: boolean;
|
|
18
|
+
stripAttachments?: boolean;
|
|
16
19
|
output?: string;
|
|
17
20
|
},
|
|
18
21
|
): Promise<Buffer | undefined>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qpdf-compress",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Native PDF compression for Node.js, powered by QPDF",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"download:qpdf": "node scripts/download-qpdf.mjs",
|
|
78
78
|
"download:harfbuzz": "node scripts/download-harfbuzz.mjs",
|
|
79
79
|
"verify:checksums": "node scripts/verify-checksums.mjs",
|
|
80
|
+
"verify:pdfa": "tsc && node scripts/verify-pdfa.mjs",
|
|
80
81
|
"test": "vitest run",
|
|
81
82
|
"test:coverage": "vitest run --coverage",
|
|
82
83
|
"typecheck": "tsc --noEmit",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"image": "verapdf/cli@sha256:d5ee329657cf9bc4b2400392dd54c7d0a0ce9980ff6fa2da5590eebeec007cdb",
|
|
3
|
+
"flavour": "3a",
|
|
4
|
+
"fixture": "pdfa3-invoice.pdf",
|
|
5
|
+
"variants": {
|
|
6
|
+
"original": {
|
|
7
|
+
"compliant": true,
|
|
8
|
+
"failedRules": []
|
|
9
|
+
},
|
|
10
|
+
"default": {
|
|
11
|
+
"compliant": false,
|
|
12
|
+
"failedRules": ["6.1.7.1-2", "6.2.4.3-2", "6.2.4.3-4", "6.6.2.1-1", "6.7.2.2-1", "6.7.3.3-1"]
|
|
13
|
+
},
|
|
14
|
+
"keep-attachments": {
|
|
15
|
+
"compliant": false,
|
|
16
|
+
"failedRules": ["6.1.7.1-2", "6.2.4.3-2", "6.2.4.3-4", "6.6.2.1-1", "6.7.2.2-1", "6.7.3.3-1"]
|
|
17
|
+
},
|
|
18
|
+
"keep-both": {
|
|
19
|
+
"compliant": false,
|
|
20
|
+
"failedRules": ["6.1.7.1-2", "6.2.4.3-2", "6.2.4.3-4", "6.7.3.3-1"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/scripts/prebuilds.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"darwin-arm64": "
|
|
3
|
-
"darwin-x64": "
|
|
4
|
-
"linux-arm": "
|
|
5
|
-
"linux-arm64": "
|
|
6
|
-
"linux-musl-arm64": "
|
|
7
|
-
"linux-musl-x64": "
|
|
8
|
-
"linux-x64": "
|
|
9
|
-
"win32-arm64": "
|
|
10
|
-
"win32-x64": "
|
|
2
|
+
"darwin-arm64": "6835d46768f1fc7366bbc60818dff396ca03c8ffb59b80e94454ca9dd5104fbf",
|
|
3
|
+
"darwin-x64": "dc222533f2ece1d5295f40f354a7fdd0846c2141a94ece211151cee6ceb45f70",
|
|
4
|
+
"linux-arm": "a0458de5782d4f900fba4de00cad8a678eb0a416d6cfbb153e82002446e941aa",
|
|
5
|
+
"linux-arm64": "ee25a8cd3ec974e6055206ecdfa96a5e5d4bc218885640b5739b149b734fe022",
|
|
6
|
+
"linux-musl-arm64": "f95653c36036947378834fa5715a55dd4ab8b0b6245a8bee95b91b280d3dda2b",
|
|
7
|
+
"linux-musl-x64": "7fdc5b849c9d4c8c7518490c274a1814c612b999e5752521f56b9e2253a9f37f",
|
|
8
|
+
"linux-x64": "a75e4213e1309ee49ed91d0c1e5aa591954db2c84fae0768fb1ec718a65b4a11",
|
|
9
|
+
"win32-arm64": "30d895806eec45f194c89b3fc1b521de164b1a2dd91adb8a7a939254baa0100d",
|
|
10
|
+
"win32-x64": "9b294de489115c6f8cb5733db260c12904f956e1ec30275776fc29cfcab600dc"
|
|
11
11
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validates the PDF/A conformance of compress() output against a recorded baseline.
|
|
4
|
+
*
|
|
5
|
+
* veraPDF is the reference PDF/A validator. It is GPL-3.0 / MPL-2.0, so it is neither
|
|
6
|
+
* vendored nor linked — it runs as an external tool in its own container, pinned by
|
|
7
|
+
* digest so a verdict cannot drift under us when the image is rebuilt.
|
|
8
|
+
*
|
|
9
|
+
* The baseline records which clauses each variant fails today. Any difference, in
|
|
10
|
+
* either direction, fails the run: a regression and an improvement both need the
|
|
11
|
+
* baseline updated deliberately (`--update`) rather than silently absorbed.
|
|
12
|
+
*
|
|
13
|
+
* Usage: npm run verify:pdfa [-- --update]
|
|
14
|
+
*/
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, join, resolve } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
import { compress } from '../dist/index.js';
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const root = resolve(__dirname, '..');
|
|
24
|
+
const baselinePath = join(__dirname, 'pdfa-baseline.json');
|
|
25
|
+
const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'));
|
|
26
|
+
const update = process.argv.includes('--update');
|
|
27
|
+
|
|
28
|
+
// the work directory must sit inside the repo: Docker Desktop shares the user's
|
|
29
|
+
// own tree, not every host temp path
|
|
30
|
+
const workDir = join(root, '.cache', 'pdfa');
|
|
31
|
+
|
|
32
|
+
const variants = {
|
|
33
|
+
original: null,
|
|
34
|
+
default: {},
|
|
35
|
+
'keep-attachments': { stripAttachments: false },
|
|
36
|
+
'keep-both': { stripAttachments: false, stripMetadata: false },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function veraPdf(name) {
|
|
40
|
+
const args = [
|
|
41
|
+
'run',
|
|
42
|
+
'--rm',
|
|
43
|
+
'--platform',
|
|
44
|
+
'linux/amd64',
|
|
45
|
+
'-v',
|
|
46
|
+
`${workDir}:/data`,
|
|
47
|
+
baseline.image,
|
|
48
|
+
'--flavour',
|
|
49
|
+
baseline.flavour,
|
|
50
|
+
'--format',
|
|
51
|
+
'mrr',
|
|
52
|
+
`/data/${name}.pdf`,
|
|
53
|
+
];
|
|
54
|
+
let xml;
|
|
55
|
+
try {
|
|
56
|
+
xml = execFileSync('docker', args, { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// veraPDF exits non-zero for a non-compliant file; the report is still on stdout
|
|
59
|
+
if (typeof err.stdout !== 'string' || !err.stdout.includes('<validationReport')) throw err;
|
|
60
|
+
xml = err.stdout;
|
|
61
|
+
}
|
|
62
|
+
const compliant = /<validationReport[^>]*isCompliant="true"/.test(xml);
|
|
63
|
+
const failed = [
|
|
64
|
+
...new Set(
|
|
65
|
+
[
|
|
66
|
+
...xml.matchAll(
|
|
67
|
+
/<rule\b[^>]*\bclause="([^"]+)"[^>]*\btestNumber="([^"]+)"[^>]*\bstatus="failed"/g,
|
|
68
|
+
),
|
|
69
|
+
].map((m) => `${m[1]}-${m[2]}`),
|
|
70
|
+
),
|
|
71
|
+
].sort();
|
|
72
|
+
return { compliant, failedRules: failed };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
execFileSync('docker', ['version', '--format', '{{.Server.Version}}'], { stdio: 'ignore' });
|
|
77
|
+
} catch {
|
|
78
|
+
console.error('Docker is required to run veraPDF. Start Docker and try again.');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
83
|
+
mkdirSync(workDir, { recursive: true });
|
|
84
|
+
|
|
85
|
+
const fixture = readFileSync(join(root, 'test', 'fixtures', baseline.fixture));
|
|
86
|
+
writeFileSync(join(workDir, 'original.pdf'), fixture);
|
|
87
|
+
for (const [name, options] of Object.entries(variants)) {
|
|
88
|
+
if (!options) continue;
|
|
89
|
+
await compress(fixture, { ...options, output: join(workDir, `${name}.pdf`) });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const results = {};
|
|
93
|
+
let failures = 0;
|
|
94
|
+
|
|
95
|
+
for (const name of Object.keys(variants)) {
|
|
96
|
+
const actual = veraPdf(name);
|
|
97
|
+
results[name] = actual;
|
|
98
|
+
|
|
99
|
+
const expected = baseline.variants[name];
|
|
100
|
+
const same =
|
|
101
|
+
expected &&
|
|
102
|
+
expected.compliant === actual.compliant &&
|
|
103
|
+
expected.failedRules.join() === actual.failedRules.join();
|
|
104
|
+
|
|
105
|
+
const verdict = actual.compliant ? 'PASS' : `FAIL (${actual.failedRules.length})`;
|
|
106
|
+
console.log(
|
|
107
|
+
`${same || update ? ' ok ' : 'DIFF'} ${name.padEnd(17)} ${verdict.padEnd(9)} ${actual.failedRules.join(' ')}`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!same && !update) {
|
|
111
|
+
failures++;
|
|
112
|
+
console.log(
|
|
113
|
+
` expected: ${expected ? `${expected.compliant ? 'PASS' : 'FAIL'} ${expected.failedRules.join(' ')}` : '(no baseline entry)'}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (update) {
|
|
119
|
+
writeFileSync(baselinePath, `${JSON.stringify({ ...baseline, variants: results }, null, 2)}\n`);
|
|
120
|
+
// keep the committed file canonical so the pre-commit hook has nothing to say
|
|
121
|
+
try {
|
|
122
|
+
execFileSync('npx', ['prettier', '--write', baselinePath], { stdio: 'ignore' });
|
|
123
|
+
} catch {
|
|
124
|
+
// prettier is a devDependency; a missing one is not worth failing the run over
|
|
125
|
+
}
|
|
126
|
+
console.log(`\nBaseline updated: ${baselinePath}`);
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (failures) {
|
|
131
|
+
console.error(
|
|
132
|
+
`\n${failures} variant(s) differ from the baseline. Re-run with --update once the change is intended.`,
|
|
133
|
+
);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
console.log('\nAll variants match the recorded baseline.');
|
package/src/qpdf_addon.cc
CHANGED
|
@@ -101,18 +101,18 @@ class CompressWorker : public Napi::AsyncWorker {
|
|
|
101
101
|
public:
|
|
102
102
|
// buffer variant
|
|
103
103
|
CompressWorker(Napi::Env env, std::vector<uint8_t> data, bool lossy,
|
|
104
|
-
bool stripMeta, std::string outputPath)
|
|
104
|
+
bool stripMeta, bool stripAttach, std::string outputPath)
|
|
105
105
|
: Napi::AsyncWorker(env), deferred_(Napi::Promise::Deferred::New(env)),
|
|
106
106
|
envAlive_(GetEnvAlive(env)), bufferData_(std::move(data)),
|
|
107
|
-
lossy_(lossy), stripMeta_(stripMeta),
|
|
108
|
-
outputPath_(std::move(outputPath)) {}
|
|
107
|
+
lossy_(lossy), stripMeta_(stripMeta), stripAttach_(stripAttach),
|
|
108
|
+
useFile_(false), outputPath_(std::move(outputPath)) {}
|
|
109
109
|
|
|
110
110
|
// file path variant
|
|
111
111
|
CompressWorker(Napi::Env env, std::string path, bool lossy, bool stripMeta,
|
|
112
|
-
std::string outputPath)
|
|
112
|
+
bool stripAttach, std::string outputPath)
|
|
113
113
|
: Napi::AsyncWorker(env), deferred_(Napi::Promise::Deferred::New(env)),
|
|
114
114
|
envAlive_(GetEnvAlive(env)), filePath_(std::move(path)), lossy_(lossy),
|
|
115
|
-
stripMeta_(stripMeta), useFile_(true),
|
|
115
|
+
stripMeta_(stripMeta), stripAttach_(stripAttach), useFile_(true),
|
|
116
116
|
outputPath_(std::move(outputPath)) {}
|
|
117
117
|
|
|
118
118
|
Napi::Promise Promise() { return deferred_.Promise(); }
|
|
@@ -187,7 +187,8 @@ protected:
|
|
|
187
187
|
coalesceContentStreams(*qpdf);
|
|
188
188
|
minifyContentStreams(*qpdf);
|
|
189
189
|
deduplicateStreams(*qpdf);
|
|
190
|
-
|
|
190
|
+
if (stripAttach_)
|
|
191
|
+
stripEmbeddedFiles(*qpdf);
|
|
191
192
|
stripJavaScript(*qpdf);
|
|
192
193
|
if (stripMeta_)
|
|
193
194
|
stripMetadata(*qpdf);
|
|
@@ -267,6 +268,7 @@ private:
|
|
|
267
268
|
std::string filePath_;
|
|
268
269
|
bool lossy_;
|
|
269
270
|
bool stripMeta_;
|
|
271
|
+
bool stripAttach_;
|
|
270
272
|
bool useFile_;
|
|
271
273
|
std::string outputPath_;
|
|
272
274
|
std::shared_ptr<Buffer> writerBuf_;
|
|
@@ -287,6 +289,7 @@ static Napi::Value Compress(const Napi::CallbackInfo &info) {
|
|
|
287
289
|
|
|
288
290
|
bool lossy = false;
|
|
289
291
|
bool stripMeta = false;
|
|
292
|
+
bool stripAttach = false;
|
|
290
293
|
std::string outputPath;
|
|
291
294
|
|
|
292
295
|
if (info.Length() >= 2 && info[1].IsObject()) {
|
|
@@ -298,6 +301,9 @@ static Napi::Value Compress(const Napi::CallbackInfo &info) {
|
|
|
298
301
|
if (options.Has("stripMetadata"))
|
|
299
302
|
stripMeta = options.Get("stripMetadata").As<Napi::Boolean>().Value();
|
|
300
303
|
|
|
304
|
+
if (options.Has("stripAttachments"))
|
|
305
|
+
stripAttach = options.Get("stripAttachments").As<Napi::Boolean>().Value();
|
|
306
|
+
|
|
301
307
|
if (options.Has("output"))
|
|
302
308
|
outputPath = options.Get("output").As<Napi::String>().Utf8Value();
|
|
303
309
|
}
|
|
@@ -306,7 +312,7 @@ static Napi::Value Compress(const Napi::CallbackInfo &info) {
|
|
|
306
312
|
auto buf = info[0].As<Napi::Buffer<uint8_t>>();
|
|
307
313
|
std::vector<uint8_t> data(buf.Data(), buf.Data() + buf.Length());
|
|
308
314
|
auto *worker = new CompressWorker(env, std::move(data), lossy, stripMeta,
|
|
309
|
-
std::move(outputPath));
|
|
315
|
+
stripAttach, std::move(outputPath));
|
|
310
316
|
worker->Queue();
|
|
311
317
|
return worker->Promise();
|
|
312
318
|
}
|
|
@@ -314,7 +320,7 @@ static Napi::Value Compress(const Napi::CallbackInfo &info) {
|
|
|
314
320
|
if (info[0].IsString()) {
|
|
315
321
|
auto path = info[0].As<Napi::String>().Utf8Value();
|
|
316
322
|
auto *worker = new CompressWorker(env, std::move(path), lossy, stripMeta,
|
|
317
|
-
std::move(outputPath));
|
|
323
|
+
stripAttach, std::move(outputPath));
|
|
318
324
|
worker->Queue();
|
|
319
325
|
return worker->Promise();
|
|
320
326
|
}
|
package/src/strip.cc
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include <set>
|
|
4
4
|
#include <string>
|
|
5
|
+
#include <vector>
|
|
5
6
|
|
|
6
7
|
#include <qpdf/QPDFObjectHandle.hh>
|
|
7
8
|
#include <qpdf/QPDFPageDocumentHelper.hh>
|
|
@@ -45,24 +46,69 @@ void stripMetadata(QPDF &qpdf) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
// ---------------------------------------------------------------------------
|
|
48
|
-
// Embedded file stripping — remove
|
|
49
|
+
// Embedded file stripping — remove every path an attachment is reachable
|
|
50
|
+
// through
|
|
49
51
|
// ---------------------------------------------------------------------------
|
|
50
52
|
|
|
53
|
+
static void removeAssociatedFiles(QPDFObjectHandle obj) {
|
|
54
|
+
auto dict = obj.isStream() ? obj.getDict() : obj;
|
|
55
|
+
if (dict.isDictionary() && dict.hasKey("/AF"))
|
|
56
|
+
dict.removeKey("/AF");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// an embedded file stream is reachable three ways: the /EmbeddedFiles name
|
|
60
|
+
// tree, an /AF associated-files array (PDF/A-3 and PDF 2.0), and a
|
|
61
|
+
// /FileAttachment annotation. Dropping only the name tree leaves the payload
|
|
62
|
+
// in the file and fully recoverable while hiding it from readers that look
|
|
63
|
+
// attachments up by name, so all three have to go — once nothing references
|
|
64
|
+
// the file specification, the writer drops it and its stream as unreferenced.
|
|
51
65
|
void stripEmbeddedFiles(QPDF &qpdf) {
|
|
52
66
|
auto root = qpdf.getRoot();
|
|
53
|
-
if (!root.hasKey("/Names"))
|
|
54
|
-
return;
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
if (root.hasKey("/Names")) {
|
|
69
|
+
auto names = root.getKey("/Names");
|
|
70
|
+
if (names.isDictionary()) {
|
|
71
|
+
if (names.hasKey("/EmbeddedFiles"))
|
|
72
|
+
names.removeKey("/EmbeddedFiles");
|
|
73
|
+
|
|
74
|
+
// if /Names is now empty, remove it too
|
|
75
|
+
if (names.getKeys().empty())
|
|
76
|
+
root.removeKey("/Names");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// /AF may hang off the catalog, a page, an XObject, or an annotation
|
|
81
|
+
for (auto &obj : qpdf.getAllObjects())
|
|
82
|
+
removeAssociatedFiles(obj);
|
|
83
|
+
|
|
84
|
+
for (auto &page : QPDFPageDocumentHelper(qpdf).getAllPages()) {
|
|
85
|
+
auto pageObj = page.getObjectHandle();
|
|
86
|
+
auto annots = pageObj.getKey("/Annots");
|
|
87
|
+
if (!annots.isArray())
|
|
88
|
+
continue;
|
|
89
|
+
|
|
90
|
+
std::vector<QPDFObjectHandle> kept;
|
|
91
|
+
for (int i = 0; i < annots.getArrayNItems(); ++i) {
|
|
92
|
+
auto annot = annots.getArrayItem(i);
|
|
93
|
+
// a directly embedded annotation dictionary is not an object of its own,
|
|
94
|
+
// so the sweep above does not reach it
|
|
95
|
+
removeAssociatedFiles(annot);
|
|
96
|
+
if (annot.isDictionary()) {
|
|
97
|
+
auto subtype = annot.getKey("/Subtype");
|
|
98
|
+
if (subtype.isName() && subtype.getName() == "/FileAttachment")
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
kept.push_back(annot);
|
|
102
|
+
}
|
|
59
103
|
|
|
60
|
-
|
|
61
|
-
|
|
104
|
+
if (static_cast<int>(kept.size()) == annots.getArrayNItems())
|
|
105
|
+
continue;
|
|
62
106
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
107
|
+
if (kept.empty())
|
|
108
|
+
pageObj.removeKey("/Annots");
|
|
109
|
+
else
|
|
110
|
+
pageObj.replaceKey("/Annots", QPDFObjectHandle::newArray(kept));
|
|
111
|
+
}
|
|
66
112
|
}
|
|
67
113
|
|
|
68
114
|
// ---------------------------------------------------------------------------
|