@u1f992/pdfdiff 0.3.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@u1f992/pdfdiff",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Visualize and quantify differences between two PDF files.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "test": "node --test",
11
- "test:cli": "node src/cli.js test/a.pdf test/b.pdf out --mask test/mask.pdf --dpi 300 && echo \"expected: Page 1, Addition: 7500, Deletion: 7500, Modification: 7500\"",
11
+ "test:cli": "node dist/cli.js test/a.pdf test/b.pdf out --mask test/mask.pdf --dpi 300 && echo \"expected: Page 1, Addition: 7500, Deletion: 7500, Modification: 7500\"",
12
12
  "build:version": "node scripts/version.ts",
13
13
  "build:wasm": "docker run --rm --volume .:/workdir --workdir /workdir --user root --entrypoint bash emscripten/emsdk:5.0.7 scripts/build-wasm.sh",
14
14
  "format:wasm": "docker run --rm --volume .:/workdir --workdir /workdir --user root --entrypoint bash emscripten/emsdk:5.0.7 scripts/build-wasm.sh format",
@@ -1,3 +1,3 @@
1
1
  const prettierConfig = {};
2
2
 
3
- export default prettierConfig;
3
+ export default prettierConfig;
package/src/cli.ts CHANGED
@@ -70,6 +70,17 @@ class PngWriterPool {
70
70
  }
71
71
  }
72
72
 
73
+ // Errors always exit 2, following diff(1)'s 0/1/2 convention, so that with
74
+ // --exit-code a caller can tell "differences found" (1) from a failed run.
75
+ process.on("uncaughtException", (err) => {
76
+ console.error(err);
77
+ process.exit(2);
78
+ });
79
+ process.on("unhandledRejection", (err) => {
80
+ console.error(err);
81
+ process.exit(2);
82
+ });
83
+
73
84
  const _wallSpan = perf.span("cli.wallTotal_ms");
74
85
 
