@embedpdf/engine-services 3.0.0-next.4 → 3.0.0-next.6

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/dist/index.d.cts CHANGED
@@ -633,23 +633,26 @@ declare class RedactionApplier {
633
633
  //#region src/features/text/PageTextReader.d.ts
634
634
  /**
635
635
  * Per-page slow-path text reader. Acquires a `pagePtr` from the
636
- * `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`),
637
- * extracts the full UTF-16 buffer with `FPDFText_GetText`, decodes,
638
- * and releases everything in reverse order.
636
+ * `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`), extracts
637
+ * the page text, and releases everything in reverse order.
639
638
  *
640
- * Memory model:
641
- * - `count` is in UTF-16 code units (FPDFText's "char").
642
- * - The buffer needs `(count + 1) * 2` bytes (PDFium writes a
643
- * trailing NUL terminator).
644
- * - `mem.readU16String` walks the buffer up to the NUL and decodes
645
- * to a JS string, faithfully preserving surrogate pairs.
639
+ * Index spaces (see engine-core `text/charmap.ts`): `charCount` is the
640
+ * CHARACTER space (`FPDFText_CountChars` — the space geometry runs tile);
641
+ * `text` is the TEXT projection. The two diverge when a character
642
+ * contributes zero text units (non-printing) or two (supplementary plane);
643
+ * `charMap` anchors encode exactly those deviations.
646
644
  *
647
- * Errors:
648
- * - `EPDFDoc_LoadPageByObjectNumber` returning 0 is `NotFound`
649
- * (raised by `PagePtrPool.acquire`).
650
- * - `FPDFText_LoadPage` returning 0 is `RuntimeUnavailable` — should
651
- * not happen on a healthy page; treat it as a runtime bug rather
652
- * than user error.
645
+ * Both come from the fork's batched extension calls, which share one
646
+ * encoding authority (`FX_UTF16Encode`) so text and map can never
647
+ * disagree:
648
+ *
649
+ * - `EPDFText_GetTextFull` — true UTF-16LE extraction; supplementary
650
+ * characters arrive as surrogate pairs instead of being silently
651
+ * dropped by `FPDFText_GetText`'s UCS-2 encoder. Called once with an
652
+ * upper-bound buffer (2 units per character + NUL), so no measure
653
+ * round-trip.
654
+ * - `EPDFText_GetCharToTextMap` — the anchors, measure-then-fill. Zero
655
+ * anchors is the common (identity) page and skips the fill call.
653
656
  */
654
657
  declare class PageTextReader {
655
658
  private readonly runtime;
package/dist/index.d.ts CHANGED
@@ -633,23 +633,26 @@ declare class RedactionApplier {
633
633
  //#region src/features/text/PageTextReader.d.ts
634
634
  /**
635
635
  * Per-page slow-path text reader. Acquires a `pagePtr` from the
636
- * `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`),
637
- * extracts the full UTF-16 buffer with `FPDFText_GetText`, decodes,
638
- * and releases everything in reverse order.
636
+ * `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`), extracts
637
+ * the page text, and releases everything in reverse order.
639
638
  *
640
- * Memory model:
641
- * - `count` is in UTF-16 code units (FPDFText's "char").
642
- * - The buffer needs `(count + 1) * 2` bytes (PDFium writes a
643
- * trailing NUL terminator).
644
- * - `mem.readU16String` walks the buffer up to the NUL and decodes
645
- * to a JS string, faithfully preserving surrogate pairs.
639
+ * Index spaces (see engine-core `text/charmap.ts`): `charCount` is the
640
+ * CHARACTER space (`FPDFText_CountChars` — the space geometry runs tile);
641
+ * `text` is the TEXT projection. The two diverge when a character
642
+ * contributes zero text units (non-printing) or two (supplementary plane);
643
+ * `charMap` anchors encode exactly those deviations.
646
644
  *
647
- * Errors:
648
- * - `EPDFDoc_LoadPageByObjectNumber` returning 0 is `NotFound`
649
- * (raised by `PagePtrPool.acquire`).
650
- * - `FPDFText_LoadPage` returning 0 is `RuntimeUnavailable` — should
651
- * not happen on a healthy page; treat it as a runtime bug rather
652
- * than user error.
645
+ * Both come from the fork's batched extension calls, which share one
646
+ * encoding authority (`FX_UTF16Encode`) so text and map can never
647
+ * disagree:
648
+ *
649
+ * - `EPDFText_GetTextFull` — true UTF-16LE extraction; supplementary
650
+ * characters arrive as surrogate pairs instead of being silently
651
+ * dropped by `FPDFText_GetText`'s UCS-2 encoder. Called once with an
652
+ * upper-bound buffer (2 units per character + NUL), so no measure
653
+ * round-trip.
654
+ * - `EPDFText_GetCharToTextMap` — the anchors, measure-then-fill. Zero
655
+ * anchors is the common (identity) page and skips the fill call.
653
656
  */
654
657
  declare class PageTextReader {
655
658
  private readonly runtime;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AbortError, EMPTY_TRANSFER, EngineError, EngineErrorCode, NO_ANNOTATION_FLAGS, PDF_SUBTYPE_TO_CODE, PdfAnnotationSubtypeCode, UNKNOWN_WEAK_ANNOTATION_STATE, appearanceImpactOf, buildPageTextLayout, buildSnippet, foldOptionsFor, foldText, invalidatesWeakIndexRefs, isValidPageObjectNumber, knownWeakAnnotationState, matchLiteral, matchRegex, normalizePdfRect, pdfQuadBounds, revisionTokensEqual, serializeError, shiftsExistingAnnotationIndices, sniffBinaryMetadata, subtypeFromCode, textSegmentsForRange, validateSearchQuery, wirePack } from "@embedpdf/engine-core/runtime";
1
+ import { AbortError, EMPTY_TRANSFER, EngineError, EngineErrorCode, NO_ANNOTATION_FLAGS, PDF_SUBTYPE_TO_CODE, PdfAnnotationSubtypeCode, UNKNOWN_WEAK_ANNOTATION_STATE, appearanceImpactOf, buildPageTextLayout, buildSnippet, charMapViolation, charRangeForTextOffsets, foldOptionsFor, foldText, invalidatesWeakIndexRefs, isValidPageObjectNumber, knownWeakAnnotationState, matchLiteral, matchRegex, normalizePdfRect, pdfQuadBounds, revisionTokensEqual, serializeError, shiftsExistingAnnotationIndices, sniffBinaryMetadata, subtypeFromCode, textSegmentsForRange, validateSearchQuery, wirePack } from "@embedpdf/engine-core/runtime";
2
2
  import { NULL_PTR } from "@embedpdf/engine-runtime";
3
3
  //#region src/runtime/lifecycle/bootstrap.ts
4
4
  const initialized = /* @__PURE__ */ new WeakSet();
@@ -4172,6 +4172,14 @@ const BOX_FAMILY_SUBTYPES = /* @__PURE__ */ new Set([
4172
4172
  14
4173
4173
  ]);
4174
4174
  /**
4175
+ * Per-appearance output-pixel ceiling (see the clamp in `renderOne`).
4176
+ * 16 M px ≈ a 4000² raster ≈ 64 MB of transient RGBA — safely inside the
4177
+ * wasm heap for the one-at-a-time batch loop, while a page-sized annotation
4178
+ * stays crisp to roughly 5–6× before its appearance starts stretching.
4179
+ * Small annotations (the overwhelming majority) never come near it.
4180
+ */
4181
+ const APPEARANCE_PIXEL_CLAMP = 16e6;
4182
+ /**
4175
4183
  * Maps an `AnnotationAppearanceMode` onto the PDFium appearance-mode int and
4176
4184
  * the `EPDFAnnot_GetAvailableAppearanceModes` bit it occupies.
4177
4185
  * N -> mode 0 / bit 1, R -> mode 1 / bit 2, D -> mode 2 / bit 4.
@@ -4270,13 +4278,15 @@ var AnnotationAppearanceReader = class {
4270
4278
  fn.EPDFAnnot_GenerateFormFieldAP(annotPtr);
4271
4279
  if (!fn.EPDFAnnot_HasAppearanceStream(annotPtr, modeInt)) return null;
4272
4280
  } else return null;
4281
+ const rectArea = Math.max(1, (rect.right - rect.left) * (rect.top - rect.bottom));
4282
+ const effScale = Math.min(scale, Math.sqrt(APPEARANCE_PIXEL_CLAMP / rectArea));
4273
4283
  return rasterize(this.runtime, {
4274
4284
  rect,
4275
4285
  page,
4276
4286
  rotation,
4277
4287
  viewport: {
4278
4288
  kind: "scale",
4279
- scale
4289
+ scale: effScale
4280
4290
  },
4281
4291
  ...maxOutputPixels !== void 0 ? { maxOutputPixels } : {},
4282
4292
  background: "transparent",
@@ -7061,23 +7071,26 @@ var RedactionApplier = class {
7061
7071
  //#region src/features/text/PageTextReader.ts
7062
7072
  /**
7063
7073
  * Per-page slow-path text reader. Acquires a `pagePtr` from the
7064
- * `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`),
7065
- * extracts the full UTF-16 buffer with `FPDFText_GetText`, decodes,
7066
- * and releases everything in reverse order.
7074
+ * `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`), extracts
7075
+ * the page text, and releases everything in reverse order.
7076
+ *
7077
+ * Index spaces (see engine-core `text/charmap.ts`): `charCount` is the
7078
+ * CHARACTER space (`FPDFText_CountChars` — the space geometry runs tile);
7079
+ * `text` is the TEXT projection. The two diverge when a character
7080
+ * contributes zero text units (non-printing) or two (supplementary plane);
7081
+ * `charMap` anchors encode exactly those deviations.
7067
7082
  *
7068
- * Memory model:
7069
- * - `count` is in UTF-16 code units (FPDFText's "char").
7070
- * - The buffer needs `(count + 1) * 2` bytes (PDFium writes a
7071
- * trailing NUL terminator).
7072
- * - `mem.readU16String` walks the buffer up to the NUL and decodes
7073
- * to a JS string, faithfully preserving surrogate pairs.
7083
+ * Both come from the fork's batched extension calls, which share one
7084
+ * encoding authority (`FX_UTF16Encode`) so text and map can never
7085
+ * disagree:
7074
7086
  *
7075
- * Errors:
7076
- * - `EPDFDoc_LoadPageByObjectNumber` returning 0 is `NotFound`
7077
- * (raised by `PagePtrPool.acquire`).
7078
- * - `FPDFText_LoadPage` returning 0 is `RuntimeUnavailable` — should
7079
- * not happen on a healthy page; treat it as a runtime bug rather
7080
- * than user error.
7087
+ * - `EPDFText_GetTextFull` — true UTF-16LE extraction; supplementary
7088
+ * characters arrive as surrogate pairs instead of being silently
7089
+ * dropped by `FPDFText_GetText`'s UCS-2 encoder. Called once with an
7090
+ * upper-bound buffer (2 units per character + NUL), so no measure
7091
+ * round-trip.
7092
+ * - `EPDFText_GetCharToTextMap` — the anchors, measure-then-fill. Zero
7093
+ * anchors is the common (identity) page and skips the fill call.
7081
7094
  */
7082
7095
  var PageTextReader = class {
7083
7096
  constructor(runtime, session) {
@@ -7086,7 +7099,7 @@ var PageTextReader = class {
7086
7099
  }
7087
7100
  read(pageObjectNumber, signal) {
7088
7101
  throwIfAborted(signal);
7089
- const { fn, mem } = this.runtime;
7102
+ const { fn } = this.runtime;
7090
7103
  const pool = this.session.pagePool();
7091
7104
  const pagePtr = pool.acquire(pageObjectNumber);
7092
7105
  try {
@@ -7095,10 +7108,18 @@ var PageTextReader = class {
7095
7108
  if (!textPagePtr) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `FPDFText_LoadPage returned null for page object ${pageObjectNumber}`);
7096
7109
  try {
7097
7110
  throwIfAborted(signal);
7098
- const charCount = fn.FPDFText_CountChars(textPagePtr);
7099
- return {
7100
- text: readPageText(fn, mem, textPagePtr, charCount),
7101
- charCount: Math.max(charCount, 0)
7111
+ const charCount = Math.max(fn.FPDFText_CountChars(textPagePtr), 0);
7112
+ const text = readTextFull(this.runtime, textPagePtr, charCount);
7113
+ const charMap = readCharMap(this.runtime, textPagePtr, pageObjectNumber);
7114
+ const violation = charMapViolation(charCount, text.length, charMap);
7115
+ if (violation !== null) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `char map invariant violated for page object ${pageObjectNumber}: ${violation}`);
7116
+ return charMap.length > 0 ? {
7117
+ text,
7118
+ charCount,
7119
+ charMap
7120
+ } : {
7121
+ text,
7122
+ charCount
7102
7123
  };
7103
7124
  } finally {
7104
7125
  fn.FPDFText_ClosePage(textPagePtr);
@@ -7108,11 +7129,43 @@ var PageTextReader = class {
7108
7129
  }
7109
7130
  }
7110
7131
  };
7111
- function readPageText(fn, mem, textPagePtr, charCount) {
7132
+ /**
7133
+ * Full-fidelity page text in one call: the buffer is sized at the maximum
7134
+ * possible expansion (every character a surrogate pair, plus the NUL), so
7135
+ * the returned requirement always fits and the measure round-trip is
7136
+ * skipped. The return value counts UTF-16 units INCLUDING the terminator.
7137
+ */
7138
+ function readTextFull(runtime, textPagePtr, charCount) {
7112
7139
  if (charCount <= 0) return "";
7113
- return withScratch(mem, (charCount + 1) * 2, (buf) => {
7114
- if (fn.FPDFText_GetText(textPagePtr, 0, charCount, buf) <= 0) return "";
7115
- return mem.readU16String(buf);
7140
+ const { fn, mem } = runtime;
7141
+ const bufUnits = charCount * 2 + 1;
7142
+ return withScratch(mem, bufUnits * 2, (buf) => {
7143
+ const written = fn.EPDFText_GetTextFull(textPagePtr, buf, bufUnits);
7144
+ if (written <= 1) return "";
7145
+ return decodeUtf16Le(mem.readBytes(buf, (written - 1) * 2));
7146
+ });
7147
+ }
7148
+ function decodeUtf16Le(bytes) {
7149
+ const units = new Uint16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 1);
7150
+ let out = "";
7151
+ const CHUNK = 8192;
7152
+ for (let i = 0; i < units.length; i += CHUNK) out += String.fromCharCode(...units.subarray(i, Math.min(i + CHUNK, units.length)));
7153
+ return out;
7154
+ }
7155
+ /** Anchor struct layout: two little-endian i32s (see EPDF_CHAR_MAP_ANCHOR). */
7156
+ const ANCHOR_BYTES = 8;
7157
+ function readCharMap(runtime, textPagePtr, pageObjectNumber) {
7158
+ const { fn, mem } = runtime;
7159
+ const count = fn.EPDFText_GetCharToTextMap(textPagePtr, NULL_PTR, 0);
7160
+ if (count < 0) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `EPDFText_GetCharToTextMap failed for page object ${pageObjectNumber}`);
7161
+ if (count === 0) return [];
7162
+ return withScratch(mem, count * ANCHOR_BYTES, (buf) => {
7163
+ fn.EPDFText_GetCharToTextMap(textPagePtr, buf, count);
7164
+ const bytes = mem.readBytes(buf, count * ANCHOR_BYTES);
7165
+ const words = new Int32Array(bytes.buffer, bytes.byteOffset, count * 2);
7166
+ const anchors = new Array(count);
7167
+ for (let i = 0; i < count; i++) anchors[i] = [words[i * 2], words[i * 2 + 1]];
7168
+ return anchors;
7116
7169
  });
7117
7170
  }
7118
7171
  //#endregion
@@ -8753,7 +8806,7 @@ function emptyResult(index, status) {
8753
8806
  //#endregion
8754
8807
  //#region src/features/search/internal/pageCorpusCache.ts
8755
8808
  /**
8756
- * Per-session, per-page search corpus: the page's extracted text plus its
8809
+ * Per-session, per-page search corpus: the page's text snapshot plus its
8757
8810
  * DEFAULT fold (the one literal queries with default options search).
8758
8811
  * This is the local engine's in-memory equivalent of the server's corpus
8759
8812
  * artifacts — same fold version, same shape, built lazily on first search
@@ -8778,19 +8831,20 @@ function acquirePageCorpus(runtime, session, pageObjectNumber, signal) {
8778
8831
  cache.set(session, pages);
8779
8832
  }
8780
8833
  const hit = pages.get(pageObjectNumber);
8781
- if (hit && hit.seq === seq) return hit.folded;
8782
- const text = new PageTextReader(runtime, session).read(pageObjectNumber, signal).text;
8783
- const folded = foldText(text);
8784
- pages.delete(pageObjectNumber);
8785
- pages.set(pageObjectNumber, {
8834
+ if (hit && hit.seq === seq) return hit;
8835
+ const snapshot = new PageTextReader(runtime, session).read(pageObjectNumber, signal);
8836
+ const entry = {
8786
8837
  seq,
8787
- folded
8788
- });
8838
+ folded: foldText(snapshot.text),
8839
+ snapshot
8840
+ };
8841
+ pages.delete(pageObjectNumber);
8842
+ pages.set(pageObjectNumber, entry);
8789
8843
  if (pages.size > MAX_CACHED_PAGES) {
8790
8844
  const oldest = pages.keys().next().value;
8791
8845
  if (oldest !== void 0) pages.delete(oldest);
8792
8846
  }
8793
- return folded;
8847
+ return entry;
8794
8848
  }
8795
8849
  //#endregion
8796
8850
  //#region src/features/search/internal/searchCursor.ts
@@ -8899,19 +8953,23 @@ var SearchReader = class {
8899
8953
  throwIfAborted(signal);
8900
8954
  const pon = order[scanned].pageObjectNumber;
8901
8955
  const corpus = acquirePageCorpus(this.runtime, this.session, pon, signal);
8956
+ const text = corpus.snapshot.text;
8902
8957
  let ranges;
8903
- if (query.regex) ranges = matchRegex(corpus.original, query);
8904
- else if (query.matchCase || query.matchDiacritics) ranges = matchLiteral(foldText(corpus.original, foldOptionsFor(query)), query);
8905
- else ranges = matchLiteral(corpus, query);
8958
+ if (query.regex) ranges = matchRegex(text, query);
8959
+ else if (query.matchCase || query.matchDiacritics) ranges = matchLiteral(foldText(text, foldOptionsFor(query)), query);
8960
+ else ranges = matchLiteral(corpus.folded, query);
8906
8961
  if (ranges.length > 0) {
8907
8962
  const layout = buildPageTextLayout(new PageGeometryReader(this.runtime, this.session).read(pon, signal));
8908
- for (const range of ranges) matches.push({
8909
- pageObjectNumber: pon,
8910
- charStart: range.start,
8911
- charCount: range.length,
8912
- segments: textSegmentsForRange(layout, range.start, range.length),
8913
- ...mode === "full" ? { snippet: buildSnippet(corpus.original, range) } : {}
8914
- });
8963
+ for (const range of ranges) {
8964
+ const chars = charRangeForTextOffsets(corpus.snapshot, range.start, range.start + range.length);
8965
+ matches.push({
8966
+ pageObjectNumber: pon,
8967
+ charStart: chars.start,
8968
+ charCount: chars.end - chars.start,
8969
+ segments: textSegmentsForRange(layout, chars.start, chars.end - chars.start),
8970
+ ...mode === "full" ? { snippet: buildSnippet(text, range) } : {}
8971
+ });
8972
+ }
8915
8973
  }
8916
8974
  scanned++;
8917
8975
  pagesThisSlice++;