@inkbar/core 0.6.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 +39 -1
- package/dist/dom.d.mts +124 -3
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +649 -10
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{signatures-By4B40DP.d.mts → signatures-CalOktkf.d.mts} +65 -12
- package/dist/signatures-CalOktkf.d.mts.map +1 -0
- package/dist/{signatures-D68fuLlt.mjs → signatures-Do_l2IlM.mjs} +617 -9
- package/dist/signatures-Do_l2IlM.mjs.map +1 -0
- package/package.json +4 -3
- package/dist/signatures-By4B40DP.d.mts.map +0 -1
- package/dist/signatures-D68fuLlt.mjs.map +0 -1
|
@@ -410,6 +410,167 @@ function mergeLineRects(rects, tolerance = 2, gap) {
|
|
|
410
410
|
return runs.map(quadFromRect);
|
|
411
411
|
}
|
|
412
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
|
|
413
574
|
//#region src/pdfjs/load.ts
|
|
414
575
|
const REQUIRED = [
|
|
415
576
|
"worker",
|
|
@@ -2792,6 +2953,30 @@ function createAnnotationStore(opts) {
|
|
|
2792
2953
|
};
|
|
2793
2954
|
}
|
|
2794
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
|
|
2795
2980
|
//#region src/model/page-cache.ts
|
|
2796
2981
|
function createPageCache(fetch) {
|
|
2797
2982
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -2832,6 +3017,235 @@ function pageInfoFromProxy(page, index) {
|
|
|
2832
3017
|
function normaliseRotation(rotate) {
|
|
2833
3018
|
return (Math.round(rotate / 90) * 90 % 360 + 360) % 360;
|
|
2834
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
|
+
}
|
|
2835
3249
|
//#endregion
|
|
2836
3250
|
//#region src/model/source.ts
|
|
2837
3251
|
function resolveSource(src, fetchImpl = globalThis.fetch) {
|
|
@@ -2893,6 +3307,82 @@ async function fetchBytes(url, fetchImpl) {
|
|
|
2893
3307
|
}
|
|
2894
3308
|
}
|
|
2895
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
|
|
2896
3386
|
//#region src/model/text.ts
|
|
2897
3387
|
/** Fractions of the font size used when PDF.js has no usable metrics for the font. */
|
|
2898
3388
|
const FALLBACK_ASCENT = .75;
|
|
@@ -2910,8 +3400,10 @@ const FALLBACK_DESCENT = .25;
|
|
|
2910
3400
|
* So p3 = (e, f) - v * descent * fontSize, p4 = p3 + u * width, p1 = p3 + v * (ascent + descent) * fontSize,
|
|
2911
3401
|
* p2 = p4 + (p1 - p3). `rect` is the axis-aligned bounding box of the quad.
|
|
2912
3402
|
*/
|
|
2913
|
-
function textContentFromPdfjs(page, tc) {
|
|
3403
|
+
function textContentFromPdfjs(page, tc, measure, glyphs) {
|
|
2914
3404
|
const items = [];
|
|
3405
|
+
const runs = [];
|
|
3406
|
+
const families = [];
|
|
2915
3407
|
for (const raw of tc.items) {
|
|
2916
3408
|
if (!("str" in raw)) continue;
|
|
2917
3409
|
const item = raw;
|
|
@@ -2956,21 +3448,61 @@ function textContentFromPdfjs(page, tc) {
|
|
|
2956
3448
|
p3,
|
|
2957
3449
|
p4
|
|
2958
3450
|
};
|
|
3451
|
+
const dir = isDir(item.dir) ? item.dir : "ltr";
|
|
2959
3452
|
items.push({
|
|
2960
3453
|
str: item.str,
|
|
2961
|
-
dir
|
|
3454
|
+
dir,
|
|
2962
3455
|
fontName: item.fontName,
|
|
2963
3456
|
fontSize,
|
|
2964
3457
|
hasEOL: item.hasEOL,
|
|
2965
3458
|
quad,
|
|
2966
3459
|
rect: rectFromQuad(quad)
|
|
2967
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");
|
|
2968
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
|
+
});
|
|
2969
3485
|
return {
|
|
2970
3486
|
page,
|
|
2971
3487
|
items
|
|
2972
3488
|
};
|
|
2973
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
|
+
}
|
|
2974
3506
|
function metrics(style) {
|
|
2975
3507
|
if (!style || style.vertical) return {
|
|
2976
3508
|
ascent: FALLBACK_ASCENT,
|
|
@@ -3009,6 +3541,7 @@ async function openDocument(source, options) {
|
|
|
3009
3541
|
let pageCache;
|
|
3010
3542
|
let destroyed = false;
|
|
3011
3543
|
let handles;
|
|
3544
|
+
const measure = canvasMeasureChar();
|
|
3012
3545
|
let destroyTask;
|
|
3013
3546
|
const store = createAnnotationStore({
|
|
3014
3547
|
mode: options.annotations ?? { mode: "uncontrolled" },
|
|
@@ -3082,19 +3615,93 @@ async function openDocument(source, options) {
|
|
|
3082
3615
|
if (!destroyed && patched.size > 0) store._patchEmbedded(patched);
|
|
3083
3616
|
}).catch(() => void 0);
|
|
3084
3617
|
}, () => void 0);
|
|
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
|
+
}
|
|
3085
3684
|
const doc = {
|
|
3086
3685
|
key: resolved.key,
|
|
3087
3686
|
status: readonly(status),
|
|
3088
3687
|
ready,
|
|
3089
3688
|
pages,
|
|
3090
3689
|
info,
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
if (!handles) throw statusError(status.get());
|
|
3094
|
-
return textContentFromPdfjs(page, await (await handles.getPage(page)).getTextContent());
|
|
3690
|
+
text(page) {
|
|
3691
|
+
return pageContent(page, true);
|
|
3095
3692
|
},
|
|
3096
3693
|
async pageText(page) {
|
|
3097
|
-
return (await doc.text(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
|
+
};
|
|
3098
3705
|
},
|
|
3099
3706
|
annotations: store,
|
|
3100
3707
|
embedded,
|
|
@@ -3118,6 +3725,7 @@ async function openDocument(source, options) {
|
|
|
3118
3725
|
history.destroy();
|
|
3119
3726
|
store.destroy();
|
|
3120
3727
|
pageCache?.clear();
|
|
3728
|
+
searchable.clear();
|
|
3121
3729
|
if (destroyTask) teardown(destroyTask);
|
|
3122
3730
|
},
|
|
3123
3731
|
get _pdfjs() {
|
|
@@ -4925,6 +5533,6 @@ function createSignatureLibrary(store) {
|
|
|
4925
5533
|
};
|
|
4926
5534
|
}
|
|
4927
5535
|
//#endregion
|
|
4928
|
-
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 };
|
|
4929
5537
|
|
|
4930
|
-
//# sourceMappingURL=signatures-
|
|
5538
|
+
//# sourceMappingURL=signatures-Do_l2IlM.mjs.map
|