qpdf-compress 0.8.0 → 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 CHANGED
@@ -6,6 +6,16 @@ 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
+
9
19
  ## [0.8.0] - 2026-09-23
10
20
 
11
21
  ### Fixed
package/README.md CHANGED
@@ -160,7 +160,7 @@ Compresses a PDF document. Automatically repairs damaged PDFs.
160
160
  | `options.stripAttachments` | `boolean` | Remove embedded file attachments. Default: `true` |
161
161
  | `options.output` | `string` | Write to file path instead of returning a `Buffer` |
162
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.
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.
164
164
 
165
165
  **Both modes:**
166
166
 
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qpdf-compress",
3
- "version": "0.8.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
+ }
@@ -1,11 +1,11 @@
1
1
  {
2
- "darwin-arm64": "91bce035bd98d4c5b51eb3c43bb4b3f1734768d4edb47042cce4dbb7b73bd001",
3
- "darwin-x64": "fe1fdd42d8ff24e5ed3777ced647d29f9878d0dde347b3925af3b388d49f4e15",
4
- "linux-arm": "478c10a51adefddc6f4cccc580edcb0d2242e8b155d495aac6ae2dc57ec96c10",
5
- "linux-arm64": "6f09b7b95ffdb382430e8d474fdad4664058210863668e85130d5f76d157796e",
6
- "linux-musl-arm64": "aee2f73320bb8d911be2dbfd657fd36ad7d767f27e5257fd8060d8c53b930c37",
7
- "linux-musl-x64": "0fa4510d0aac780f955880bf511e086b9c88af97f87d348f97569859d65d191d",
8
- "linux-x64": "75b2b183f810af513c7905406c695e325b39d5ef6125a8845465b75985515f0d",
9
- "win32-arm64": "fbac6517c8ea188ea9cfb72c5daac0e6b90a5cb8e185a6e1cf7dff116b4ff032",
10
- "win32-x64": "6b0a6f16c5cbe881aa3b4da12dcd138a637cf0b6764505a0e82a8b23df4d48d9"
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.');