@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.
@@ -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: isDir(item.dir) ? item.dir : "ltr",
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
- async text(page) {
3092
- if (destroyed) throw destroyedError();
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)).items.map((item) => item.str).join(" ");
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 { importAnnotations as $, derived as $t, styleGroupOf as A, RectSchema as At, isTextMarkup as B, withCalloutBounds as Bt, repliesTo as C, NoteIconSchema as Ct, STYLE_GROUPS as D, PolygonSchema as Dt, withoutReplies as E, PolySchema as Et, typesInGroup as F, TextMarkupSchema as Ft, resizedRect as G, isKnown as Gt, movedEndpoint as H, LINE_ENDINGS as Ht, hasCallout as I, FringeSchema as It, withBox as J, cssVariableDefaults as Jt, scaled as K, pdfNameSet as Kt, hasCornerHandles as L, calloutStroke as Lt, styleGroupsOf as M, ShapeSchema as Mt, supportedProps as N, SquareSchema as Nt, createToolStyle as O, PolylineSchema as Ot, toFields as P, StampSchema as Pt, exportAnnotations as Q, createRecordStore as Qt, hasRectGeometry as R, freeTextBox as Rt, isThreadRoot as S, MaskSchema as St, threadRoot as T, PointSchema as Tt, movedTip as U, NOTE_ICONS as Ut, keepsAspect as V, INTENTS as Vt, resizedBox as W, PDF_NAME$1 as Wt, createAnnotationStore as X, staticDefaults as Xt, openDocument as Y, normalizeColor as Yt, deriveRect as Z, mergeLineRects as Zt, defaultEnds as _, unionRects as _n, IMAGE_DATA_URL as _t, createLocalSignatureStore as a, multiplyMatrix as an, pdfColorToHex as at, isAbandonedNote as b, isInkError as bn, LineSchema as bt, normaliseInk as c, quadsFromFlat as cn, AnnotationSchema as ct, recogniseStroke as d, rectFromPoints as dn, FreeTextSchema as dt, readonly as en, createHistory as et, DEFAULT_CORNER_RADIUS as f, rectFromQuad as fn, FreeTextStyleSchema as ft, constrain as g, translateRect as gn, GLOSS_VERSION$1 as gt, SHAPE_KINDS as h, translateQuad as hn, GLOSS_SCHEMA_ID as ht, createInkTemplate as i, invertMatrix as in, importedId as it, styleGroupOfType as j, RedactSchema as jt, fromAnnotation as k, QuadSchema as kt, placeSignature as l, quadsFromRect as ln, CircleSchema as lt, DEFAULT_SHAPE_SIZE as m, rectsIntersect as mn, GLOSS_MEDIA_TYPE as mt, SIGNATURE_INK_WIDTH as n, applyMatrix as nn, SUPPORTED_SUBTYPES as nt, createMemorySignatureStore as o, pointsFromFlat as on, summarise as ot, DEFAULT_SHAPE_OPTIONS as p, rectFromQuads as pn, GLOSS_FORMAT$1 as pt, translated as q, parsePdfDate as qt, createImageTemplate as r, approxEqual as rn, importEmbeddedPage as rt, createSignatureLibrary as s, quadFromRect as sn, AnnotationDocumentSchema as st, DEFAULT_SIGNATURE_WIDTH as t, writable as tn, isProduction as tt, RECOGNISE_THRESHOLD as u, rectContainsPoint as un, ExportedAnnotationSchema as ut, isPointerOnlyKind as v, Emitter as vn, InkSchema as vt, threadOf as w, NoteSchema as wt, isReply as x, LoupeSchema as xt, shapeInput as y, InkError as yn, LineEndingSchema as yt, isBubble as z, isStandardCallout as zt };
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-D68fuLlt.mjs.map
5538
+ //# sourceMappingURL=signatures-Do_l2IlM.mjs.map