@inkbar/core 0.5.0 → 0.7.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/README.md +111 -1
- package/dist/dom.d.mts +597 -40
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +2154 -436
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +16 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +181 -25
- package/dist/index.mjs.map +1 -1
- package/dist/{signatures-BwaXi7C8.d.mts → signatures-CalOktkf.d.mts} +148 -13
- package/dist/signatures-CalOktkf.d.mts.map +1 -0
- package/dist/{signatures-CMapTKrC.mjs → signatures-Do_l2IlM.mjs} +882 -56
- package/dist/signatures-Do_l2IlM.mjs.map +1 -0
- package/package.json +4 -3
- package/dist/signatures-BwaXi7C8.d.mts.map +0 -1
- package/dist/signatures-CMapTKrC.mjs.map +0 -1
|
@@ -96,6 +96,15 @@ function quadFromRect(r) {
|
|
|
96
96
|
}
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
+
/** One quad covering `r`, in the corner order of `quadFromRect`. */
|
|
100
|
+
function quadsFromRect(r) {
|
|
101
|
+
return [quadFromRect(r)];
|
|
102
|
+
}
|
|
103
|
+
/** The bounding box of every corner of `quads`. */
|
|
104
|
+
function rectFromQuads(quads) {
|
|
105
|
+
if (quads.length === 0) throw new RangeError("rectFromQuads needs at least one quad");
|
|
106
|
+
return unionRects(quads.map(rectFromQuad));
|
|
107
|
+
}
|
|
99
108
|
function unionRects(rects) {
|
|
100
109
|
if (rects.length === 0) throw new RangeError("unionRects needs at least one rect");
|
|
101
110
|
let x1 = Infinity;
|
|
@@ -251,6 +260,102 @@ function readonly(w) {
|
|
|
251
260
|
};
|
|
252
261
|
}
|
|
253
262
|
//#endregion
|
|
263
|
+
//#region src/model/records.ts
|
|
264
|
+
/**
|
|
265
|
+
* A keyed list of plain records. Records are stored as frozen copies, so a change's `before` and `after` can be
|
|
266
|
+
* kept as history entries without copying.
|
|
267
|
+
*/
|
|
268
|
+
function createRecordStore(key, opts = {}) {
|
|
269
|
+
const list = writable((opts.initial ?? []).map(frozen));
|
|
270
|
+
const emitter = new Emitter();
|
|
271
|
+
let destroyed = false;
|
|
272
|
+
function commit(next, change) {
|
|
273
|
+
if (destroyed) return;
|
|
274
|
+
list.set(next);
|
|
275
|
+
emitter.emit("change", change);
|
|
276
|
+
opts.onChange?.(next, change);
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
key,
|
|
280
|
+
list: readonly(list),
|
|
281
|
+
get: (id) => list.get().find((r) => r.id === id),
|
|
282
|
+
add(record) {
|
|
283
|
+
if (list.get().some((r) => r.id === record.id)) throw new InkError("DuplicateId", `A record with id ${record.id} already exists`, {
|
|
284
|
+
key,
|
|
285
|
+
id: record.id
|
|
286
|
+
});
|
|
287
|
+
const stored = frozen(record);
|
|
288
|
+
commit([...list.get(), stored], {
|
|
289
|
+
kind: "add",
|
|
290
|
+
record: stored
|
|
291
|
+
});
|
|
292
|
+
return stored;
|
|
293
|
+
},
|
|
294
|
+
update(id, patch) {
|
|
295
|
+
const before = list.get().find((r) => r.id === id);
|
|
296
|
+
if (destroyed || !before) return void 0;
|
|
297
|
+
const after = Object.freeze({
|
|
298
|
+
...before,
|
|
299
|
+
...patch,
|
|
300
|
+
id: before.id
|
|
301
|
+
});
|
|
302
|
+
commit(list.get().map((r) => r.id === id ? after : r), {
|
|
303
|
+
kind: "update",
|
|
304
|
+
id,
|
|
305
|
+
before,
|
|
306
|
+
after
|
|
307
|
+
});
|
|
308
|
+
return after;
|
|
309
|
+
},
|
|
310
|
+
remove(id) {
|
|
311
|
+
const record = list.get().find((r) => r.id === id);
|
|
312
|
+
if (destroyed || !record) return void 0;
|
|
313
|
+
commit(list.get().filter((r) => r.id !== id), {
|
|
314
|
+
kind: "remove",
|
|
315
|
+
record
|
|
316
|
+
});
|
|
317
|
+
return record;
|
|
318
|
+
},
|
|
319
|
+
replaceAll(incoming, reason) {
|
|
320
|
+
const prev = new Map(list.get().map((r) => [r.id, r]));
|
|
321
|
+
const added = [];
|
|
322
|
+
const updated = [];
|
|
323
|
+
const next = incoming.map((r) => {
|
|
324
|
+
const old = prev.get(r.id);
|
|
325
|
+
if (!old) {
|
|
326
|
+
added.push(r.id);
|
|
327
|
+
return frozen(r);
|
|
328
|
+
}
|
|
329
|
+
if (shallowEqual(old, r)) return old;
|
|
330
|
+
updated.push(r.id);
|
|
331
|
+
return frozen(r);
|
|
332
|
+
});
|
|
333
|
+
const kept = new Set(next.map((r) => r.id));
|
|
334
|
+
commit(next, {
|
|
335
|
+
kind: "replace",
|
|
336
|
+
reason,
|
|
337
|
+
added,
|
|
338
|
+
updated,
|
|
339
|
+
removed: [...prev.keys()].filter((id) => !kept.has(id))
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
on: (event, fn) => emitter.on(event, fn),
|
|
343
|
+
destroy() {
|
|
344
|
+
destroyed = true;
|
|
345
|
+
emitter.clear();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/** Freezes a copy, so the caller's object stays theirs; an already frozen record is kept. */
|
|
350
|
+
function frozen(record) {
|
|
351
|
+
return Object.isFrozen(record) ? record : Object.freeze({ ...record });
|
|
352
|
+
}
|
|
353
|
+
function shallowEqual(a, b) {
|
|
354
|
+
const ka = Object.keys(a);
|
|
355
|
+
const kb = Object.keys(b);
|
|
356
|
+
return ka.length === kb.length && ka.every((k) => Object.hasOwn(b, k) && a[k] === b[k]);
|
|
357
|
+
}
|
|
358
|
+
//#endregion
|
|
254
359
|
//#region src/model/selection.ts
|
|
255
360
|
const union = (a, b) => {
|
|
256
361
|
const x = Math.min(a.x, b.x);
|
|
@@ -305,6 +410,167 @@ function mergeLineRects(rects, tolerance = 2, gap) {
|
|
|
305
410
|
return runs.map(quadFromRect);
|
|
306
411
|
}
|
|
307
412
|
//#endregion
|
|
413
|
+
//#region src/pdfjs/glyphs.ts
|
|
414
|
+
const IDENTITY = [
|
|
415
|
+
1,
|
|
416
|
+
0,
|
|
417
|
+
0,
|
|
418
|
+
1,
|
|
419
|
+
0,
|
|
420
|
+
0
|
|
421
|
+
];
|
|
422
|
+
const FONT_IDENTITY = [
|
|
423
|
+
.001,
|
|
424
|
+
0,
|
|
425
|
+
0,
|
|
426
|
+
.001,
|
|
427
|
+
0,
|
|
428
|
+
0
|
|
429
|
+
];
|
|
430
|
+
/**
|
|
431
|
+
* The glyphs a page draws, in content order, with where each starts and where its own width ends
|
|
432
|
+
* in user space. The pen moves the way PDF.js's canvas renderer moves it (`showText` and
|
|
433
|
+
* `showType3Text`): text matrix and moves, the current transformation matrix, font size and font
|
|
434
|
+
* matrix, character and word spacing, horizontal scaling, and `TJ` kerning. Glyphs in vertical
|
|
435
|
+
* fonts are left out. Annotation appearances are not read.
|
|
436
|
+
*/
|
|
437
|
+
async function pageGlyphs(page, pdfjs) {
|
|
438
|
+
const ops = pdfjs.OPS;
|
|
439
|
+
const list = await page.getOperatorList({ annotationMode: pdfjs.AnnotationMode.DISABLE });
|
|
440
|
+
const out = [];
|
|
441
|
+
let s = {
|
|
442
|
+
ctm: IDENTITY,
|
|
443
|
+
textMatrix: IDENTITY,
|
|
444
|
+
x: 0,
|
|
445
|
+
y: 0,
|
|
446
|
+
lineX: 0,
|
|
447
|
+
lineY: 0,
|
|
448
|
+
font: {},
|
|
449
|
+
fontSize: 0,
|
|
450
|
+
fontDirection: 1,
|
|
451
|
+
charSpacing: 0,
|
|
452
|
+
wordSpacing: 0,
|
|
453
|
+
hScale: 1,
|
|
454
|
+
leading: 0,
|
|
455
|
+
rise: 0
|
|
456
|
+
};
|
|
457
|
+
const saved = [];
|
|
458
|
+
const save = () => {
|
|
459
|
+
saved.push(s);
|
|
460
|
+
s = { ...s };
|
|
461
|
+
};
|
|
462
|
+
const restore = () => {
|
|
463
|
+
s = saved.pop() ?? s;
|
|
464
|
+
};
|
|
465
|
+
const setFont = (name, size) => {
|
|
466
|
+
let font = {};
|
|
467
|
+
try {
|
|
468
|
+
font = page.commonObjs.get(String(name)) ?? {};
|
|
469
|
+
} catch {}
|
|
470
|
+
const n = Number(size);
|
|
471
|
+
s.font = font;
|
|
472
|
+
s.fontDirection = n < 0 ? -1 : 1;
|
|
473
|
+
s.fontSize = Math.abs(n);
|
|
474
|
+
};
|
|
475
|
+
const moveText = (x, y) => {
|
|
476
|
+
s.x = s.lineX += x;
|
|
477
|
+
s.y = s.lineY += y;
|
|
478
|
+
};
|
|
479
|
+
const show = (glyphs) => {
|
|
480
|
+
const { font, fontSize, fontDirection, charSpacing, wordSpacing } = s;
|
|
481
|
+
if (fontSize === 0) return;
|
|
482
|
+
const fontMatrix = font.fontMatrix ?? FONT_IDENTITY;
|
|
483
|
+
const toUser = multiplyMatrix(s.textMatrix, s.ctm);
|
|
484
|
+
const at = (x) => applyMatrix(toUser, {
|
|
485
|
+
x: s.x + x,
|
|
486
|
+
y: s.y + s.rise
|
|
487
|
+
});
|
|
488
|
+
const hScale = s.hScale * fontDirection;
|
|
489
|
+
let x = 0;
|
|
490
|
+
for (const g of glyphs) {
|
|
491
|
+
if (typeof g === "number") {
|
|
492
|
+
x += -g * fontSize / 1e3;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (!g) continue;
|
|
496
|
+
const spacing = charSpacing + (g.isSpace ? wordSpacing : 0);
|
|
497
|
+
const width = font.isType3Font ? (g.width * fontMatrix[0] + fontMatrix[4]) * fontSize : g.width * fontSize * fontMatrix[0];
|
|
498
|
+
if (!font.vertical) out.push({
|
|
499
|
+
text: g.unicode,
|
|
500
|
+
start: at(x * hScale),
|
|
501
|
+
end: at((x + width) * hScale)
|
|
502
|
+
});
|
|
503
|
+
x += width + spacing * (font.isType3Font ? 1 : fontDirection);
|
|
504
|
+
}
|
|
505
|
+
s.x += x * hScale;
|
|
506
|
+
};
|
|
507
|
+
for (let i = 0; i < list.fnArray.length; i++) {
|
|
508
|
+
const args = list.argsArray[i];
|
|
509
|
+
switch (list.fnArray[i]) {
|
|
510
|
+
case ops.save:
|
|
511
|
+
save();
|
|
512
|
+
break;
|
|
513
|
+
case ops.restore:
|
|
514
|
+
restore();
|
|
515
|
+
break;
|
|
516
|
+
case ops.paintFormXObjectBegin:
|
|
517
|
+
save();
|
|
518
|
+
if (args?.[0]) s.ctm = multiplyMatrix(args[0], s.ctm);
|
|
519
|
+
break;
|
|
520
|
+
case ops.paintFormXObjectEnd:
|
|
521
|
+
restore();
|
|
522
|
+
break;
|
|
523
|
+
case ops.transform:
|
|
524
|
+
s.ctm = multiplyMatrix(args, s.ctm);
|
|
525
|
+
break;
|
|
526
|
+
case ops.beginText:
|
|
527
|
+
s.textMatrix = IDENTITY;
|
|
528
|
+
s.x = s.lineX = s.y = s.lineY = 0;
|
|
529
|
+
break;
|
|
530
|
+
case ops.setTextMatrix:
|
|
531
|
+
s.textMatrix = args[0];
|
|
532
|
+
s.x = s.lineX = s.y = s.lineY = 0;
|
|
533
|
+
break;
|
|
534
|
+
case ops.moveText:
|
|
535
|
+
moveText(Number(args[0]), Number(args[1]));
|
|
536
|
+
break;
|
|
537
|
+
case ops.setLeadingMoveText:
|
|
538
|
+
s.leading = Number(args[1]);
|
|
539
|
+
moveText(Number(args[0]), Number(args[1]));
|
|
540
|
+
break;
|
|
541
|
+
case ops.setLeading:
|
|
542
|
+
s.leading = -Number(args[0]);
|
|
543
|
+
break;
|
|
544
|
+
case ops.nextLine:
|
|
545
|
+
moveText(0, s.leading);
|
|
546
|
+
break;
|
|
547
|
+
case ops.setFont:
|
|
548
|
+
setFont(args[0], args[1]);
|
|
549
|
+
break;
|
|
550
|
+
case ops.setGState:
|
|
551
|
+
for (const [key, value] of args[0]) if (key === "Font") {
|
|
552
|
+
const [name, size] = value;
|
|
553
|
+
setFont(name, size);
|
|
554
|
+
}
|
|
555
|
+
break;
|
|
556
|
+
case ops.setCharSpacing:
|
|
557
|
+
s.charSpacing = Number(args[0]);
|
|
558
|
+
break;
|
|
559
|
+
case ops.setWordSpacing:
|
|
560
|
+
s.wordSpacing = Number(args[0]);
|
|
561
|
+
break;
|
|
562
|
+
case ops.setHScale:
|
|
563
|
+
s.hScale = Number(args[0]) / 100;
|
|
564
|
+
break;
|
|
565
|
+
case ops.setTextRise:
|
|
566
|
+
s.rise = Number(args[0]);
|
|
567
|
+
break;
|
|
568
|
+
case ops.showText: show(args[0]);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
//#endregion
|
|
308
574
|
//#region src/pdfjs/load.ts
|
|
309
575
|
const REQUIRED = [
|
|
310
576
|
"worker",
|
|
@@ -1792,6 +2058,7 @@ function isProduction() {
|
|
|
1792
2058
|
}
|
|
1793
2059
|
//#endregion
|
|
1794
2060
|
//#region src/model/annotations/history.ts
|
|
2061
|
+
const noop = () => {};
|
|
1795
2062
|
/**
|
|
1796
2063
|
* Snapshot-based undo and redo over an uncontrolled store.
|
|
1797
2064
|
*
|
|
@@ -1811,6 +2078,8 @@ function createHistory(store, opts = {}) {
|
|
|
1811
2078
|
batch(fn) {
|
|
1812
2079
|
fn();
|
|
1813
2080
|
},
|
|
2081
|
+
track: () => noop,
|
|
2082
|
+
react: () => noop,
|
|
1814
2083
|
destroy() {}
|
|
1815
2084
|
};
|
|
1816
2085
|
const limit = opts.limit ?? 200;
|
|
@@ -1818,14 +2087,84 @@ function createHistory(store, opts = {}) {
|
|
|
1818
2087
|
const redoStack = [];
|
|
1819
2088
|
let previous = store.list.get();
|
|
1820
2089
|
let replaying = false;
|
|
1821
|
-
/**
|
|
2090
|
+
/**
|
|
2091
|
+
* Open step depth (batches, and an edit while its reactions run); while above zero, changes
|
|
2092
|
+
* update the baselines but push no entry.
|
|
2093
|
+
*/
|
|
1822
2094
|
let depth = 0;
|
|
1823
|
-
/** The
|
|
1824
|
-
let
|
|
2095
|
+
/** The lists when the outermost step opened (or were restarted by a replace). */
|
|
2096
|
+
let start = null;
|
|
2097
|
+
/** Each tracked store with its list as of the last change history saw. */
|
|
2098
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
2099
|
+
const reactions = /* @__PURE__ */ new Set();
|
|
1825
2100
|
const sync = () => {
|
|
1826
2101
|
canUndo.set(undoStack.length > 0);
|
|
1827
2102
|
canRedo.set(redoStack.length > 0);
|
|
1828
2103
|
};
|
|
2104
|
+
const push = (entry) => {
|
|
2105
|
+
undoStack.push(entry);
|
|
2106
|
+
if (undoStack.length > limit) undoStack.shift();
|
|
2107
|
+
redoStack.length = 0;
|
|
2108
|
+
sync();
|
|
2109
|
+
};
|
|
2110
|
+
/**
|
|
2111
|
+
* Forgets `recordStore`'s records in every entry, so undo and redo leave it alone. An entry
|
|
2112
|
+
* left with nothing to replay (no annotation change, no other store) is dropped.
|
|
2113
|
+
*/
|
|
2114
|
+
const dropEntries = (recordStore) => {
|
|
2115
|
+
for (const stack of [undoStack, redoStack]) {
|
|
2116
|
+
const kept = stack.filter((entry) => {
|
|
2117
|
+
if (!entry.records.delete(recordStore)) return true;
|
|
2118
|
+
const { before, after } = entry.annotations;
|
|
2119
|
+
return entry.records.size > 0 || !sameList(before, after);
|
|
2120
|
+
});
|
|
2121
|
+
stack.splice(0, stack.length, ...kept);
|
|
2122
|
+
}
|
|
2123
|
+
sync();
|
|
2124
|
+
};
|
|
2125
|
+
const resetStacks = () => {
|
|
2126
|
+
undoStack.length = 0;
|
|
2127
|
+
redoStack.length = 0;
|
|
2128
|
+
sync();
|
|
2129
|
+
};
|
|
2130
|
+
/** Opens the outermost step from `annotations` and every tracked store's baseline. */
|
|
2131
|
+
const open = (annotations) => {
|
|
2132
|
+
const records = /* @__PURE__ */ new Map();
|
|
2133
|
+
for (const [s, t] of tracked) records.set(s, t.previous);
|
|
2134
|
+
start = {
|
|
2135
|
+
annotations,
|
|
2136
|
+
records
|
|
2137
|
+
};
|
|
2138
|
+
};
|
|
2139
|
+
/**
|
|
2140
|
+
* Closes the outermost step: one entry for everything that changed since it opened. A step
|
|
2141
|
+
* whose lists end where they began (the same objects in the same order) records nothing.
|
|
2142
|
+
*/
|
|
2143
|
+
const close = () => {
|
|
2144
|
+
const from = start;
|
|
2145
|
+
start = null;
|
|
2146
|
+
if (!from) return;
|
|
2147
|
+
const annotations = {
|
|
2148
|
+
before: from.annotations,
|
|
2149
|
+
after: store.list.get()
|
|
2150
|
+
};
|
|
2151
|
+
previous = annotations.after;
|
|
2152
|
+
const records = /* @__PURE__ */ new Map();
|
|
2153
|
+
for (const [s, t] of tracked) {
|
|
2154
|
+
const before = from.records.get(s) ?? t.previous;
|
|
2155
|
+
const after = s.list.get();
|
|
2156
|
+
t.previous = after;
|
|
2157
|
+
if (!sameList(before, after)) records.set(s, {
|
|
2158
|
+
before,
|
|
2159
|
+
after
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
if (records.size === 0 && sameList(annotations.before, annotations.after)) return;
|
|
2163
|
+
push({
|
|
2164
|
+
annotations,
|
|
2165
|
+
records
|
|
2166
|
+
});
|
|
2167
|
+
};
|
|
1829
2168
|
let destroyed = false;
|
|
1830
2169
|
const unsubscribe = store.on("change", (change) => {
|
|
1831
2170
|
const current = store.list.get();
|
|
@@ -1838,39 +2177,40 @@ function createHistory(store, opts = {}) {
|
|
|
1838
2177
|
const f = change.fields.get(a.id);
|
|
1839
2178
|
return f && withEmbeddedFields(a, f) || a;
|
|
1840
2179
|
});
|
|
1841
|
-
for (const
|
|
1842
|
-
|
|
1843
|
-
|
|
2180
|
+
for (const { annotations } of [...undoStack, ...redoStack]) {
|
|
2181
|
+
annotations.before = fill(annotations.before);
|
|
2182
|
+
annotations.after = fill(annotations.after);
|
|
1844
2183
|
}
|
|
1845
|
-
if (
|
|
2184
|
+
if (start) start.annotations = fill(start.annotations);
|
|
1846
2185
|
previous = current;
|
|
1847
2186
|
return;
|
|
1848
2187
|
}
|
|
1849
2188
|
if (change.kind === "replace") {
|
|
1850
|
-
undoStack.length = 0;
|
|
1851
|
-
redoStack.length = 0;
|
|
1852
2189
|
previous = current;
|
|
1853
|
-
if (depth > 0)
|
|
1854
|
-
|
|
2190
|
+
if (depth > 0) open(current);
|
|
2191
|
+
resetStacks();
|
|
1855
2192
|
return;
|
|
1856
2193
|
}
|
|
1857
2194
|
if (depth > 0) {
|
|
1858
2195
|
previous = current;
|
|
2196
|
+
for (const fn of [...reactions]) fn(change);
|
|
1859
2197
|
return;
|
|
1860
2198
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
sync();
|
|
2199
|
+
open(previous);
|
|
2200
|
+
depth++;
|
|
2201
|
+
try {
|
|
2202
|
+
for (const fn of [...reactions]) fn(change);
|
|
2203
|
+
} finally {
|
|
2204
|
+
if (--depth === 0 && !destroyed) close();
|
|
2205
|
+
}
|
|
1869
2206
|
});
|
|
1870
|
-
|
|
2207
|
+
/** Replays a step's lists under the `replaying` guard, skipping stores no longer tracked. */
|
|
2208
|
+
const apply = (entry, side) => {
|
|
1871
2209
|
replaying = true;
|
|
1872
2210
|
try {
|
|
1873
|
-
|
|
2211
|
+
const { annotations } = entry;
|
|
2212
|
+
if (!sameList(annotations.before, annotations.after)) store.replaceAll([...annotations[side]], "history");
|
|
2213
|
+
for (const [s, snapshot] of entry.records) if (tracked.has(s)) s.replaceAll([...snapshot[side]], "history");
|
|
1874
2214
|
} finally {
|
|
1875
2215
|
replaying = false;
|
|
1876
2216
|
}
|
|
@@ -1882,14 +2222,14 @@ function createHistory(store, opts = {}) {
|
|
|
1882
2222
|
const entry = undoStack.pop();
|
|
1883
2223
|
if (destroyed || !entry) return;
|
|
1884
2224
|
redoStack.push(entry);
|
|
1885
|
-
apply(entry
|
|
2225
|
+
apply(entry, "before");
|
|
1886
2226
|
sync();
|
|
1887
2227
|
},
|
|
1888
2228
|
redo() {
|
|
1889
2229
|
const entry = redoStack.pop();
|
|
1890
2230
|
if (destroyed || !entry) return;
|
|
1891
2231
|
undoStack.push(entry);
|
|
1892
|
-
apply(entry
|
|
2232
|
+
apply(entry, "after");
|
|
1893
2233
|
sync();
|
|
1894
2234
|
},
|
|
1895
2235
|
batch(fn) {
|
|
@@ -1897,45 +2237,78 @@ function createHistory(store, opts = {}) {
|
|
|
1897
2237
|
fn();
|
|
1898
2238
|
return;
|
|
1899
2239
|
}
|
|
1900
|
-
if (depth++ === 0)
|
|
2240
|
+
if (depth++ === 0) open(store.list.get());
|
|
1901
2241
|
try {
|
|
1902
2242
|
const result = fn();
|
|
1903
2243
|
if (result !== null && typeof result === "object" && typeof result.then === "function" && !isProduction()) console.warn("[inkbar] history.batch(fn): fn returned a promise. Changes after its first await are not part of the batch.");
|
|
1904
2244
|
} finally {
|
|
1905
|
-
if (--depth === 0 && !destroyed)
|
|
1906
|
-
const before = batchStart;
|
|
1907
|
-
batchStart = null;
|
|
1908
|
-
const after = store.list.get();
|
|
1909
|
-
if (before && !sameAnnotations(before, after)) {
|
|
1910
|
-
undoStack.push({
|
|
1911
|
-
before,
|
|
1912
|
-
after
|
|
1913
|
-
});
|
|
1914
|
-
if (undoStack.length > limit) undoStack.shift();
|
|
1915
|
-
redoStack.length = 0;
|
|
1916
|
-
sync();
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
2245
|
+
if (--depth === 0 && !destroyed) close();
|
|
1919
2246
|
}
|
|
1920
2247
|
},
|
|
2248
|
+
track(recordStore) {
|
|
2249
|
+
if (destroyed || tracked.has(recordStore)) return noop;
|
|
2250
|
+
const unsubscribeStore = recordStore.on("change", (change) => {
|
|
2251
|
+
const t = tracked.get(recordStore);
|
|
2252
|
+
if (!t) return;
|
|
2253
|
+
const before = t.previous;
|
|
2254
|
+
const current = recordStore.list.get();
|
|
2255
|
+
t.previous = current;
|
|
2256
|
+
if (replaying || change.kind === "replace" && change.reason === "history") return;
|
|
2257
|
+
if (change.kind === "replace") {
|
|
2258
|
+
if (depth > 0 && start) start.records.set(recordStore, current);
|
|
2259
|
+
dropEntries(recordStore);
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
if (depth > 0) return;
|
|
2263
|
+
push({
|
|
2264
|
+
annotations: {
|
|
2265
|
+
before: previous,
|
|
2266
|
+
after: previous
|
|
2267
|
+
},
|
|
2268
|
+
records: /* @__PURE__ */ new Map([[recordStore, {
|
|
2269
|
+
before,
|
|
2270
|
+
after: current
|
|
2271
|
+
}]])
|
|
2272
|
+
});
|
|
2273
|
+
});
|
|
2274
|
+
tracked.set(recordStore, {
|
|
2275
|
+
previous: recordStore.list.get(),
|
|
2276
|
+
unsubscribe: unsubscribeStore
|
|
2277
|
+
});
|
|
2278
|
+
if (start) start.records.set(recordStore, recordStore.list.get());
|
|
2279
|
+
return () => {
|
|
2280
|
+
if (tracked.get(recordStore)?.unsubscribe !== unsubscribeStore) return;
|
|
2281
|
+
unsubscribeStore();
|
|
2282
|
+
tracked.delete(recordStore);
|
|
2283
|
+
start?.records.delete(recordStore);
|
|
2284
|
+
};
|
|
2285
|
+
},
|
|
2286
|
+
react(fn) {
|
|
2287
|
+
if (destroyed) return noop;
|
|
2288
|
+
const run = (edit) => fn(edit);
|
|
2289
|
+
reactions.add(run);
|
|
2290
|
+
return () => {
|
|
2291
|
+
reactions.delete(run);
|
|
2292
|
+
};
|
|
2293
|
+
},
|
|
1921
2294
|
destroy() {
|
|
1922
2295
|
destroyed = true;
|
|
1923
|
-
|
|
2296
|
+
start = null;
|
|
1924
2297
|
unsubscribe();
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2298
|
+
for (const t of tracked.values()) t.unsubscribe();
|
|
2299
|
+
tracked.clear();
|
|
2300
|
+
reactions.clear();
|
|
2301
|
+
resetStacks();
|
|
1928
2302
|
},
|
|
1929
2303
|
clear() {
|
|
1930
|
-
undoStack.length = 0;
|
|
1931
|
-
redoStack.length = 0;
|
|
1932
2304
|
previous = store.list.get();
|
|
1933
|
-
|
|
2305
|
+
for (const [s, t] of tracked) t.previous = s.list.get();
|
|
2306
|
+
resetStacks();
|
|
1934
2307
|
}
|
|
1935
2308
|
};
|
|
1936
2309
|
}
|
|
1937
|
-
/** The same
|
|
1938
|
-
function
|
|
2310
|
+
/** The same objects in the same order. */
|
|
2311
|
+
function sameList(a, b) {
|
|
1939
2312
|
return a === b || a.length === b.length && a.every((x, i) => x === b[i]);
|
|
1940
2313
|
}
|
|
1941
2314
|
//#endregion
|
|
@@ -2382,6 +2755,7 @@ function createAnnotationStore(opts) {
|
|
|
2382
2755
|
console.warn(`[inkbar] ControlledMutationIgnored: the host did not apply a ${entry.change.kind} for annotation ${entry.id}. In controlled mode, pass the list from onChange back into the store.`);
|
|
2383
2756
|
}
|
|
2384
2757
|
function commit(next, change) {
|
|
2758
|
+
if (change.kind !== "replace") emitter.emit("edit", change);
|
|
2385
2759
|
if (controlled) {
|
|
2386
2760
|
if (change.kind !== "replace") {
|
|
2387
2761
|
const key = changeKey(change);
|
|
@@ -2480,6 +2854,7 @@ function createAnnotationStore(opts) {
|
|
|
2480
2854
|
after
|
|
2481
2855
|
};
|
|
2482
2856
|
if (controlled && before.source === "pdf") {
|
|
2857
|
+
emitter.emit("edit", change);
|
|
2483
2858
|
list.set(swap(list.get()));
|
|
2484
2859
|
if (proposed) proposed = next;
|
|
2485
2860
|
emitter.emit("change", change);
|
|
@@ -2578,6 +2953,30 @@ function createAnnotationStore(opts) {
|
|
|
2578
2953
|
};
|
|
2579
2954
|
}
|
|
2580
2955
|
//#endregion
|
|
2956
|
+
//#region src/model/measure-char.ts
|
|
2957
|
+
/**
|
|
2958
|
+
* Measures characters on an offscreen canvas in the font family PDF.js reports for a run, the
|
|
2959
|
+
* same family its text layer measures with. Undefined where there is no canvas (Node), so text
|
|
2960
|
+
* items carry no advances there. Widths are cached per family and character.
|
|
2961
|
+
*/
|
|
2962
|
+
function canvasMeasureChar() {
|
|
2963
|
+
if (typeof OffscreenCanvas === "undefined") return void 0;
|
|
2964
|
+
const ctx = new OffscreenCanvas(1, 1).getContext("2d");
|
|
2965
|
+
if (!ctx) return void 0;
|
|
2966
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2967
|
+
return (char, family) => {
|
|
2968
|
+
let widths = cache.get(family);
|
|
2969
|
+
if (!widths) cache.set(family, widths = /* @__PURE__ */ new Map());
|
|
2970
|
+
let w = widths.get(char);
|
|
2971
|
+
if (w === void 0) {
|
|
2972
|
+
ctx.font = `100px ${family}`;
|
|
2973
|
+
w = ctx.measureText(char).width;
|
|
2974
|
+
widths.set(char, w);
|
|
2975
|
+
}
|
|
2976
|
+
return w;
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
//#endregion
|
|
2581
2980
|
//#region src/model/page-cache.ts
|
|
2582
2981
|
function createPageCache(fetch) {
|
|
2583
2982
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -2618,6 +3017,235 @@ function pageInfoFromProxy(page, index) {
|
|
|
2618
3017
|
function normaliseRotation(rotate) {
|
|
2619
3018
|
return (Math.round(rotate / 90) * 90 % 360 + 360) % 360;
|
|
2620
3019
|
}
|
|
3020
|
+
/** How far off the baseline, as a fraction of the font size, an item can sit and still share the line. */
|
|
3021
|
+
const SAME_LINE = .5;
|
|
3022
|
+
const FOLD = {};
|
|
3023
|
+
for (const c of "‘’‚‛") FOLD[c] = "'";
|
|
3024
|
+
for (const c of "“”„‟") FOLD[c] = "\"";
|
|
3025
|
+
for (const c of "‐‑‒–—―") FOLD[c] = "-";
|
|
3026
|
+
/** A line ending in one of these joins to the next line's word: hyphen, U+2010, U+2011, soft hyphen. */
|
|
3027
|
+
const JOINING_HYPHENS = /* @__PURE__ */ new Set([
|
|
3028
|
+
"-",
|
|
3029
|
+
"‐",
|
|
3030
|
+
"‑",
|
|
3031
|
+
""
|
|
3032
|
+
]);
|
|
3033
|
+
const MARKS = /\p{M}/gu;
|
|
3034
|
+
const WHITESPACE = /^\s$/u;
|
|
3035
|
+
/** Sticky: set `lastIndex` to test the code point at that index without slicing. */
|
|
3036
|
+
const LETTER_AT = /\p{L}/uy;
|
|
3037
|
+
/** One code point to its normalised form: NFKD, marks stripped, quotes and dashes folded. */
|
|
3038
|
+
function fold(ch) {
|
|
3039
|
+
const out = ch.normalize("NFKD").replace(MARKS, "");
|
|
3040
|
+
let s = "";
|
|
3041
|
+
for (const c of out) s += FOLD[c] ?? c;
|
|
3042
|
+
return s;
|
|
3043
|
+
}
|
|
3044
|
+
function letterAt(s, i) {
|
|
3045
|
+
LETTER_AT.lastIndex = i;
|
|
3046
|
+
return LETTER_AT.test(s);
|
|
3047
|
+
}
|
|
3048
|
+
const isSpace = (ch) => ch !== void 0 && WHITESPACE.test(ch);
|
|
3049
|
+
function lineGap(prev, next) {
|
|
3050
|
+
const dx = prev.quad.p4.x - prev.quad.p3.x;
|
|
3051
|
+
const dy = prev.quad.p4.y - prev.quad.p3.y;
|
|
3052
|
+
const len = Math.hypot(dx, dy);
|
|
3053
|
+
const ux = len === 0 ? 1 : dx / len;
|
|
3054
|
+
const uy = len === 0 ? 0 : dy / len;
|
|
3055
|
+
const rtl = prev.dir === "rtl" && next.dir === "rtl";
|
|
3056
|
+
const from = rtl ? next.quad.p4 : prev.quad.p4;
|
|
3057
|
+
const to = rtl ? prev.quad.p3 : next.quad.p3;
|
|
3058
|
+
const sx = to.x - from.x;
|
|
3059
|
+
const sy = to.y - from.y;
|
|
3060
|
+
const size = Math.max(prev.fontSize, next.fontSize);
|
|
3061
|
+
return {
|
|
3062
|
+
gap: sx * ux + sy * uy,
|
|
3063
|
+
sameLine: Math.abs(sx * -uy + sy * ux) <= SAME_LINE * size
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
/**
|
|
3067
|
+
* Joins a page's items the way the page reads: in content order with nothing between them,
|
|
3068
|
+
* a space after an item that ends its line, and a space where two items on a line sit further
|
|
3069
|
+
* apart than `GAP_SPACE` of the font size. Neither adds a space next to whitespace already there.
|
|
3070
|
+
*/
|
|
3071
|
+
function searchableText(content) {
|
|
3072
|
+
const chars = [];
|
|
3073
|
+
const item = [];
|
|
3074
|
+
const offset = [];
|
|
3075
|
+
const eolSpace = /* @__PURE__ */ new Set();
|
|
3076
|
+
let prev;
|
|
3077
|
+
const space = (eol) => {
|
|
3078
|
+
if (eol) eolSpace.add(chars.length);
|
|
3079
|
+
chars.push(" ");
|
|
3080
|
+
item.push(-1);
|
|
3081
|
+
offset.push(-1);
|
|
3082
|
+
};
|
|
3083
|
+
content.items.forEach((it, i) => {
|
|
3084
|
+
if (it.str === "") {
|
|
3085
|
+
if (it.hasEOL && chars.length > 0 && !isSpace(chars[chars.length - 1])) space(true);
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
if (prev && chars.length > 0 && !isSpace(chars[chars.length - 1])) {
|
|
3089
|
+
const { gap, sameLine } = lineGap(prev, it);
|
|
3090
|
+
const size = Math.max(prev.fontSize, it.fontSize);
|
|
3091
|
+
if (!isSpace(it.str[0]) && (!sameLine || gap > .15 * size)) space(false);
|
|
3092
|
+
}
|
|
3093
|
+
for (let k = 0; k < it.str.length; k++) {
|
|
3094
|
+
chars.push(it.str[k]);
|
|
3095
|
+
item.push(i);
|
|
3096
|
+
offset.push(k);
|
|
3097
|
+
}
|
|
3098
|
+
if (it.hasEOL && !isSpace(it.str[it.str.length - 1])) space(true);
|
|
3099
|
+
prev = it;
|
|
3100
|
+
});
|
|
3101
|
+
const raw = chars.join("");
|
|
3102
|
+
let norm = "";
|
|
3103
|
+
const map = [];
|
|
3104
|
+
for (let r = 0; r < raw.length;) {
|
|
3105
|
+
const ch = String.fromCodePoint(raw.codePointAt(r));
|
|
3106
|
+
if (JOINING_HYPHENS.has(ch) && eolSpace.has(r + 1) && letterAt(raw, r + 2)) {
|
|
3107
|
+
r += 2;
|
|
3108
|
+
continue;
|
|
3109
|
+
}
|
|
3110
|
+
for (const c of fold(ch)) {
|
|
3111
|
+
const out = isSpace(c) ? " " : c;
|
|
3112
|
+
if (out === " " && norm.endsWith(" ")) continue;
|
|
3113
|
+
norm += out;
|
|
3114
|
+
for (let u = 0; u < out.length; u++) map.push(r);
|
|
3115
|
+
}
|
|
3116
|
+
r += ch.length;
|
|
3117
|
+
}
|
|
3118
|
+
map.push(raw.length);
|
|
3119
|
+
const normToRaw = Int32Array.from(map);
|
|
3120
|
+
return {
|
|
3121
|
+
page: content.page,
|
|
3122
|
+
raw,
|
|
3123
|
+
norm,
|
|
3124
|
+
normToRaw,
|
|
3125
|
+
rawToItem: Int32Array.from(item),
|
|
3126
|
+
rawToOffset: Int32Array.from(offset),
|
|
3127
|
+
content
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
/** Folds a query the way `norm` is folded; case is lowered here only when the match ignores it. */
|
|
3131
|
+
function normalizeQuery(query, matchCase) {
|
|
3132
|
+
let out = "";
|
|
3133
|
+
for (const ch of query) out += fold(ch);
|
|
3134
|
+
out = out.replace(/\s+/gu, " ");
|
|
3135
|
+
return matchCase ? out : out.toLowerCase();
|
|
3136
|
+
}
|
|
3137
|
+
/** Whether `query` folds to nothing but whitespace, which a search treats as no query at all. */
|
|
3138
|
+
function isBlankQuery(query) {
|
|
3139
|
+
return normalizeQuery(query, true).trim() === "";
|
|
3140
|
+
}
|
|
3141
|
+
const WORD_BEFORE = /[\p{L}\p{N}_]$/u;
|
|
3142
|
+
const WORD_AFTER = /^[\p{L}\p{N}_]/u;
|
|
3143
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3144
|
+
/** Match ranges `[start, end)` into `norm`, in order and without overlap. */
|
|
3145
|
+
function normRanges(norm, query, matchCase) {
|
|
3146
|
+
const out = [];
|
|
3147
|
+
if (typeof query === "string") {
|
|
3148
|
+
if (isBlankQuery(query)) return out;
|
|
3149
|
+
const needle = normalizeQuery(query, true);
|
|
3150
|
+
const haystack = matchCase ? norm : norm.toLowerCase();
|
|
3151
|
+
const target = matchCase ? needle : needle.toLowerCase();
|
|
3152
|
+
if (haystack.length === norm.length && target.length === needle.length) {
|
|
3153
|
+
for (let at = haystack.indexOf(target); at !== -1; at = haystack.indexOf(target, at + target.length)) out.push([at, at + target.length]);
|
|
3154
|
+
return out;
|
|
3155
|
+
}
|
|
3156
|
+
return regexRanges(norm, new RegExp(escapeRegExp(needle), "giu"));
|
|
3157
|
+
}
|
|
3158
|
+
const flags = query.flags.replace(/[gy]/g, "") + "g";
|
|
3159
|
+
return regexRanges(norm, new RegExp(query.source, flags));
|
|
3160
|
+
}
|
|
3161
|
+
function regexRanges(norm, re) {
|
|
3162
|
+
const out = [];
|
|
3163
|
+
const unicode = re.unicode || re.flags.includes("v");
|
|
3164
|
+
for (let m = re.exec(norm); m; m = re.exec(norm)) {
|
|
3165
|
+
if (m[0] !== "") {
|
|
3166
|
+
out.push([m.index, m.index + m[0].length]);
|
|
3167
|
+
continue;
|
|
3168
|
+
}
|
|
3169
|
+
re.lastIndex = m.index + (unicode && norm.codePointAt(m.index) > 65535 ? 2 : 1);
|
|
3170
|
+
}
|
|
3171
|
+
return out;
|
|
3172
|
+
}
|
|
3173
|
+
const lerp = (a, b, t) => ({
|
|
3174
|
+
x: a.x + (b.x - a.x) * t,
|
|
3175
|
+
y: a.y + (b.y - a.y) * t
|
|
3176
|
+
});
|
|
3177
|
+
/**
|
|
3178
|
+
* The part of an item's quad from character `a` to `b`, cut along its baseline at the item's
|
|
3179
|
+
* `advances`, or at an equal share per character when it has none. With advances it spans every
|
|
3180
|
+
* boundary from `a` to `b` and the end of every glyph in between, so glyphs the pen drew out of
|
|
3181
|
+
* order, or that kerning overlaps with the next one, stay inside it.
|
|
3182
|
+
*/
|
|
3183
|
+
function cutQuad(it, a, b) {
|
|
3184
|
+
const q = it.quad;
|
|
3185
|
+
const len = it.str.length;
|
|
3186
|
+
const span = it.advances?.slice(a, b + 1);
|
|
3187
|
+
const ends = it.glyphEnds?.slice(a, b) ?? [];
|
|
3188
|
+
const t0 = span ? Math.min(...span) : a / len;
|
|
3189
|
+
const t1 = span ? Math.max(...span, ...ends) : b / len;
|
|
3190
|
+
return {
|
|
3191
|
+
p1: lerp(q.p1, q.p2, t0),
|
|
3192
|
+
p2: lerp(q.p1, q.p2, t1),
|
|
3193
|
+
p3: lerp(q.p3, q.p4, t0),
|
|
3194
|
+
p4: lerp(q.p3, q.p4, t1)
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
function quadsFor(st, start, end) {
|
|
3198
|
+
const spans = /* @__PURE__ */ new Map();
|
|
3199
|
+
for (let r = start; r < end; r++) {
|
|
3200
|
+
const i = st.rawToItem[r];
|
|
3201
|
+
if (i < 0) continue;
|
|
3202
|
+
const k = st.rawToOffset[r];
|
|
3203
|
+
const span = spans.get(i);
|
|
3204
|
+
if (span) {
|
|
3205
|
+
span[0] = Math.min(span[0], k);
|
|
3206
|
+
span[1] = Math.max(span[1], k + 1);
|
|
3207
|
+
} else spans.set(i, [k, k + 1]);
|
|
3208
|
+
}
|
|
3209
|
+
const quads = [];
|
|
3210
|
+
let exact = true;
|
|
3211
|
+
for (const [i, [a, b]] of spans) {
|
|
3212
|
+
const it = st.content.items[i];
|
|
3213
|
+
quads.push(cutQuad(it, a, b));
|
|
3214
|
+
if (!it.glyphEnds) exact = false;
|
|
3215
|
+
}
|
|
3216
|
+
return {
|
|
3217
|
+
quads,
|
|
3218
|
+
exact
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
const collapse = (s) => s.replace(/\s+/gu, " ");
|
|
3222
|
+
/** Every hit for `query` on one page, in page order. */
|
|
3223
|
+
function matchPage(st, query, opts) {
|
|
3224
|
+
const hits = [];
|
|
3225
|
+
for (const [ns, ne] of normRanges(st.norm, query, opts.matchCase)) {
|
|
3226
|
+
if (opts.wholeWord) {
|
|
3227
|
+
if (WORD_BEFORE.test(st.norm.slice(Math.max(0, ns - 2), ns))) continue;
|
|
3228
|
+
if (WORD_AFTER.test(st.norm.slice(ne, ne + 2))) continue;
|
|
3229
|
+
}
|
|
3230
|
+
const start = st.normToRaw[ns];
|
|
3231
|
+
const last = st.normToRaw[ne - 1];
|
|
3232
|
+
const end = last + (st.raw.codePointAt(last) > 65535 ? 2 : 1);
|
|
3233
|
+
const { quads, exact } = quadsFor(st, start, end);
|
|
3234
|
+
if (quads.length === 0) continue;
|
|
3235
|
+
hits.push({
|
|
3236
|
+
page: st.page,
|
|
3237
|
+
start,
|
|
3238
|
+
end,
|
|
3239
|
+
text: st.raw.slice(start, end),
|
|
3240
|
+
quads,
|
|
3241
|
+
rect: rectFromQuads(quads),
|
|
3242
|
+
exact,
|
|
3243
|
+
before: collapse(st.raw.slice(Math.max(0, start - 40), start)),
|
|
3244
|
+
after: collapse(st.raw.slice(end, end + 40))
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
return hits;
|
|
3248
|
+
}
|
|
2621
3249
|
//#endregion
|
|
2622
3250
|
//#region src/model/source.ts
|
|
2623
3251
|
function resolveSource(src, fetchImpl = globalThis.fetch) {
|
|
@@ -2679,6 +3307,82 @@ async function fetchBytes(url, fetchImpl) {
|
|
|
2679
3307
|
}
|
|
2680
3308
|
}
|
|
2681
3309
|
//#endregion
|
|
3310
|
+
//#region src/model/glyph-advances.ts
|
|
3311
|
+
/** How many glyphs past the last match to look for the start of the next item first. */
|
|
3312
|
+
const LOOKAHEAD = 256;
|
|
3313
|
+
/** How far, as a fraction of the font size, a glyph may start from the item origin and still match. */
|
|
3314
|
+
const START_SLACK = .05;
|
|
3315
|
+
/** How far off the baseline, as a fraction of the font size, a glyph may sit (text rise). */
|
|
3316
|
+
const RISE_SLACK = 1;
|
|
3317
|
+
/**
|
|
3318
|
+
* Each item's character boundaries as fractions of its width (see `TextItem.advances`), from the
|
|
3319
|
+
* glyphs that drew it. PDF.js builds text items from the same glyphs in the same order. An item
|
|
3320
|
+
* matches a glyph sequence that spells its text and whose first glyph starts at the item's origin.
|
|
3321
|
+
* Boundaries are where the pen put each glyph, projected onto the item's baseline, so they hold
|
|
3322
|
+
* through moves, scaling and spacing inside an item, and may run past 1 when PDF.js reports a
|
|
3323
|
+
* width shorter than the glyphs it drew. An item that cannot be matched, or that runs right to
|
|
3324
|
+
* left or top to bottom, gets undefined.
|
|
3325
|
+
*/
|
|
3326
|
+
function glyphAdvances(runs, glyphs) {
|
|
3327
|
+
let next = 0;
|
|
3328
|
+
return runs.map((run) => {
|
|
3329
|
+
if (run.dir !== "ltr" || run.str.trim() === "" || !(run.width > 0)) return void 0;
|
|
3330
|
+
const tryFrom = (from, to) => {
|
|
3331
|
+
for (let start = from; start < to; start++) {
|
|
3332
|
+
const m = matchRun(run, glyphs, start);
|
|
3333
|
+
if (m) return m;
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
const m = tryFrom(next, Math.min(glyphs.length, next + LOOKAHEAD)) ?? tryFrom(0, glyphs.length);
|
|
3337
|
+
if (!m) return void 0;
|
|
3338
|
+
next = m.next;
|
|
3339
|
+
return m.bounds;
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
/** The text a glyph's text stands for in PDF.js text content, which expands ligatures. */
|
|
3343
|
+
function variants(text) {
|
|
3344
|
+
const n = text.normalize("NFKC");
|
|
3345
|
+
return n === text ? [text] : [text, n];
|
|
3346
|
+
}
|
|
3347
|
+
/** A glyph the item may leave out: a space, or a character that draws nothing. */
|
|
3348
|
+
const skippable = (text) => /^[\s\p{Cf}]*$/u.test(text);
|
|
3349
|
+
function matchRun(run, glyphs, start) {
|
|
3350
|
+
const { str, origin, along, width, fontSize } = run;
|
|
3351
|
+
const t = (p) => ((p.x - origin.x) * along.x + (p.y - origin.y) * along.y) / width;
|
|
3352
|
+
const off = (p) => Math.abs((p.x - origin.x) * along.y - (p.y - origin.y) * along.x);
|
|
3353
|
+
const first = glyphs[start];
|
|
3354
|
+
if (!first || Math.abs(t(first.start) * width) > START_SLACK * fontSize || off(first.start) > RISE_SLACK * fontSize) return void 0;
|
|
3355
|
+
const bounds = [];
|
|
3356
|
+
const ends = [];
|
|
3357
|
+
let end = 0;
|
|
3358
|
+
let g = start;
|
|
3359
|
+
while (bounds.length < str.length) {
|
|
3360
|
+
const glyph = glyphs[g];
|
|
3361
|
+
const at = bounds.length;
|
|
3362
|
+
const text = glyph && variants(glyph.text).find((v) => v !== "" && str.startsWith(v, at));
|
|
3363
|
+
if (glyph && text) {
|
|
3364
|
+
const t0 = t(glyph.start);
|
|
3365
|
+
end = t(glyph.end);
|
|
3366
|
+
for (let k = 0; k < text.length; k++) {
|
|
3367
|
+
bounds.push(t0 + (end - t0) * k / text.length);
|
|
3368
|
+
ends.push(t0 + (end - t0) * (k + 1) / text.length);
|
|
3369
|
+
}
|
|
3370
|
+
g++;
|
|
3371
|
+
} else if (at > 0 && /\s/u.test(str[at])) {
|
|
3372
|
+
bounds.push(end);
|
|
3373
|
+
ends.push(end);
|
|
3374
|
+
} else if (at > 0 && glyph && skippable(glyph.text)) g++;
|
|
3375
|
+
else return void 0;
|
|
3376
|
+
}
|
|
3377
|
+
return {
|
|
3378
|
+
bounds: {
|
|
3379
|
+
advances: [...bounds, end],
|
|
3380
|
+
ends
|
|
3381
|
+
},
|
|
3382
|
+
next: g
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
//#endregion
|
|
2682
3386
|
//#region src/model/text.ts
|
|
2683
3387
|
/** Fractions of the font size used when PDF.js has no usable metrics for the font. */
|
|
2684
3388
|
const FALLBACK_ASCENT = .75;
|
|
@@ -2696,8 +3400,10 @@ const FALLBACK_DESCENT = .25;
|
|
|
2696
3400
|
* So p3 = (e, f) - v * descent * fontSize, p4 = p3 + u * width, p1 = p3 + v * (ascent + descent) * fontSize,
|
|
2697
3401
|
* p2 = p4 + (p1 - p3). `rect` is the axis-aligned bounding box of the quad.
|
|
2698
3402
|
*/
|
|
2699
|
-
function textContentFromPdfjs(page, tc) {
|
|
3403
|
+
function textContentFromPdfjs(page, tc, measure, glyphs) {
|
|
2700
3404
|
const items = [];
|
|
3405
|
+
const runs = [];
|
|
3406
|
+
const families = [];
|
|
2701
3407
|
for (const raw of tc.items) {
|
|
2702
3408
|
if (!("str" in raw)) continue;
|
|
2703
3409
|
const item = raw;
|
|
@@ -2742,21 +3448,61 @@ function textContentFromPdfjs(page, tc) {
|
|
|
2742
3448
|
p3,
|
|
2743
3449
|
p4
|
|
2744
3450
|
};
|
|
3451
|
+
const dir = isDir(item.dir) ? item.dir : "ltr";
|
|
2745
3452
|
items.push({
|
|
2746
3453
|
str: item.str,
|
|
2747
|
-
dir
|
|
3454
|
+
dir,
|
|
2748
3455
|
fontName: item.fontName,
|
|
2749
3456
|
fontSize,
|
|
2750
3457
|
hasEOL: item.hasEOL,
|
|
2751
3458
|
quad,
|
|
2752
3459
|
rect: rectFromQuad(quad)
|
|
2753
3460
|
});
|
|
3461
|
+
runs.push({
|
|
3462
|
+
str: item.str,
|
|
3463
|
+
dir,
|
|
3464
|
+
fontSize,
|
|
3465
|
+
origin: {
|
|
3466
|
+
x: e,
|
|
3467
|
+
y: f
|
|
3468
|
+
},
|
|
3469
|
+
along: u,
|
|
3470
|
+
width: w
|
|
3471
|
+
});
|
|
3472
|
+
families.push(tc.styles[item.fontName]?.fontFamily ?? "sans-serif");
|
|
2754
3473
|
}
|
|
3474
|
+
const exact = glyphs ? glyphAdvances(runs, glyphs) : [];
|
|
3475
|
+
items.forEach((it, i) => {
|
|
3476
|
+
const bounds = exact[i];
|
|
3477
|
+
if (bounds) {
|
|
3478
|
+
it.advances = bounds.advances;
|
|
3479
|
+
it.glyphEnds = bounds.ends;
|
|
3480
|
+
} else if (measure && it.dir === "ltr") {
|
|
3481
|
+
const advances = advancesOf(it.str, families[i], measure);
|
|
3482
|
+
if (advances) it.advances = advances;
|
|
3483
|
+
}
|
|
3484
|
+
});
|
|
2755
3485
|
return {
|
|
2756
3486
|
page,
|
|
2757
3487
|
items
|
|
2758
3488
|
};
|
|
2759
3489
|
}
|
|
3490
|
+
/** Each character boundary of `str` as a fraction of its measured width, or undefined if it measures 0. */
|
|
3491
|
+
function advancesOf(str, family, measure) {
|
|
3492
|
+
const ends = [0];
|
|
3493
|
+
let sum = 0;
|
|
3494
|
+
for (let i = 0; i < str.length; i++) {
|
|
3495
|
+
const cp = str.codePointAt(i);
|
|
3496
|
+
if (cp > 65535) {
|
|
3497
|
+
sum += measure(String.fromCodePoint(cp), family);
|
|
3498
|
+
ends.push(sum);
|
|
3499
|
+
i++;
|
|
3500
|
+
} else sum += measure(str[i], family);
|
|
3501
|
+
ends.push(sum);
|
|
3502
|
+
}
|
|
3503
|
+
if (!(sum > 0)) return void 0;
|
|
3504
|
+
return ends.map((e) => e / sum);
|
|
3505
|
+
}
|
|
2760
3506
|
function metrics(style) {
|
|
2761
3507
|
if (!style || style.vertical) return {
|
|
2762
3508
|
ascent: FALLBACK_ASCENT,
|
|
@@ -2795,6 +3541,7 @@ async function openDocument(source, options) {
|
|
|
2795
3541
|
let pageCache;
|
|
2796
3542
|
let destroyed = false;
|
|
2797
3543
|
let handles;
|
|
3544
|
+
const measure = canvasMeasureChar();
|
|
2798
3545
|
let destroyTask;
|
|
2799
3546
|
const store = createAnnotationStore({
|
|
2800
3547
|
mode: options.annotations ?? { mode: "uncontrolled" },
|
|
@@ -2868,16 +3615,93 @@ async function openDocument(source, options) {
|
|
|
2868
3615
|
if (!destroyed && patched.size > 0) store._patchEmbedded(patched);
|
|
2869
3616
|
}).catch(() => void 0);
|
|
2870
3617
|
}, () => void 0);
|
|
2871
|
-
|
|
3618
|
+
/**
|
|
3619
|
+
* A page's text with item boundaries measured or, with `glyphs`, taken from where the page
|
|
3620
|
+
* draws each glyph. Reading the glyphs means reading the page's drawing commands, which also
|
|
3621
|
+
* decodes its images, so search does it only for pages with hits.
|
|
3622
|
+
*/
|
|
3623
|
+
async function pageContent(page, glyphs) {
|
|
3624
|
+
if (destroyed) throw destroyedError();
|
|
3625
|
+
if (!handles) throw statusError(status.get());
|
|
3626
|
+
const p = await handles.getPage(page);
|
|
3627
|
+
const tc = await p.getTextContent();
|
|
3628
|
+
const placed = glyphs && tc.items.length > 0 ? await pageGlyphs(p, handles.module).catch(() => void 0) : void 0;
|
|
3629
|
+
return textContentFromPdfjs(page, tc, measure, placed);
|
|
3630
|
+
}
|
|
3631
|
+
/** Each page's searchable text, and whether its boundaries come from the page's glyphs. */
|
|
3632
|
+
const searchable = /* @__PURE__ */ new Map();
|
|
3633
|
+
const abortError = (signal) => signal?.reason ?? new DOMException("Aborted", "AbortError");
|
|
3634
|
+
const searchableFor = (page, placed) => {
|
|
3635
|
+
let cached = searchable.get(page);
|
|
3636
|
+
if (!cached) cached = pageContent(page, placed).then((c) => ({
|
|
3637
|
+
st: searchableText(c),
|
|
3638
|
+
placed
|
|
3639
|
+
}));
|
|
3640
|
+
else if (placed) cached = cached.then((c) => c.placed ? c : pageContent(page, true).then((pc) => ({
|
|
3641
|
+
st: searchableText(pc),
|
|
3642
|
+
placed: true
|
|
3643
|
+
})));
|
|
3644
|
+
else return cached.then((c) => c.st);
|
|
3645
|
+
const entry = cached;
|
|
3646
|
+
searchable.set(page, entry);
|
|
3647
|
+
entry.catch(() => {
|
|
3648
|
+
if (searchable.get(page) === entry) searchable.delete(page);
|
|
3649
|
+
});
|
|
3650
|
+
return entry.then((c) => c.st);
|
|
3651
|
+
};
|
|
3652
|
+
async function* scan(query, opts) {
|
|
3653
|
+
const { signal } = opts;
|
|
3654
|
+
const matchOpts = {
|
|
3655
|
+
matchCase: opts.matchCase ?? false,
|
|
3656
|
+
wholeWord: opts.wholeWord ?? false
|
|
3657
|
+
};
|
|
3658
|
+
const order = opts.pages ?? Array.from({ length: pages.length }, (_, i) => i);
|
|
3659
|
+
const check = () => {
|
|
3660
|
+
if (signal?.aborted || destroyed) throw abortError(signal);
|
|
3661
|
+
};
|
|
3662
|
+
check();
|
|
3663
|
+
for (let n = 0; n < order.length; n++) {
|
|
3664
|
+
if (n > 0) await new Promise((r) => setTimeout(r));
|
|
3665
|
+
check();
|
|
3666
|
+
let hits;
|
|
3667
|
+
try {
|
|
3668
|
+
hits = matchPage(await searchableFor(order[n], false), query, matchOpts);
|
|
3669
|
+
if (hits.length > 0) {
|
|
3670
|
+
check();
|
|
3671
|
+
hits = matchPage(await searchableFor(order[n], true), query, matchOpts);
|
|
3672
|
+
}
|
|
3673
|
+
} catch (error) {
|
|
3674
|
+
check();
|
|
3675
|
+
throw error;
|
|
3676
|
+
}
|
|
3677
|
+
check();
|
|
3678
|
+
for (const hit of hits) {
|
|
3679
|
+
yield hit;
|
|
3680
|
+
check();
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
const doc = {
|
|
2872
3685
|
key: resolved.key,
|
|
2873
3686
|
status: readonly(status),
|
|
2874
3687
|
ready,
|
|
2875
3688
|
pages,
|
|
2876
3689
|
info,
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
3690
|
+
text(page) {
|
|
3691
|
+
return pageContent(page, true);
|
|
3692
|
+
},
|
|
3693
|
+
async pageText(page) {
|
|
3694
|
+
return searchableText(await doc.text(page)).raw;
|
|
3695
|
+
},
|
|
3696
|
+
search(query, opts = {}) {
|
|
3697
|
+
return {
|
|
3698
|
+
[Symbol.asyncIterator]: () => scan(query, opts),
|
|
3699
|
+
async all() {
|
|
3700
|
+
const hits = [];
|
|
3701
|
+
for await (const hit of scan(query, opts)) hits.push(hit);
|
|
3702
|
+
return hits;
|
|
3703
|
+
}
|
|
3704
|
+
};
|
|
2881
3705
|
},
|
|
2882
3706
|
annotations: store,
|
|
2883
3707
|
embedded,
|
|
@@ -2901,6 +3725,7 @@ async function openDocument(source, options) {
|
|
|
2901
3725
|
history.destroy();
|
|
2902
3726
|
store.destroy();
|
|
2903
3727
|
pageCache?.clear();
|
|
3728
|
+
searchable.clear();
|
|
2904
3729
|
if (destroyTask) teardown(destroyTask);
|
|
2905
3730
|
},
|
|
2906
3731
|
get _pdfjs() {
|
|
@@ -2908,6 +3733,7 @@ async function openDocument(source, options) {
|
|
|
2908
3733
|
return handles;
|
|
2909
3734
|
}
|
|
2910
3735
|
};
|
|
3736
|
+
return doc;
|
|
2911
3737
|
}
|
|
2912
3738
|
const EMBEDDED_CONCURRENCY = 8;
|
|
2913
3739
|
/** Runs `task(0..count-1)` with at most `limit` in flight; results come back in index order. */
|
|
@@ -4707,6 +5533,6 @@ function createSignatureLibrary(store) {
|
|
|
4707
5533
|
};
|
|
4708
5534
|
}
|
|
4709
5535
|
//#endregion
|
|
4710
|
-
export {
|
|
5536
|
+
export { exportAnnotations as $, createRecordStore as $t, styleGroupOf as A, QuadSchema as At, isTextMarkup as B, isStandardCallout as Bt, repliesTo as C, MaskSchema as Ct, STYLE_GROUPS as D, PolySchema as Dt, withoutReplies as E, PointSchema as Et, typesInGroup as F, StampSchema as Ft, resizedRect as G, PDF_NAME$1 as Gt, movedEndpoint as H, INTENTS as Ht, hasCallout as I, TextMarkupSchema as It, withBox as J, parsePdfDate as Jt, scaled as K, isKnown as Kt, hasCornerHandles as L, FringeSchema as Lt, styleGroupsOf as M, RedactSchema as Mt, supportedProps as N, ShapeSchema as Nt, createToolStyle as O, PolygonSchema as Ot, toFields as P, SquareSchema as Pt, deriveRect as Q, mergeLineRects as Qt, hasRectGeometry as R, calloutStroke as Rt, isThreadRoot as S, LoupeSchema as St, threadRoot as T, NoteSchema as Tt, movedTip as U, LINE_ENDINGS as Ut, keepsAspect as V, withCalloutBounds as Vt, resizedBox as W, NOTE_ICONS as Wt, isBlankQuery as X, normalizeColor as Xt, openDocument as Y, cssVariableDefaults as Yt, createAnnotationStore as Z, staticDefaults as Zt, defaultEnds as _, translateRect as _n, GLOSS_VERSION$1 as _t, createLocalSignatureStore as a, invertMatrix as an, importedId as at, isAbandonedNote as b, InkError as bn, LineEndingSchema as bt, normaliseInk as c, quadFromRect as cn, AnnotationDocumentSchema as ct, recogniseStroke as d, rectContainsPoint as dn, ExportedAnnotationSchema as dt, derived as en, importAnnotations as et, DEFAULT_CORNER_RADIUS as f, rectFromPoints as fn, FreeTextSchema as ft, constrain as g, translateQuad as gn, GLOSS_SCHEMA_ID as gt, SHAPE_KINDS as h, rectsIntersect as hn, GLOSS_MEDIA_TYPE as ht, createInkTemplate as i, approxEqual as in, importEmbeddedPage as it, styleGroupOfType as j, RectSchema as jt, fromAnnotation as k, PolylineSchema as kt, placeSignature as l, quadsFromFlat as ln, AnnotationSchema as lt, DEFAULT_SHAPE_SIZE as m, rectFromQuads as mn, GLOSS_FORMAT$1 as mt, SIGNATURE_INK_WIDTH as n, writable as nn, isProduction as nt, createMemorySignatureStore as o, multiplyMatrix as on, pdfColorToHex as ot, DEFAULT_SHAPE_OPTIONS as p, rectFromQuad as pn, FreeTextStyleSchema as pt, translated as q, pdfNameSet as qt, createImageTemplate as r, applyMatrix as rn, SUPPORTED_SUBTYPES as rt, createSignatureLibrary as s, pointsFromFlat as sn, summarise as st, DEFAULT_SIGNATURE_WIDTH as t, readonly as tn, createHistory as tt, RECOGNISE_THRESHOLD as u, quadsFromRect as un, CircleSchema as ut, isPointerOnlyKind as v, unionRects as vn, IMAGE_DATA_URL as vt, threadOf as w, NoteIconSchema as wt, isReply as x, isInkError as xn, LineSchema as xt, shapeInput as y, Emitter as yn, InkSchema as yt, isBubble as z, freeTextBox as zt };
|
|
4711
5537
|
|
|
4712
|
-
//# sourceMappingURL=signatures-
|
|
5538
|
+
//# sourceMappingURL=signatures-Do_l2IlM.mjs.map
|