@u1f992/pdfdiff 0.2.0 → 0.2.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.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Visualize and quantify differences between two PDF files.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -9,7 +9,8 @@
9
9
  "scripts": {
10
10
  "test": "node --test",
11
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\"",
12
- "build": "rollup -c",
12
+ "build:version": "node scripts/version.ts",
13
+ "build": "npm run build:version && rollup -c",
13
14
  "serve": "npm run build && http-server dist"
14
15
  },
15
16
  "repository": {
@@ -0,0 +1,35 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { execSync } from "node:child_process";
3
+
4
+ const version = JSON.parse(readFileSync("package.json", "utf-8")).version;
5
+
6
+ let suffix = "";
7
+ try {
8
+ execSync(`git describe --tags --match "v${version}" --exact-match`, {
9
+ stdio: "ignore",
10
+ });
11
+ } catch {
12
+ try {
13
+ const hash = execSync("git rev-parse --short HEAD", {
14
+ encoding: "utf-8",
15
+ }).trim();
16
+ suffix = `+${hash}`;
17
+ } catch {
18
+ // not in a git repo
19
+ }
20
+ }
21
+
22
+ try {
23
+ const dirty = execSync("git status --porcelain", {
24
+ encoding: "utf-8",
25
+ }).trim();
26
+ if (dirty) {
27
+ suffix += ".dirty";
28
+ }
29
+ } catch {
30
+ // not in a git repo
31
+ }
32
+
33
+ const full = version + suffix;
34
+ writeFileSync("src/version.ts", `export const VERSION = "${full}";\n`, "utf-8");
35
+ console.log(full);
package/src/browser.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  /// <reference lib="dom" />
2
2
 
3
3
  import * as pdfdiff from "./index.ts";
4
+ import { VERSION } from "./version.ts";
5
+
6
+ const versionEl = document.getElementById("version");
7
+ if (versionEl) versionEl.textContent = "v" + VERSION;
4
8
 
5
9
  async function readFileAsUint8Array(file: File): Promise<Uint8Array> {
6
10
  return new Promise((resolve, reject) => {
package/src/cli.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  formatHex,
30
30
  visualizeDifferences,
31
31
  } from "./index.ts";
32
+ import { VERSION } from "./version.ts";
32
33
 
33
34
  const {
34
35
  positionals,
@@ -89,16 +90,7 @@ NOTES:
89
90
  process.exit(0);
90
91
  }
91
92
  if (version) {
92
- try {
93
- const versionStr = JSON.parse(
94
- fs.readFileSync(new URL("../package.json", import.meta.url), {
95
- encoding: "utf-8",
96
- }),
97
- ).version;
98
- console.log(versionStr);
99
- } catch {
100
- console.log("unknown");
101
- }
93
+ console.log(VERSION);
102
94
  process.exit(0);
103
95
  }
104
96
 
package/src/index.html CHANGED
@@ -9,7 +9,7 @@
9
9
  <script type="module" src="./browser.js"></script>
10
10
  </head>
11
11
  <body>
12
- <h1>pdfdiff</h1>
12
+ <h1>pdfdiff <small id="version"></small></h1>
13
13
  <div class="form-container">
14
14
  <form id="pdf-diff-form">
15
15
  <div>
package/src/index.ts CHANGED
@@ -23,9 +23,12 @@ import { type Pallet } from "./diff.ts";
23
23
  import { isValidAlignStrategy, type AlignStrategy } from "./image.ts";
24
24
  import { withIndex } from "./iterable.ts";
25
25
  import { parseHex, formatHex } from "./rgba-color.ts";
26
+ import { VERSION } from "./version.ts";
26
27
  import type { JimpInstance } from "./jimp.ts";
27
28
  import type {
29
+ ErrorMessage,
28
30
  InitMessage,
31
+ LoadedMessage,
29
32
  PageMessage,
30
33
  PageResultMessage,
31
34
  ReadyMessage,
@@ -80,37 +83,53 @@ function asSharedBytes(bytes: Uint8Array): Uint8Array {
80
83
  return new Uint8Array(bytes);
81
84
  }
82
85
 
86
+ type WorkerResponse =
87
+ | LoadedMessage
88
+ | ReadyMessage
89
+ | PageResultMessage
90
+ | ErrorMessage;
91
+
83
92
  class WorkerHandle {
84
93
  worker: InstanceType<typeof Worker>;
85
- private pendingResolve:
86
- | ((data: ReadyMessage | PageResultMessage) => void)
87
- | null = null;
94
+ private loaded: Promise<void>;
95
+ private pendingResolve: ((data: WorkerResponse) => void) | null = null;
88
96
  private pendingReject: ((reason: unknown) => void) | null = null;
89
97
 
90
98
  constructor(url: URL) {
91
99
  this.worker = new Worker(url, { type: "module" });
92
- this.worker.addEventListener(
93
- "message",
94
- (e: MessageEvent<ReadyMessage | PageResultMessage>) => {
100
+ this.loaded = new Promise<void>((resolveLoaded, rejectLoaded) => {
101
+ const onMessage = (e: MessageEvent<WorkerResponse>) => {
102
+ const data = e.data;
103
+ if (data.type === "loaded") {
104
+ resolveLoaded();
105
+ return;
106
+ }
95
107
  const resolve = this.pendingResolve;
108
+ const reject = this.pendingReject;
96
109
  this.pendingResolve = null;
97
110
  this.pendingReject = null;
98
- resolve?.(e.data);
99
- },
100
- );
101
- this.worker.addEventListener("error", (e: ErrorEvent) => {
102
- const reject = this.pendingReject;
103
- this.pendingResolve = null;
104
- this.pendingReject = null;
105
- reject?.(e.error ?? new Error(e.message));
111
+ if (data.type === "error") {
112
+ reject?.(new Error(`worker: ${data.message}`));
113
+ } else {
114
+ resolve?.(data);
115
+ }
116
+ };
117
+ this.worker.addEventListener("message", onMessage);
118
+ this.worker.addEventListener("error", (e: ErrorEvent) => {
119
+ const err = e.error ?? new Error(e.message);
120
+ rejectLoaded(err);
121
+ const reject = this.pendingReject;
122
+ this.pendingResolve = null;
123
+ this.pendingReject = null;
124
+ reject?.(err);
125
+ });
106
126
  });
107
127
  }
108
128
 
109
- init(msg: InitMessage): Promise<ReadyMessage> {
129
+ async init(msg: InitMessage): Promise<ReadyMessage> {
130
+ await this.loaded;
110
131
  return new Promise<ReadyMessage>((resolve, reject) => {
111
- this.pendingResolve = resolve as (
112
- data: ReadyMessage | PageResultMessage,
113
- ) => void;
132
+ this.pendingResolve = resolve as (data: WorkerResponse) => void;
114
133
  this.pendingReject = reject;
115
134
  this.worker.postMessage(msg);
116
135
  });
@@ -118,9 +137,7 @@ class WorkerHandle {
118
137
 
119
138
  processPage(index: number): Promise<PageResultMessage> {
120
139
  return new Promise<PageResultMessage>((resolve, reject) => {
121
- this.pendingResolve = resolve as (
122
- data: ReadyMessage | PageResultMessage,
123
- ) => void;
140
+ this.pendingResolve = resolve as (data: WorkerResponse) => void;
124
141
  this.pendingReject = reject;
125
142
  const msg: PageMessage = { type: "page", index };
126
143
  this.worker.postMessage(msg);
@@ -133,10 +150,8 @@ class WorkerHandle {
133
150
  }
134
151
 
135
152
  function workerUrl(): URL {
136
- return new URL(
137
- import.meta.url.endsWith(".ts") ? "./worker.ts" : "./worker.js",
138
- import.meta.url,
139
- );
153
+ const file = import.meta.url.endsWith(".ts") ? "./worker.ts" : "./worker.js";
154
+ return new URL(`${file}?v=${encodeURIComponent(VERSION)}`, import.meta.url);
140
155
  }
141
156
 
142
157
  function pageResultToResult(msg: PageResultMessage): Result {
package/src/worker.ts CHANGED
@@ -42,6 +42,10 @@ export type PageMessage = {
42
42
  index: number;
43
43
  };
44
44
 
45
+ export type LoadedMessage = {
46
+ type: "loaded";
47
+ };
48
+
45
49
  export type ReadyMessage = {
46
50
  type: "ready";
47
51
  };
@@ -57,6 +61,11 @@ export type PageResultMessage = {
57
61
  modification: [number, number][];
58
62
  };
59
63
 
64
+ export type ErrorMessage = {
65
+ type: "error";
66
+ message: string;
67
+ };
68
+
60
69
  let pdfA: mupdf.Document;
61
70
  let pdfB: mupdf.Document;
62
71
  let pdfMask: mupdf.Document;
@@ -131,29 +140,41 @@ async function processPage(index: number): Promise<PageResultMessage> {
131
140
  self.addEventListener(
132
141
  "message",
133
142
  async (e: MessageEvent<InitMessage | PageMessage>) => {
134
- const msg = e.data;
135
- if (msg.type === "init") {
136
- pdfA = mupdf.PDFDocument.openDocument(msg.aBytes, "application/pdf");
137
- pdfB = mupdf.PDFDocument.openDocument(msg.bBytes, "application/pdf");
138
- pdfMask = msg.maskBytes
139
- ? mupdf.PDFDocument.openDocument(msg.maskBytes, "application/pdf")
140
- : new mupdf.PDFDocument();
141
- opts = {
142
- dpi: msg.dpi,
143
- alpha: msg.alpha,
144
- pallet: msg.pallet,
145
- align: msg.align,
143
+ try {
144
+ const msg = e.data;
145
+ if (msg.type === "init") {
146
+ pdfA = mupdf.PDFDocument.openDocument(msg.aBytes, "application/pdf");
147
+ pdfB = mupdf.PDFDocument.openDocument(msg.bBytes, "application/pdf");
148
+ pdfMask = msg.maskBytes
149
+ ? mupdf.PDFDocument.openDocument(msg.maskBytes, "application/pdf")
150
+ : new mupdf.PDFDocument();
151
+ opts = {
152
+ dpi: msg.dpi,
153
+ alpha: msg.alpha,
154
+ pallet: msg.pallet,
155
+ align: msg.align,
156
+ };
157
+ if (pdfA.countPages() > 0) pdfA.loadPage(0).destroy();
158
+ const ready: ReadyMessage = { type: "ready" };
159
+ self.postMessage(ready);
160
+ } else if (msg.type === "page") {
161
+ const result = await processPage(msg.index);
162
+ self.postMessage(result, [
163
+ result.a.data,
164
+ result.b.data,
165
+ result.diff.data,
166
+ ]);
167
+ }
168
+ } catch (err) {
169
+ const errorMsg: ErrorMessage = {
170
+ type: "error",
171
+ message:
172
+ err instanceof Error ? `${err.message}\n${err.stack}` : String(err),
146
173
  };
147
- if (pdfA.countPages() > 0) pdfA.loadPage(0).destroy();
148
- const ready: ReadyMessage = { type: "ready" };
149
- self.postMessage(ready);
150
- } else if (msg.type === "page") {
151
- const result = await processPage(msg.index);
152
- self.postMessage(result, [
153
- result.a.data,
154
- result.b.data,
155
- result.diff.data,
156
- ]);
174
+ self.postMessage(errorMsg);
157
175
  }
158
176
  },
159
177
  );
178
+
179
+ const loaded: LoadedMessage = { type: "loaded" };
180
+ self.postMessage(loaded);