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