75
86
  const {
@@ -83,6 +94,7 @@ const {
83
94
  "deletion-color": deletionColorHex,
84
95
  "modification-color": modificationColorHex,
85
96
  workers: workers_,
97
+ "exit-code": exitCode_,
86
98
  version,
87
99
  help,
88
100
  },
@@ -97,6 +109,7 @@ const {
97
109
  "deletion-color": { type: "string" },
98
110
  "modification-color": { type: "string" },
99
111
  workers: { type: "string" },
112
+ "exit-code": { type: "boolean" },
100
113
  version: { type: "boolean", short: "v" },
101
114
  help: { type: "boolean", short: "h" },
102
115
  },
@@ -117,9 +130,15 @@ OPTIONS:
117
130
  --deletion-color <#HEX> default: ${formatHex(defaultOptions.pallet.deletion)}
118
131
  --modification-color <#HEX> default: ${formatHex(defaultOptions.pallet.modification)}
119
132
  --workers <N> default: ${defaultOptions.workers}
133
+ --exit-code exit 1 if differences are found
120
134
  -v, --version
121
135
  -h, --help
122
136
 
137
+ EXIT STATUS:
138
+ 0 success (with --exit-code: no differences found)
139
+ 1 differences found (only with --exit-code)
140
+ 2 error
141
+
123
142
  NOTES:
124
143
  Pages are rendered with Ghostscript (gs-wasm). Each page render spins up a
125
144
  transient Ghostscript instance (~26 MB WASM binary plus a rasterization
@@ -192,12 +211,15 @@ if (Number.isNaN(workers) || workers < 1) {
192
211
  throw new Error("Invalid workers value");
193
212
  }
194
213
 
214
+ const exitCodeOnDiff = exitCode_ ?? false;
215
+
195
216
  fs.mkdirSync(outDir, { recursive: true });
196
217
  const writerPool = new PngWriterPool(
197
218
  workers,
198
219
  new URL("./cli-png-worker.js", import.meta.url),
199
220
  );
200
221
  const pendingWrites: Promise<void>[] = [];
222
+ let hasDiff = false;
201
223
 
202
224
  const _loopSpan = perf.span("cli.loopWall_ms");
203
225
  for await (const [
@@ -221,6 +243,9 @@ for await (const [
221
243
  console.log(
222
244
  `Page ${i}, Addition: ${addition.length}, Deletion: ${deletion.length}, Modification: ${modification.length}`,
223
245
  );
246
+ if (addition.length > 0 || deletion.length > 0 || modification.length > 0) {
247
+ hasDiff = true;
248
+ }
224
249
  const dir = path.join(outDir, i.toString(10));
225
250
  fs.mkdirSync(dir, { recursive: true });
226
251
  const sSubmit = perf.span("cli.poolSubmit_ms");
@@ -256,6 +281,10 @@ await writerPool.terminate();
256
281
  _loopSpan.stop();
257
282
  _wallSpan.stop();
258
283
 
284
+ if (exitCodeOnDiff && hasDiff) {
285
+ process.exitCode = 1;
286
+ }
287
+
259
288
  if (perf.enabled) {
260
289
  const counters = perf.dump();
261
290
  process.stderr.write("\n=== PERF ===\n");
package/src/decode.ts CHANGED
@@ -8,8 +8,6 @@ import type { JimpInstance } from "./jimp.ts";
8
8
  * `jimp.fromBuffer` accepts an ArrayBuffer directly, which keeps this usable in
9
9
  * the browser without relying on Node's `Buffer`.
10
10
  */
11
- export async function decodePng(
12
- png: ArrayBuffer,
13
- ): Promise<JimpInstance> {
11
+ export async function decodePng(png: ArrayBuffer): Promise<JimpInstance> {
14
12
  return (await jimp.Jimp.fromBuffer(png)) as JimpInstance;
15
13
  }
package/src/index.ts CHANGED
@@ -249,7 +249,11 @@ export async function* visualizeDifferences(
249
249
  const makeSlots = (count: number): Slot[] =>
250
250
  Array.from({ length: maxPages }, (_, i) => {
251
251
  if (i >= count) {
252
- return { p: Promise.resolve(null), resolve: () => {}, reject: () => {} };
252
+ return {
253
+ p: Promise.resolve(null),
254
+ resolve: () => {},
255
+ reject: () => {},
256
+ };
253
257
  }
254
258
  let resolve!: (v: Uint8Array<ArrayBuffer> | null) => void;
255
259
  let reject!: (e: unknown) => void;
@@ -279,7 +283,10 @@ export async function* visualizeDifferences(
279
283
  const totalRenderPages = aPages + bPages + (hasMask ? maskPages : 0);
280
284
  const chunkSize = Math.max(
281
285
  1,
282
- Math.min(maxPages, Math.max(MIN_CHUNK, Math.ceil(totalRenderPages / (2 * R)))),
286
+ Math.min(
287
+ maxPages,
288
+ Math.max(MIN_CHUNK, Math.ceil(totalRenderPages / (2 * R))),
289
+ ),
283
290
  );
284
291
  type Task = { bytes: Uint8Array; start: number; end: number; slots: Slot[] };
285
292
  const tasks: Task[] = [];
package/src/pdf.ts CHANGED
@@ -95,7 +95,9 @@ export async function renderPageRangePng(
95
95
  });
96
96
  sRender.stop();
97
97
  if (exitCode !== 0) {
98
- throw new Error(`gs render failed (pages ${first}-${last}, exit ${exitCode})`);
98
+ throw new Error(
99
+ `gs render failed (pages ${first}-${last}, exit ${exitCode})`,
100
+ );
99
101
  }
100
102
  const result = new Map<number, Uint8Array<ArrayBuffer>>();
101
103
  for (let k = 1; k <= pageCount; k++) {
package/tsconfig.json CHANGED
@@ -1,50 +1,53 @@
1
- {
2
- // Visit https://aka.ms/tsconfig to read more about this file
3
- "compilerOptions": {
4
- // File Layout
5
- "rootDir": "./src",
6
- "outDir": "./dist",
7
-
8
- // Environment Settings
9
- // See also https://aka.ms/tsconfig/module
10
- "module": "esnext",
11
- "target": "esnext",
12
- // "types": [],
13
- // For nodejs:
14
- "lib": ["esnext", "WebWorker", "DOM"],
15
- "types": ["node"],
16
- // and npm install -D @types/node
17
-
18
- // Other Outputs
19
- "sourceMap": true,
20
- "declaration": true,
21
- "declarationMap": true,
22
-
23
- // Stricter Typechecking Options
24
- "noUncheckedIndexedAccess": true,
25
- "exactOptionalPropertyTypes": true,
26
-
27
- // Style Options
28
- // "noImplicitReturns": true,
29
- // "noImplicitOverride": true,
30
- // "noUnusedLocals": true,
31
- // "noUnusedParameters": true,
32
- // "noFallthroughCasesInSwitch": true,
33
- // "noPropertyAccessFromIndexSignature": true,
34
-
35
- // Recommended Options
36
- "strict": true,
37
- "jsx": "react-jsx",
38
- "verbatimModuleSyntax": true,
39
- "isolatedModules": true,
40
- "noUncheckedSideEffectImports": true,
41
- "moduleDetection": "force",
42
- "skipLibCheck": true,
43
-
44
- // With Rollup
45
- "moduleResolution": "bundler",
46
- "allowImportingTsExtensions": true,
47
- "rewriteRelativeImportExtensions": true,
48
- "noEmit": true
49
- }
50
- }
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // File Layout
5
+ "rootDir": "./src",
6
+ "outDir": "./dist",
7
+
8
+ // Environment Settings
9
+ // See also https://aka.ms/tsconfig/module
10
+ "module": "esnext",
11
+ "target": "esnext",
12
+ // "types": [],
13
+ // For nodejs:
14
+ "lib": ["esnext", "WebWorker", "DOM"],
15
+ "types": ["node"],
16
+ // and npm install -D @types/node
17
+
18
+ // Other Outputs
19
+ "sourceMap": true,
20
+ "declaration": true,
21
+ "declarationMap": true,
22
+
23
+ // Stricter Typechecking Options
24
+ "noUncheckedIndexedAccess": true,
25
+ "exactOptionalPropertyTypes": true,
26
+
27
+ // Style Options
28
+ // "noImplicitReturns": true,
29
+ // "noImplicitOverride": true,
30
+ // "noUnusedLocals": true,
31
+ // "noUnusedParameters": true,
32
+ // "noFallthroughCasesInSwitch": true,
33
+ // "noPropertyAccessFromIndexSignature": true,
34
+
35
+ // Recommended Options
36
+ "strict": true,
37
+ "jsx": "react-jsx",
38
+ "verbatimModuleSyntax": true,
39
+ "isolatedModules": true,
40
+ "noUncheckedSideEffectImports": true,
41
+ "moduleDetection": "force",
42
+ "skipLibCheck": true,
43
+
44
+ // With Rollup
45
+ "moduleResolution": "bundler",
46
+ "allowImportingTsExtensions": true,
47
+ "rewriteRelativeImportExtensions": true,
48
+ "noEmit": true
49
+ },
50
+ // Keep `tsc --noEmit` runnable as a type check: scripts/version.ts is run
51
+ // directly by node (type stripping) and lives outside rootDir.
52
+ "include": ["src"]
53
+ }
package/wasm/core.c CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  #include <math.h>
4
4
  #include <stdlib.h>
5
- #include <string.h>
6
5
 
7
6
  #define COORD_INITIAL_CAPACITY 256
8
7
 
8
+ /* Zero constant for CoreResult; assigning it replaces struct-zeroing memset,
9
+ which clang-tidy's insecureAPI check rejects. */
10
+ static const CoreResult CORE_RESULT_ZERO = {0};
11
+
9
12
  typedef struct {
10
13
  int32_t *data;
11
14
  int32_t count;
@@ -76,7 +79,7 @@ void core_result_free(CoreResult *r) {
76
79
  free(r->addition_xy);
77
80
  free(r->deletion_xy);
78
81
  free(r->modification_xy);
79
- memset(r, 0, sizeof(*r));
82
+ *r = CORE_RESULT_ZERO;
80
83
  }
81
84
 
82
85
  int32_t process_page(const uint8_t *a_pixels, const uint8_t *b_pixels,
@@ -89,7 +92,7 @@ int32_t process_page(const uint8_t *a_pixels, const uint8_t *b_pixels,
89
92
  return CORE_ERROR_INVALID;
90
93
  }
91
94
 
92
- memset(out, 0, sizeof(*out));
95
+ *out = CORE_RESULT_ZERO;
93
96
 
94
97
  size_t pixel_count = (size_t)width * (size_t)height;
95
98
  size_t byte_count = pixel_count * 4;