papilot 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +126 -2
- package/README.zh-CN.md +127 -0
- package/bin/papilot.mjs +113 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/Inconsolatazi4-Regular-Dbtq7t_r.otf +0 -0
- package/dist/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
- package/dist/assets/TeXGyrePagellaX-Italic-CSuYIstn.otf +0 -0
- package/dist/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
- package/dist/assets/cursor-editorTextHighlight-kG3uUOwL.svg +8 -0
- package/dist/assets/index-BhQFCMTx.js +131 -0
- package/dist/assets/index-CAV3-87U.css +1 -0
- package/dist/assets/pdf-view-DJo0X38w.js +598 -0
- package/dist/assets/pdf.worker.min-CjEcRF4W.mjs +28 -0
- package/dist/favicon.png +0 -0
- package/dist/index.html +4 -0
- package/examples/papilot-paper/.latex-review.json +4 -0
- package/examples/papilot-paper/appendix/proofs.tex +60 -0
- package/examples/papilot-paper/appendix/reference.tex +54 -0
- package/examples/papilot-paper/main.tex +57 -0
- package/examples/papilot-paper/preamble.tex +53 -0
- package/examples/papilot-paper/references.bib +158 -0
- package/examples/papilot-paper/sections/01-introduction.tex +66 -0
- package/examples/papilot-paper/sections/02-using.tex +134 -0
- package/examples/papilot-paper/sections/03-background.tex +97 -0
- package/examples/papilot-paper/sections/04-model.tex +185 -0
- package/examples/papilot-paper/sections/05-conflicts.tex +99 -0
- package/examples/papilot-paper/sections/06-building.tex +70 -0
- package/examples/papilot-paper/sections/07-geometry.tex +193 -0
- package/examples/papilot-paper/sections/08-notes.tex +104 -0
- package/examples/papilot-paper/sections/09-agents.tex +106 -0
- package/examples/papilot-paper/sections/10-usage.tex +56 -0
- package/examples/papilot-paper/sections/11-git.tex +62 -0
- package/examples/papilot-paper/sections/12-security.tex +41 -0
- package/examples/papilot-paper/sections/13-evaluation.tex +106 -0
- package/examples/papilot-paper/sections/14-related.tex +38 -0
- package/examples/papilot-paper/sections/15-conclusion.tex +27 -0
- package/package.json +85 -4
- package/scripts/demo.ts +21 -0
- package/scripts/service.ts +35 -0
- package/src/notes.ts +40 -0
- package/src/server/agents.ts +343 -0
- package/src/server/codex.ts +63 -0
- package/src/server/frontend.ts +15 -0
- package/src/server/main.ts +202 -0
- package/src/server/manuscript.ts +337 -0
- package/src/server/paths.ts +10 -0
- package/src/server/pdf.ts +27 -0
- package/src/server/review.ts +53 -0
- package/src/server/security.ts +15 -0
- package/src/server/store.ts +60 -0
- package/src/server/workspace.ts +529 -0
- package/src/shared.ts +28 -0
- package/vendor/latex-review/LICENSE +21 -0
- package/vendor/latex-review/dist/web/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
- package/vendor/latex-review/dist/web/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
- package/vendor/latex-review/dist/web/assets/index-DyzYnIGk.css +1 -0
- package/vendor/latex-review/dist/web/assets/index-HxBK-dls.js +19 -0
- package/vendor/latex-review/dist/web/index.html +18 -0
- package/vendor/latex-review/package-lock.json +3114 -0
- package/vendor/latex-review/package.json +49 -0
- package/vendor/latex-review/src/cli.ts +494 -0
- package/vendor/latex-review/src/client.ts +198 -0
- package/vendor/latex-review/src/core/build.ts +296 -0
- package/vendor/latex-review/src/core/changes.ts +126 -0
- package/vendor/latex-review/src/core/config.ts +270 -0
- package/vendor/latex-review/src/core/diff.ts +54 -0
- package/vendor/latex-review/src/core/files.ts +96 -0
- package/vendor/latex-review/src/core/git.ts +317 -0
- package/vendor/latex-review/src/core/outline.ts +314 -0
- package/vendor/latex-review/src/core/process.ts +111 -0
- package/vendor/latex-review/src/core/publish.ts +339 -0
- package/vendor/latex-review/src/core/review.ts +734 -0
- package/vendor/latex-review/src/core/store.ts +66 -0
- package/vendor/latex-review/src/core/tasks.ts +237 -0
- package/vendor/latex-review/src/render/inline.ts +805 -0
- package/vendor/latex-review/src/render/locate.ts +481 -0
- package/vendor/latex-review/src/render/pdf.ts +281 -0
- package/vendor/latex-review/src/render/pdfdiff.ts +272 -0
- package/vendor/latex-review/src/render/round.ts +236 -0
- package/vendor/latex-review/src/render/worker.ts +10 -0
- package/vendor/latex-review/src/runtime.ts +23 -0
- package/vendor/latex-review/src/server.ts +401 -0
- package/vendor/latex-review/src/types.ts +283 -0
- package/vendor/latex-review/src/web/ChangeMap.tsx +212 -0
- package/vendor/latex-review/src/web/PdfStrip.tsx +77 -0
- package/vendor/latex-review/src/web/fonts/GUST-FONT-LICENSE.txt +29 -0
- package/vendor/latex-review/src/web/fonts/LPPL.txt +415 -0
- package/vendor/latex-review/src/web/fonts/MANIFEST-newpx.txt +20 -0
- package/vendor/latex-review/src/web/fonts/README-newpx.txt +473 -0
- package/vendor/latex-review/src/web/fonts/README.md +12 -0
- package/vendor/latex-review/src/web/fonts/TeXGyrePagellaX-Bold.otf +0 -0
- package/vendor/latex-review/src/web/fonts/TeXGyrePagellaX-Regular.otf +0 -0
- package/vendor/latex-review/src/web/index.html +17 -0
- package/vendor/latex-review/src/web/main.tsx +10 -0
- package/vendor/latex-review/src/web/model.ts +153 -0
- package/vendor/latex-review/src/web/overview.tsx +146 -0
- package/vendor/latex-review/src/web/review.tsx +991 -0
- package/vendor/latex-review/src/web/style.css +1249 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import * as mupdf from "mupdf";
|
|
3
|
+
import type { Rect } from "../types.ts";
|
|
4
|
+
export { mupdf };
|
|
5
|
+
export const union = (a: Rect, b: Rect): Rect => [
|
|
6
|
+
Math.min(a[0], b[0]),
|
|
7
|
+
Math.min(a[1], b[1]),
|
|
8
|
+
Math.max(a[2], b[2]),
|
|
9
|
+
Math.max(a[3], b[3]),
|
|
10
|
+
];
|
|
11
|
+
export const intersect = (a: Rect, b: Rect): Rect => [
|
|
12
|
+
Math.max(a[0], b[0]),
|
|
13
|
+
Math.max(a[1], b[1]),
|
|
14
|
+
Math.min(a[2], b[2]),
|
|
15
|
+
Math.min(a[3], b[3]),
|
|
16
|
+
];
|
|
17
|
+
export const intersects = (a: Rect, b: Rect) =>
|
|
18
|
+
a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1];
|
|
19
|
+
export const containsCenter = (a: Rect, b: Rect) =>
|
|
20
|
+
a[0] <= (b[0] + b[2]) / 2 &&
|
|
21
|
+
(b[0] + b[2]) / 2 <= a[2] &&
|
|
22
|
+
a[1] <= (b[1] + b[3]) / 2 &&
|
|
23
|
+
(b[1] + b[3]) / 2 <= a[3];
|
|
24
|
+
export interface Character {
|
|
25
|
+
text: string;
|
|
26
|
+
rect: Rect;
|
|
27
|
+
}
|
|
28
|
+
export interface Word {
|
|
29
|
+
text: string;
|
|
30
|
+
rect: Rect;
|
|
31
|
+
}
|
|
32
|
+
export interface Line {
|
|
33
|
+
text: string;
|
|
34
|
+
rect: Rect;
|
|
35
|
+
chars: Character[];
|
|
36
|
+
}
|
|
37
|
+
export interface TextBlock {
|
|
38
|
+
text: string;
|
|
39
|
+
rect: Rect;
|
|
40
|
+
lines: Line[];
|
|
41
|
+
}
|
|
42
|
+
export interface OutlineEntry {
|
|
43
|
+
level: number;
|
|
44
|
+
title: string;
|
|
45
|
+
uri: string;
|
|
46
|
+
name: string;
|
|
47
|
+
page: number;
|
|
48
|
+
y: number;
|
|
49
|
+
}
|
|
50
|
+
export interface Raster {
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
channels: number;
|
|
54
|
+
stride: number;
|
|
55
|
+
pixels: Uint8Array;
|
|
56
|
+
}
|
|
57
|
+
export class PdfPage {
|
|
58
|
+
private columns?: boolean;
|
|
59
|
+
raw: mupdf.Page;
|
|
60
|
+
bounds: Rect;
|
|
61
|
+
blocks: TextBlock[] = [];
|
|
62
|
+
lines: Line[] = [];
|
|
63
|
+
words: Word[] = [];
|
|
64
|
+
label: string;
|
|
65
|
+
constructor(
|
|
66
|
+
document: mupdf.Document,
|
|
67
|
+
public number: number,
|
|
68
|
+
) {
|
|
69
|
+
this.raw = document.loadPage(number);
|
|
70
|
+
this.bounds = this.raw.getBounds();
|
|
71
|
+
this.label = this.raw.getLabel() || String(number + 1);
|
|
72
|
+
const text = this.raw.toStructuredText(
|
|
73
|
+
"preserve-whitespace,preserve-ligatures",
|
|
74
|
+
);
|
|
75
|
+
let block: TextBlock, line: Line;
|
|
76
|
+
text.walk({
|
|
77
|
+
beginTextBlock: (rect) => {
|
|
78
|
+
block = { rect, text: "", lines: [] };
|
|
79
|
+
this.blocks.push(block);
|
|
80
|
+
},
|
|
81
|
+
beginLine: (rect) => {
|
|
82
|
+
line = { rect, text: "", chars: [] };
|
|
83
|
+
block.lines.push(line);
|
|
84
|
+
this.lines.push(line);
|
|
85
|
+
},
|
|
86
|
+
onChar: (c, _origin, _font, _size, quad) => {
|
|
87
|
+
const xs = [quad[0], quad[2], quad[4], quad[6]],
|
|
88
|
+
ys = [quad[1], quad[3], quad[5], quad[7]];
|
|
89
|
+
line.chars.push({
|
|
90
|
+
text: c,
|
|
91
|
+
rect: [
|
|
92
|
+
Math.min(...xs),
|
|
93
|
+
Math.min(...ys),
|
|
94
|
+
Math.max(...xs),
|
|
95
|
+
Math.max(...ys),
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
line.text += c;
|
|
99
|
+
},
|
|
100
|
+
endLine: () => {
|
|
101
|
+
block.text += line.text + "\n";
|
|
102
|
+
let word: Word | null = null;
|
|
103
|
+
for (const char of line.chars) {
|
|
104
|
+
if (/\s/u.test(char.text)) word = null;
|
|
105
|
+
else if (word) {
|
|
106
|
+
word.text += char.text;
|
|
107
|
+
word.rect = union(word.rect, char.rect);
|
|
108
|
+
} else {
|
|
109
|
+
word = { text: char.text, rect: [...char.rect] };
|
|
110
|
+
this.words.push(word);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
text.destroy();
|
|
116
|
+
}
|
|
117
|
+
get width() {
|
|
118
|
+
return this.bounds[2] - this.bounds[0];
|
|
119
|
+
}
|
|
120
|
+
get twoColumns() {
|
|
121
|
+
if (this.columns === undefined) {
|
|
122
|
+
const middle = this.bounds[0] + this.width / 2;
|
|
123
|
+
const prose = this.lines.filter(
|
|
124
|
+
(l) =>
|
|
125
|
+
l.text.trim().split(/\s+/).length >= 5 &&
|
|
126
|
+
l.rect[2] - l.rect[0] > this.width * 0.25,
|
|
127
|
+
);
|
|
128
|
+
const left = prose.filter((l) => l.rect[2] < middle),
|
|
129
|
+
right = prose.filter((l) => l.rect[0] > middle);
|
|
130
|
+
this.columns =
|
|
131
|
+
left.filter((a) =>
|
|
132
|
+
right.some(
|
|
133
|
+
(b) =>
|
|
134
|
+
Math.min(a.rect[3], b.rect[3]) - Math.max(a.rect[1], b.rect[1]) >
|
|
135
|
+
3,
|
|
136
|
+
),
|
|
137
|
+
).length >= 3;
|
|
138
|
+
}
|
|
139
|
+
return this.columns;
|
|
140
|
+
}
|
|
141
|
+
column(rect: Rect) {
|
|
142
|
+
if (!this.twoColumns) return -1;
|
|
143
|
+
const middle = this.bounds[0] + this.width / 2;
|
|
144
|
+
return rect[2] <= middle ? 0 : rect[0] >= middle ? 1 : -1;
|
|
145
|
+
}
|
|
146
|
+
columnBounds(column: number): [number, number] {
|
|
147
|
+
const middle = this.bounds[0] + this.width / 2;
|
|
148
|
+
return column === 0
|
|
149
|
+
? [this.bounds[0], middle]
|
|
150
|
+
: column === 1
|
|
151
|
+
? [middle, this.bounds[2]]
|
|
152
|
+
: [this.bounds[0], this.bounds[2]];
|
|
153
|
+
}
|
|
154
|
+
get height() {
|
|
155
|
+
return this.bounds[3] - this.bounds[1];
|
|
156
|
+
}
|
|
157
|
+
search(needle: string): Rect[] {
|
|
158
|
+
return this.raw
|
|
159
|
+
.search(needle, {})
|
|
160
|
+
.flat()
|
|
161
|
+
.map((q) => [
|
|
162
|
+
Math.min(q[0], q[2], q[4], q[6]),
|
|
163
|
+
Math.min(q[1], q[3], q[5], q[7]),
|
|
164
|
+
Math.max(q[0], q[2], q[4], q[6]),
|
|
165
|
+
Math.max(q[1], q[3], q[5], q[7]),
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
text(clip: Rect = this.bounds) {
|
|
169
|
+
return this.lines
|
|
170
|
+
.map((l) =>
|
|
171
|
+
l.chars
|
|
172
|
+
.filter((c) => containsCenter(clip, c.rect))
|
|
173
|
+
.map((c) => c.text)
|
|
174
|
+
.join(""),
|
|
175
|
+
)
|
|
176
|
+
.filter(Boolean)
|
|
177
|
+
.join("\n");
|
|
178
|
+
}
|
|
179
|
+
raster(clip: Rect, scale = 2, gray = false): Raster {
|
|
180
|
+
const matrix: mupdf.Matrix = [scale, 0, 0, scale, 0, 0],
|
|
181
|
+
rect: Rect = [
|
|
182
|
+
Math.floor(clip[0] * scale),
|
|
183
|
+
Math.floor(clip[1] * scale),
|
|
184
|
+
Math.ceil(clip[2] * scale),
|
|
185
|
+
Math.ceil(clip[3] * scale),
|
|
186
|
+
],
|
|
187
|
+
pixmap = new mupdf.Pixmap(
|
|
188
|
+
gray ? mupdf.ColorSpace.DeviceGray : mupdf.ColorSpace.DeviceRGB,
|
|
189
|
+
rect,
|
|
190
|
+
false,
|
|
191
|
+
);
|
|
192
|
+
pixmap.clear(255);
|
|
193
|
+
const device = new mupdf.DrawDevice(mupdf.Matrix.identity, pixmap);
|
|
194
|
+
this.raw.run(device, matrix);
|
|
195
|
+
device.close();
|
|
196
|
+
device.destroy();
|
|
197
|
+
const result = {
|
|
198
|
+
width: pixmap.getWidth(),
|
|
199
|
+
height: pixmap.getHeight(),
|
|
200
|
+
channels: pixmap.getNumberOfComponents(),
|
|
201
|
+
stride: pixmap.getStride(),
|
|
202
|
+
pixels: new Uint8Array(pixmap.getPixels()),
|
|
203
|
+
};
|
|
204
|
+
pixmap.destroy();
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
svg(top: number, bottom: number) {
|
|
208
|
+
const buffer = new mupdf.Buffer(),
|
|
209
|
+
writer = new mupdf.DocumentWriter(buffer, "svg", "text=path"),
|
|
210
|
+
device = writer.beginPage([0, 0, this.width, bottom - top]);
|
|
211
|
+
this.raw.run(device, [1, 0, 0, 1, 0, -top]);
|
|
212
|
+
writer.endPage();
|
|
213
|
+
writer.close();
|
|
214
|
+
const svg = buffer.asString();
|
|
215
|
+
device.destroy();
|
|
216
|
+
writer.destroy();
|
|
217
|
+
buffer.destroy();
|
|
218
|
+
return svg;
|
|
219
|
+
}
|
|
220
|
+
close() {
|
|
221
|
+
this.raw.destroy();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export class PdfDocument {
|
|
225
|
+
raw: mupdf.Document;
|
|
226
|
+
private pages = new Map<number, PdfPage>();
|
|
227
|
+
outline: OutlineEntry[] = [];
|
|
228
|
+
constructor(input: string | Uint8Array) {
|
|
229
|
+
this.raw = mupdf.Document.openDocument(
|
|
230
|
+
typeof input === "string" ? fs.readFileSync(input) : input,
|
|
231
|
+
"application/pdf",
|
|
232
|
+
);
|
|
233
|
+
const walk = (
|
|
234
|
+
items: NonNullable<ReturnType<mupdf.Document["loadOutline"]>>,
|
|
235
|
+
level: number,
|
|
236
|
+
) => {
|
|
237
|
+
for (const item of items) {
|
|
238
|
+
const uri = item.uri ?? "",
|
|
239
|
+
dest = this.destination(uri);
|
|
240
|
+
this.outline.push({
|
|
241
|
+
level,
|
|
242
|
+
title: item.title ?? "",
|
|
243
|
+
uri,
|
|
244
|
+
name:
|
|
245
|
+
new URLSearchParams(uri.replace(/^#/, "")).get("nameddest") ?? "",
|
|
246
|
+
page: dest?.page ?? item.page ?? -1,
|
|
247
|
+
y: dest?.y ?? 0,
|
|
248
|
+
});
|
|
249
|
+
if (item.down) walk(item.down, level + 1);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
walk(this.raw.loadOutline() ?? [], 1);
|
|
253
|
+
}
|
|
254
|
+
get length() {
|
|
255
|
+
return this.raw.countPages();
|
|
256
|
+
}
|
|
257
|
+
page(number: number) {
|
|
258
|
+
if (!this.pages.has(number))
|
|
259
|
+
this.pages.set(number, new PdfPage(this.raw, number));
|
|
260
|
+
return this.pages.get(number)!;
|
|
261
|
+
}
|
|
262
|
+
destination(uri: string) {
|
|
263
|
+
if (!uri) return null;
|
|
264
|
+
try {
|
|
265
|
+
const dest = this.raw.resolveLinkDestination(uri);
|
|
266
|
+
return dest.page >= 0
|
|
267
|
+
? {
|
|
268
|
+
page: dest.page,
|
|
269
|
+
y: Number.isFinite(dest.y) ? dest.y : 0,
|
|
270
|
+
x: Number.isFinite(dest.x) ? dest.x : 0,
|
|
271
|
+
}
|
|
272
|
+
: null;
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
close() {
|
|
278
|
+
for (const page of this.pages.values()) page.close();
|
|
279
|
+
this.raw.destroy();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { opcodes } from "../core/diff.ts";
|
|
2
|
+
import type { Mark, Position, Rect } from "../types.ts";
|
|
3
|
+
import { containsCenter, PdfDocument, union, type Raster } from "./pdf.ts";
|
|
4
|
+
export const normalize = (text: string) =>
|
|
5
|
+
text.normalize("NFKC").replaceAll("\u00ad", "");
|
|
6
|
+
interface Glyph {
|
|
7
|
+
text: string;
|
|
8
|
+
rect: Rect;
|
|
9
|
+
pair: number;
|
|
10
|
+
page: number;
|
|
11
|
+
space: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface Word {
|
|
14
|
+
text: string;
|
|
15
|
+
rect: Rect;
|
|
16
|
+
pair: number;
|
|
17
|
+
page: number;
|
|
18
|
+
}
|
|
19
|
+
export function extract(
|
|
20
|
+
document: PdfDocument,
|
|
21
|
+
positions: Position[],
|
|
22
|
+
): [Glyph[], Word[]] {
|
|
23
|
+
const glyphs: Glyph[] = [],
|
|
24
|
+
words: Word[] = [];
|
|
25
|
+
positions.forEach((position, pair) => {
|
|
26
|
+
const page = document.page(position.page),
|
|
27
|
+
clip = position.clip;
|
|
28
|
+
for (const line of page.lines) {
|
|
29
|
+
let space = true;
|
|
30
|
+
for (const char of line.chars) {
|
|
31
|
+
if (!containsCenter(clip, char.rect)) {
|
|
32
|
+
space = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
for (const value of normalize(char.text)) {
|
|
36
|
+
if (/\s/u.test(value)) space = true;
|
|
37
|
+
else {
|
|
38
|
+
glyphs.push({
|
|
39
|
+
text: value,
|
|
40
|
+
rect: char.rect,
|
|
41
|
+
space,
|
|
42
|
+
pair,
|
|
43
|
+
page: position.page,
|
|
44
|
+
});
|
|
45
|
+
space = false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const word of page.words)
|
|
51
|
+
if (containsCenter(clip, word.rect))
|
|
52
|
+
words.push({
|
|
53
|
+
text: normalize(word.text),
|
|
54
|
+
rect: word.rect,
|
|
55
|
+
pair,
|
|
56
|
+
page: position.page,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
for (let i = glyphs.length - 2; i > 0; i--) {
|
|
60
|
+
const a = glyphs[i - 1],
|
|
61
|
+
b = glyphs[i],
|
|
62
|
+
c = glyphs[i + 1];
|
|
63
|
+
if (
|
|
64
|
+
b.text === "-" &&
|
|
65
|
+
/\p{L}/u.test(a.text) &&
|
|
66
|
+
/\p{Ll}/u.test(c.text) &&
|
|
67
|
+
(b.pair !== c.pair || c.rect[1] > b.rect[1] + 3)
|
|
68
|
+
) {
|
|
69
|
+
c.space = false;
|
|
70
|
+
glyphs.splice(i, 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return [glyphs, words];
|
|
74
|
+
}
|
|
75
|
+
function tokens(glyphs: Glyph[]) {
|
|
76
|
+
const result: { text: string; start: number; end: number }[] = [];
|
|
77
|
+
let previousWord = false;
|
|
78
|
+
glyphs.forEach((glyph, i) => {
|
|
79
|
+
const word = /[\p{L}\p{N}_]/u.test(glyph.text);
|
|
80
|
+
if (result.length && word && previousWord && !glyph.space) {
|
|
81
|
+
result.at(-1)!.text += glyph.text;
|
|
82
|
+
result.at(-1)!.end = i + 1;
|
|
83
|
+
} else result.push({ text: glyph.text, start: i, end: i + 1 });
|
|
84
|
+
previousWord = word;
|
|
85
|
+
});
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
export function mergeRects(rects: Rect[]): Rect[] {
|
|
89
|
+
const result: Rect[] = [];
|
|
90
|
+
for (const rect of [...rects].sort(
|
|
91
|
+
(a, b) => Math.round(a[1] / 2) - Math.round(b[1] / 2) || a[0] - b[0],
|
|
92
|
+
)) {
|
|
93
|
+
let merged = false;
|
|
94
|
+
for (let i = result.length - 1; i >= Math.max(0, result.length - 12); i--) {
|
|
95
|
+
const previous = result[i];
|
|
96
|
+
if (
|
|
97
|
+
Math.abs(previous[1] - rect[1]) < 1.7 &&
|
|
98
|
+
Math.abs(previous[3] - rect[3]) < 2 &&
|
|
99
|
+
rect[0] - previous[2] >= -1 &&
|
|
100
|
+
rect[0] - previous[2] < 4
|
|
101
|
+
) {
|
|
102
|
+
result[i] = union(previous, rect);
|
|
103
|
+
merged = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!merged) result.push([...rect]);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
function marks(
|
|
112
|
+
glyphs: Glyph[],
|
|
113
|
+
indices: number[],
|
|
114
|
+
words: Word[],
|
|
115
|
+
kind: Mark["kind"],
|
|
116
|
+
): Record<number, Mark[]> {
|
|
117
|
+
const selected = new Map<number, Rect[]>();
|
|
118
|
+
for (const i of new Set(indices)) {
|
|
119
|
+
const g = glyphs[i];
|
|
120
|
+
if (!selected.has(g.pair)) selected.set(g.pair, []);
|
|
121
|
+
selected.get(g.pair)!.push(g.rect);
|
|
122
|
+
}
|
|
123
|
+
return Object.fromEntries(
|
|
124
|
+
[...selected].map(([pair, rects]) => {
|
|
125
|
+
const boxes = words
|
|
126
|
+
.filter(
|
|
127
|
+
(w) =>
|
|
128
|
+
w.pair === pair && rects.some((r) => containsCenter(w.rect, r)),
|
|
129
|
+
)
|
|
130
|
+
.map((w) => w.rect);
|
|
131
|
+
return [
|
|
132
|
+
pair,
|
|
133
|
+
mergeRects(boxes.length ? boxes : rects).map((rect) => ({
|
|
134
|
+
kind,
|
|
135
|
+
rect,
|
|
136
|
+
})),
|
|
137
|
+
];
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
export function joined(words: Word[]) {
|
|
142
|
+
const result: string[] = [];
|
|
143
|
+
let previous: Word | null = null;
|
|
144
|
+
for (const word of words) {
|
|
145
|
+
const last = result.at(-1) ?? "";
|
|
146
|
+
if (
|
|
147
|
+
previous &&
|
|
148
|
+
/\p{L}-$/u.test(last) &&
|
|
149
|
+
/^\p{Ll}/u.test(word.text) &&
|
|
150
|
+
(word.pair !== previous.pair || word.rect[1] > previous.rect[3] - 2)
|
|
151
|
+
)
|
|
152
|
+
result[result.length - 1] = last.slice(0, -1) + word.text;
|
|
153
|
+
else result.push(word.text);
|
|
154
|
+
previous = word;
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
export function textChanges(
|
|
159
|
+
oldDocument: PdfDocument | null,
|
|
160
|
+
newDocument: PdfDocument | null,
|
|
161
|
+
left: Position[],
|
|
162
|
+
right: Position[],
|
|
163
|
+
) {
|
|
164
|
+
const [a, aw] = left.length
|
|
165
|
+
? extract(oldDocument!, left)
|
|
166
|
+
: ([[], []] as [Glyph[], Word[]]),
|
|
167
|
+
[b, bw] = right.length
|
|
168
|
+
? extract(newDocument!, right)
|
|
169
|
+
: ([[], []] as [Glyph[], Word[]]),
|
|
170
|
+
at = tokens(a),
|
|
171
|
+
bt = tokens(b);
|
|
172
|
+
const removed: number[] = [],
|
|
173
|
+
added: number[] = [],
|
|
174
|
+
movedA: number[] = [],
|
|
175
|
+
movedB: number[] = [];
|
|
176
|
+
const indices = (t: ReturnType<typeof tokens>, i: number, j: number) =>
|
|
177
|
+
t
|
|
178
|
+
.slice(i, j)
|
|
179
|
+
.flatMap((t) =>
|
|
180
|
+
Array.from({ length: t.end - t.start }, (_, k) => t.start + k),
|
|
181
|
+
);
|
|
182
|
+
for (const [tag, i, j, x, y] of opcodes(
|
|
183
|
+
at.map((t) => t.text),
|
|
184
|
+
bt.map((t) => t.text),
|
|
185
|
+
)) {
|
|
186
|
+
const ai = indices(at, i, j),
|
|
187
|
+
bi = indices(bt, x, y);
|
|
188
|
+
if (tag !== "equal") {
|
|
189
|
+
removed.push(...ai);
|
|
190
|
+
added.push(...bi);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
ai.forEach((u, index) => {
|
|
194
|
+
const v = bi[index],
|
|
195
|
+
old = a[u],
|
|
196
|
+
next = b[v],
|
|
197
|
+
p = left[old.pair],
|
|
198
|
+
q = right[next.pair],
|
|
199
|
+
r = old.rect,
|
|
200
|
+
s = next.rect;
|
|
201
|
+
if (
|
|
202
|
+
old.pair !== next.pair ||
|
|
203
|
+
[
|
|
204
|
+
r[0] - p.clip[0] - s[0] + q.clip[0],
|
|
205
|
+
r[1] - p.clip[1] - s[1] + q.clip[1],
|
|
206
|
+
r[2] - r[0] - s[2] + s[0],
|
|
207
|
+
r[3] - r[1] - s[3] + s[1],
|
|
208
|
+
].some((d) => Math.abs(d) > 0.65)
|
|
209
|
+
) {
|
|
210
|
+
movedA.push(u);
|
|
211
|
+
movedB.push(v);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const changed = Boolean(removed.length || added.length),
|
|
216
|
+
oldWords = joined(aw),
|
|
217
|
+
newWords = joined(bw),
|
|
218
|
+
edits = changed
|
|
219
|
+
? opcodes(oldWords, newWords)
|
|
220
|
+
.filter((o) => o[0] !== "equal")
|
|
221
|
+
.map(([, i, j, x, y]) => {
|
|
222
|
+
const old = oldWords.slice(i, j).join(" "),
|
|
223
|
+
next = newWords.slice(x, y).join(" ");
|
|
224
|
+
return {
|
|
225
|
+
old: old.slice(0, 180),
|
|
226
|
+
new: next.slice(0, 180),
|
|
227
|
+
truncated: old.length > 180 || next.length > 180,
|
|
228
|
+
};
|
|
229
|
+
})
|
|
230
|
+
: [];
|
|
231
|
+
return {
|
|
232
|
+
old_marks: marks(
|
|
233
|
+
a,
|
|
234
|
+
changed ? removed : movedA,
|
|
235
|
+
aw,
|
|
236
|
+
changed ? "removed" : "moved",
|
|
237
|
+
),
|
|
238
|
+
new_marks: marks(
|
|
239
|
+
b,
|
|
240
|
+
changed ? added : movedB,
|
|
241
|
+
bw,
|
|
242
|
+
changed ? "added" : "moved",
|
|
243
|
+
),
|
|
244
|
+
text_changed: changed,
|
|
245
|
+
moved: Boolean(movedA.length || movedB.length),
|
|
246
|
+
text_available: Boolean(a.length || b.length),
|
|
247
|
+
edits,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
export function pixelDifference(old: Raster | null, next: Raster | null) {
|
|
251
|
+
const width = Math.max(old?.width ?? 0, next?.width ?? 0),
|
|
252
|
+
height = Math.max(old?.height ?? 0, next?.height ?? 0);
|
|
253
|
+
let count = 0;
|
|
254
|
+
for (let y = 0; y < height; y++)
|
|
255
|
+
for (let x = 0; x < width; x++) {
|
|
256
|
+
let difference = 0;
|
|
257
|
+
for (let c = 0; c < 3; c++) {
|
|
258
|
+
const value = (image: Raster | null) =>
|
|
259
|
+
image && x < image.width && y < image.height
|
|
260
|
+
? image.pixels[
|
|
261
|
+
y * image.stride +
|
|
262
|
+
x * image.channels +
|
|
263
|
+
Math.min(c, image.channels - 1)
|
|
264
|
+
]
|
|
265
|
+
: 255;
|
|
266
|
+
difference +=
|
|
267
|
+
Math.abs(value(old) - value(next)) * [0.299, 0.587, 0.114][c];
|
|
268
|
+
}
|
|
269
|
+
if (difference >= 24) count++;
|
|
270
|
+
}
|
|
271
|
+
return count;
|
|
272
|
+
}
|