@embedpdf/engine-services 3.0.0-next.1 → 3.0.0-next.10
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 +901 -422
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +138 -20
- package/dist/index.d.ts +138 -20
- package/dist/index.js +901 -424
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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, buildSnippet, foldOptionsFor, foldText, invalidatesWeakIndexRefs, isValidPageObjectNumber, knownWeakAnnotationState, matchLiteral, matchRegex, normalizePdfRect,
|
|
1
|
+
import { AbortError, EMPTY_TRANSFER, EngineError, EngineErrorCode, NO_ANNOTATION_FLAGS, PAGE_INSERT_BLANK_MAX_COUNT, 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();
|
|
@@ -225,8 +225,10 @@ var EventHub = class {
|
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
227
|
publish(event) {
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
if ("origin" in event) {
|
|
229
|
+
const { serverId } = event.origin;
|
|
230
|
+
if (serverId !== null) this.serverId = this.serverId === null ? serverId : Math.max(this.serverId, serverId);
|
|
231
|
+
}
|
|
230
232
|
for (const listener of [...this.listeners]) try {
|
|
231
233
|
listener(event);
|
|
232
234
|
} catch (err) {
|
|
@@ -1163,13 +1165,34 @@ var MetadataMutator = class {
|
|
|
1163
1165
|
};
|
|
1164
1166
|
//#endregion
|
|
1165
1167
|
//#region src/runtime/memory/structs.ts
|
|
1168
|
+
/** Generated from `EPDF_CHAR_GEOMETRY`; pinned by native static_asserts. */
|
|
1169
|
+
const EPDF_CHAR_GEOMETRY_LAYOUT = {
|
|
1170
|
+
bytes: 124,
|
|
1171
|
+
offsets: {
|
|
1172
|
+
looseBox: 0,
|
|
1173
|
+
tightBox: 16,
|
|
1174
|
+
looseQuad: 32,
|
|
1175
|
+
tightQuad: 64,
|
|
1176
|
+
matrix: 96,
|
|
1177
|
+
flags: 120
|
|
1178
|
+
},
|
|
1179
|
+
flags: {
|
|
1180
|
+
hasTightBox: 1,
|
|
1181
|
+
hasLooseQuad: 2,
|
|
1182
|
+
hasTightQuad: 4,
|
|
1183
|
+
upright: 8,
|
|
1184
|
+
space: 16,
|
|
1185
|
+
empty: 32,
|
|
1186
|
+
synthesized: 64
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1166
1189
|
/** Decode an `FS_RECTF` struct that a native call wrote into `ptr`. */
|
|
1167
|
-
function readRectF(mem, ptr) {
|
|
1190
|
+
function readRectF(mem, ptr, byteOffset = 0) {
|
|
1168
1191
|
return {
|
|
1169
|
-
left: readF32(mem, ptr,
|
|
1170
|
-
top: readF32(mem, ptr, 4),
|
|
1171
|
-
right: readF32(mem, ptr, 8),
|
|
1172
|
-
bottom: readF32(mem, ptr, 12)
|
|
1192
|
+
left: readF32(mem, ptr, byteOffset),
|
|
1193
|
+
top: readF32(mem, ptr, byteOffset + 4),
|
|
1194
|
+
right: readF32(mem, ptr, byteOffset + 8),
|
|
1195
|
+
bottom: readF32(mem, ptr, byteOffset + 12)
|
|
1173
1196
|
};
|
|
1174
1197
|
}
|
|
1175
1198
|
/** Decode an `FS_SIZEF` struct that a native call wrote into `ptr`. */
|
|
@@ -1705,6 +1728,42 @@ var PagesInserter = class {
|
|
|
1705
1728
|
cache: null
|
|
1706
1729
|
};
|
|
1707
1730
|
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Create `count` blank pages of `size` at `destIndex`. Same mutation
|
|
1733
|
+
* contract as `insert`, but native creation (`FPDFPage_New`) instead of a
|
|
1734
|
+
* deep copy: no source document, so none of the retain-until-close
|
|
1735
|
+
* lifetime hazard above. Each page gets an empty content stream
|
|
1736
|
+
* (`FPDFPage_GenerateContent`) so the saved PDF renders identically in
|
|
1737
|
+
* third-party viewers.
|
|
1738
|
+
*/
|
|
1739
|
+
insertBlank(spec, destIndex, signal) {
|
|
1740
|
+
throwIfAborted(signal);
|
|
1741
|
+
const { size } = spec;
|
|
1742
|
+
const count = spec.count ?? 1;
|
|
1743
|
+
if (!Number.isFinite(size?.width) || !Number.isFinite(size?.height) || size.width <= 0 || size.height <= 0) throw new EngineError(EngineErrorCode.InvalidArg, `pages.insertBlank size must have positive finite dimensions, got ${size?.width}x${size?.height}`);
|
|
1744
|
+
if (!Number.isInteger(count) || count < 1 || count > PAGE_INSERT_BLANK_MAX_COUNT) throw new EngineError(EngineErrorCode.InvalidArg, `pages.insertBlank count ${count} out of range [1, ${PAGE_INSERT_BLANK_MAX_COUNT}]`);
|
|
1745
|
+
const { fn } = this.runtime;
|
|
1746
|
+
const destPtr = this.session.requireDocPtr();
|
|
1747
|
+
const beforeCount = fn.FPDF_GetPageCount(destPtr);
|
|
1748
|
+
const at = destIndex ?? beforeCount;
|
|
1749
|
+
if (!Number.isInteger(at) || at < 0 || at > beforeCount) throw new EngineError(EngineErrorCode.InvalidArg, `pages.insertBlank destIndex ${at} out of range [0, ${beforeCount}]`);
|
|
1750
|
+
for (let i = 0; i < count; i++) {
|
|
1751
|
+
const pagePtr = fn.FPDFPage_New(destPtr, at + i, size.width, size.height);
|
|
1752
|
+
if (!pagePtr) {
|
|
1753
|
+
for (let j = 0; j < i; j++) fn.FPDFPage_Delete(destPtr, at);
|
|
1754
|
+
throw new EngineError(EngineErrorCode.Unknown, `FPDFPage_New rejected page ${i + 1}/${count} at index ${at + i}`);
|
|
1755
|
+
}
|
|
1756
|
+
fn.FPDFPage_GenerateContent(pagePtr);
|
|
1757
|
+
fn.FPDF_ClosePage(pagePtr);
|
|
1758
|
+
}
|
|
1759
|
+
this.session.refreshPageRegistry();
|
|
1760
|
+
const layout = new PagesReader(this.runtime, this.session).read(signal);
|
|
1761
|
+
return {
|
|
1762
|
+
insertedPageObjectNumbers: layout.pages.slice(at, at + count).map((page) => page.pageObjectNumber),
|
|
1763
|
+
layout,
|
|
1764
|
+
cache: null
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1708
1767
|
};
|
|
1709
1768
|
//#endregion
|
|
1710
1769
|
//#region src/features/annotations/internal/read/annotationReadContext.ts
|
|
@@ -2153,6 +2212,49 @@ function readQuadPoints(fn, mem, annotPtr) {
|
|
|
2153
2212
|
});
|
|
2154
2213
|
}
|
|
2155
2214
|
//#endregion
|
|
2215
|
+
//#region src/features/annotations/internal/read/readAnnotationTransformMetadata.ts
|
|
2216
|
+
/**
|
|
2217
|
+
* Read-side twin of `writers/.../writeAnnotationTransformMetadata.ts`. Decodes
|
|
2218
|
+
* the EmbedPDF rotation keys from /EMBD_Metadata back onto the DTO:
|
|
2219
|
+
*
|
|
2220
|
+
* - `Rotation` — degrees, PDF convention. Returned for EVERY rotatable
|
|
2221
|
+
* kind (box AND vertex); the plugin converts it back to its CW-content
|
|
2222
|
+
* `rot` once at the repository seam.
|
|
2223
|
+
* - `UnrotatedRect` — the logical box. BOX kinds only (square/circle/
|
|
2224
|
+
* free-text); a vertex annotation never carries one (its points are the
|
|
2225
|
+
* visual), so vertex readers don't ask for it.
|
|
2226
|
+
*
|
|
2227
|
+
* Both are absent on un-rotated annotations (and on everything authored before
|
|
2228
|
+
* rotation shipped), so both reads return `undefined` and the DTO field stays
|
|
2229
|
+
* unset.
|
|
2230
|
+
*/
|
|
2231
|
+
const KEY_ROTATION$1 = "Rotation";
|
|
2232
|
+
const KEY_UNROTATED_RECT$1 = "UnrotatedRect";
|
|
2233
|
+
/** The advisory/portable rotation scalar (degrees, PDF convention), or
|
|
2234
|
+
* `undefined` when absent or zero. */
|
|
2235
|
+
function readAnnotationRotation(fn, mem, annotPtr) {
|
|
2236
|
+
if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) return void 0;
|
|
2237
|
+
return withScratch(mem, 4, (buf) => {
|
|
2238
|
+
if (!fn.EPDFAnnot_GetEmbedMetadataNumber(annotPtr, KEY_ROTATION$1, buf)) return void 0;
|
|
2239
|
+
const v = readF32(mem, buf);
|
|
2240
|
+
return v ? v : void 0;
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
/** The logical (unrotated) box of a rotated BOX annotation, or `undefined`. */
|
|
2244
|
+
function readAnnotationUnrotatedRect(fn, mem, annotPtr) {
|
|
2245
|
+
if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) return void 0;
|
|
2246
|
+
return withScratch(mem, 16, (buf) => {
|
|
2247
|
+
if (!fn.EPDFAnnot_GetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT$1, buf)) return void 0;
|
|
2248
|
+
const r = readRectF(mem, buf);
|
|
2249
|
+
return {
|
|
2250
|
+
left: r.left,
|
|
2251
|
+
bottom: r.bottom,
|
|
2252
|
+
right: r.right,
|
|
2253
|
+
top: r.top
|
|
2254
|
+
};
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
//#endregion
|
|
2156
2258
|
//#region src/features/annotations/internal/textEditIntent.ts
|
|
2157
2259
|
/** Adobe-compatible `/IT` names, normalized at the engine-core boundary. */
|
|
2158
2260
|
const IT_REPLACE = "Replace";
|
|
@@ -2187,13 +2289,17 @@ function readCaret(fn, mem, annotPtr, base) {
|
|
|
2187
2289
|
const opacity = ca == null ? 1 : Math.max(0, Math.min(1, ca));
|
|
2188
2290
|
const rd = readRectangleDifferences(fn, mem, annotPtr);
|
|
2189
2291
|
const intent = caretIntentFromName(readIntent(fn, mem, annotPtr));
|
|
2292
|
+
const rotation = readAnnotationRotation(fn, mem, annotPtr);
|
|
2293
|
+
const unrotatedRect = readAnnotationUnrotatedRect(fn, mem, annotPtr);
|
|
2190
2294
|
return {
|
|
2191
2295
|
...base,
|
|
2192
2296
|
subtype: "caret",
|
|
2193
2297
|
intent,
|
|
2194
2298
|
color,
|
|
2195
2299
|
opacity,
|
|
2196
|
-
rectDifferences: rd
|
|
2300
|
+
rectDifferences: rd,
|
|
2301
|
+
...rotation != null ? { rotation } : {},
|
|
2302
|
+
...unrotatedRect ? { unrotatedRect } : {}
|
|
2197
2303
|
};
|
|
2198
2304
|
}
|
|
2199
2305
|
//#endregion
|
|
@@ -2611,49 +2717,6 @@ function readFilledStyleExtras(fn, mem, annotPtr) {
|
|
|
2611
2717
|
};
|
|
2612
2718
|
}
|
|
2613
2719
|
//#endregion
|
|
2614
|
-
//#region src/features/annotations/internal/read/readAnnotationTransformMetadata.ts
|
|
2615
|
-
/**
|
|
2616
|
-
* Read-side twin of `writers/.../writeAnnotationTransformMetadata.ts`. Decodes
|
|
2617
|
-
* the EmbedPDF rotation keys from /EMBD_Metadata back onto the DTO:
|
|
2618
|
-
*
|
|
2619
|
-
* - `Rotation` — degrees, PDF convention. Returned for EVERY rotatable
|
|
2620
|
-
* kind (box AND vertex); the plugin converts it back to its CW-content
|
|
2621
|
-
* `rot` once at the repository seam.
|
|
2622
|
-
* - `UnrotatedRect` — the logical box. BOX kinds only (square/circle/
|
|
2623
|
-
* free-text); a vertex annotation never carries one (its points are the
|
|
2624
|
-
* visual), so vertex readers don't ask for it.
|
|
2625
|
-
*
|
|
2626
|
-
* Both are absent on un-rotated annotations (and on everything authored before
|
|
2627
|
-
* rotation shipped), so both reads return `undefined` and the DTO field stays
|
|
2628
|
-
* unset.
|
|
2629
|
-
*/
|
|
2630
|
-
const KEY_ROTATION$1 = "Rotation";
|
|
2631
|
-
const KEY_UNROTATED_RECT$1 = "UnrotatedRect";
|
|
2632
|
-
/** The advisory/portable rotation scalar (degrees, PDF convention), or
|
|
2633
|
-
* `undefined` when absent or zero. */
|
|
2634
|
-
function readAnnotationRotation(fn, mem, annotPtr) {
|
|
2635
|
-
if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) return void 0;
|
|
2636
|
-
return withScratch(mem, 4, (buf) => {
|
|
2637
|
-
if (!fn.EPDFAnnot_GetEmbedMetadataNumber(annotPtr, KEY_ROTATION$1, buf)) return void 0;
|
|
2638
|
-
const v = readF32(mem, buf);
|
|
2639
|
-
return v ? v : void 0;
|
|
2640
|
-
});
|
|
2641
|
-
}
|
|
2642
|
-
/** The logical (unrotated) box of a rotated BOX annotation, or `undefined`. */
|
|
2643
|
-
function readAnnotationUnrotatedRect(fn, mem, annotPtr) {
|
|
2644
|
-
if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) return void 0;
|
|
2645
|
-
return withScratch(mem, 16, (buf) => {
|
|
2646
|
-
if (!fn.EPDFAnnot_GetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT$1, buf)) return void 0;
|
|
2647
|
-
const r = readRectF(mem, buf);
|
|
2648
|
-
return {
|
|
2649
|
-
left: r.left,
|
|
2650
|
-
bottom: r.bottom,
|
|
2651
|
-
right: r.right,
|
|
2652
|
-
top: r.top
|
|
2653
|
-
};
|
|
2654
|
-
});
|
|
2655
|
-
}
|
|
2656
|
-
//#endregion
|
|
2657
2720
|
//#region src/features/annotations/internal/read/readFreeTextAnnotation.ts
|
|
2658
2721
|
/** Default `/DA` colour (black) when an annotation has no default appearance. */
|
|
2659
2722
|
const DEFAULT_FREETEXT_COLOR$1 = {
|
|
@@ -3106,6 +3169,47 @@ function readStampName(fn, mem, annotPtr) {
|
|
|
3106
3169
|
return readAnnotString(fn, mem, annotPtr, "Name");
|
|
3107
3170
|
}
|
|
3108
3171
|
//#endregion
|
|
3172
|
+
//#region src/features/annotations/internal/annotationState.ts
|
|
3173
|
+
/**
|
|
3174
|
+
* `/State` + `/StateModel` spelling maps — ISO 32000 §12.5.6.3, Table 174.
|
|
3175
|
+
*
|
|
3176
|
+
* Known values are wire-stable lowercase (the house vocabulary — same
|
|
3177
|
+
* rationale as `AnnotationReplyType` / `AnnotationBorderStyle`); the PDF
|
|
3178
|
+
* stores the capitalized Table 174 spellings. Unknown values — custom
|
|
3179
|
+
* Acrobat state models and their states — pass through VERBATIM in both
|
|
3180
|
+
* directions, so foreign review workflows survive a round trip.
|
|
3181
|
+
*/
|
|
3182
|
+
const STATE_MODEL_TO_PDF = {
|
|
3183
|
+
review: "Review",
|
|
3184
|
+
marked: "Marked"
|
|
3185
|
+
};
|
|
3186
|
+
const STATE_TO_PDF = {
|
|
3187
|
+
accepted: "Accepted",
|
|
3188
|
+
rejected: "Rejected",
|
|
3189
|
+
cancelled: "Cancelled",
|
|
3190
|
+
completed: "Completed",
|
|
3191
|
+
none: "None",
|
|
3192
|
+
marked: "Marked",
|
|
3193
|
+
unmarked: "Unmarked"
|
|
3194
|
+
};
|
|
3195
|
+
const PDF_TO_STATE_MODEL = invert(STATE_MODEL_TO_PDF);
|
|
3196
|
+
const PDF_TO_STATE = invert(STATE_TO_PDF);
|
|
3197
|
+
function stateModelToPdf(value) {
|
|
3198
|
+
return STATE_MODEL_TO_PDF[value] ?? value;
|
|
3199
|
+
}
|
|
3200
|
+
function stateModelFromPdf(value) {
|
|
3201
|
+
return PDF_TO_STATE_MODEL[value] ?? value;
|
|
3202
|
+
}
|
|
3203
|
+
function stateToPdf(value) {
|
|
3204
|
+
return STATE_TO_PDF[value] ?? value;
|
|
3205
|
+
}
|
|
3206
|
+
function stateFromPdf(value) {
|
|
3207
|
+
return PDF_TO_STATE[value] ?? value;
|
|
3208
|
+
}
|
|
3209
|
+
function invert(map) {
|
|
3210
|
+
return Object.fromEntries(Object.entries(map).map(([wire, pdf]) => [pdf, wire]));
|
|
3211
|
+
}
|
|
3212
|
+
//#endregion
|
|
3109
3213
|
//#region src/features/annotations/internal/read/readTextAnnotation.ts
|
|
3110
3214
|
/** Default `/C` — matches the generator's yellow note fill and the writer default. */
|
|
3111
3215
|
const DEFAULT_NOTE_COLOR$1 = {
|
|
@@ -3120,12 +3224,16 @@ function readText(fn, mem, annotPtr, base) {
|
|
|
3120
3224
|
const ca = readAnnotOpacity(fn, mem, annotPtr);
|
|
3121
3225
|
const opacity = ca == null ? 1 : Math.max(0, Math.min(1, ca));
|
|
3122
3226
|
const icon = NOTE_CODE_TO_ICON[fn.EPDFAnnot_GetName(annotPtr)] ?? DEFAULT_NOTE_ICON;
|
|
3227
|
+
const stateRaw = readAnnotString(fn, mem, annotPtr, "State");
|
|
3228
|
+
const stateModelRaw = readAnnotString(fn, mem, annotPtr, "StateModel");
|
|
3123
3229
|
return {
|
|
3124
3230
|
...base,
|
|
3125
3231
|
subtype: "text",
|
|
3126
3232
|
icon,
|
|
3127
3233
|
color,
|
|
3128
|
-
opacity
|
|
3234
|
+
opacity,
|
|
3235
|
+
state: stateRaw === null ? null : stateFromPdf(stateRaw),
|
|
3236
|
+
stateModel: stateModelRaw === null ? null : stateModelFromPdf(stateModelRaw)
|
|
3129
3237
|
};
|
|
3130
3238
|
}
|
|
3131
3239
|
//#endregion
|
|
@@ -3840,6 +3948,7 @@ function readAnnotationBase(fn, mem, annotPtr, pageObjectNumber, index, revision
|
|
|
3840
3948
|
const rect = readAnnotRect(fn, mem, annotPtr);
|
|
3841
3949
|
const flags = readAnnotFlags(fn, annotPtr);
|
|
3842
3950
|
const contents = readAnnotString(fn, mem, annotPtr, "Contents");
|
|
3951
|
+
const subject = readAnnotString(fn, mem, annotPtr, "Subj");
|
|
3843
3952
|
const author = readAnnotString(fn, mem, annotPtr, "T");
|
|
3844
3953
|
const createdRaw = readAnnotString(fn, mem, annotPtr, "CreationDate");
|
|
3845
3954
|
const modifiedRaw = readAnnotString(fn, mem, annotPtr, "M");
|
|
@@ -3856,6 +3965,7 @@ function readAnnotationBase(fn, mem, annotPtr, pageObjectNumber, index, revision
|
|
|
3856
3965
|
flags,
|
|
3857
3966
|
rect,
|
|
3858
3967
|
contents,
|
|
3968
|
+
subject,
|
|
3859
3969
|
author,
|
|
3860
3970
|
created: createdRaw ? pdfDateToIso(createdRaw) : null,
|
|
3861
3971
|
modified: modifiedRaw ? pdfDateToIso(modifiedRaw) : null,
|
|
@@ -4130,19 +4240,31 @@ const ANNOT_SUBTYPE_WIDGET = 20;
|
|
|
4130
4240
|
/** `FPDF_ANNOT_FREETEXT` — free-text annotation subtype code. */
|
|
4131
4241
|
const ANNOT_SUBTYPE_FREETEXT = 3;
|
|
4132
4242
|
/**
|
|
4133
|
-
* BOX-family subtypes (free-text 3, square 5, circle 6, stamp 13):
|
|
4134
|
-
* whose v3 writers put rotation in the AP `/Matrix` +
|
|
4135
|
-
* `/UnrotatedRect`. Only these are eligible for
|
|
4136
|
-
* rendering — vertex kinds
|
|
4137
|
-
* geometry, so their rasters must
|
|
4243
|
+
* BOX-family subtypes (free-text 3, square 5, circle 6, stamp 13, caret 14):
|
|
4244
|
+
* the kinds whose v3 writers put rotation in the AP `/Matrix` +
|
|
4245
|
+
* `/EMBD_Metadata` `/UnrotatedRect`. Only these are eligible for
|
|
4246
|
+
* rotation-stripped appearance rendering — vertex kinds
|
|
4247
|
+
* (line/polyline/polygon/ink) pre-rotate their geometry, so their rasters must
|
|
4248
|
+
* stay on the classic path. This set MUST cover every kind whose reader
|
|
4249
|
+
* surfaces the rotation pair: `fromDTO` (plugin-annotation repository) mirrors
|
|
4250
|
+
* this exact condition to re-apply the stripped rotation as `apRot`.
|
|
4138
4251
|
*/
|
|
4139
4252
|
const BOX_FAMILY_SUBTYPES = /* @__PURE__ */ new Set([
|
|
4140
4253
|
3,
|
|
4141
4254
|
5,
|
|
4142
4255
|
6,
|
|
4143
|
-
13
|
|
4256
|
+
13,
|
|
4257
|
+
14
|
|
4144
4258
|
]);
|
|
4145
4259
|
/**
|
|
4260
|
+
* Per-appearance output-pixel ceiling (see the clamp in `renderOne`).
|
|
4261
|
+
* 16 M px ≈ a 4000² raster ≈ 64 MB of transient RGBA — safely inside the
|
|
4262
|
+
* wasm heap for the one-at-a-time batch loop, while a page-sized annotation
|
|
4263
|
+
* stays crisp to roughly 5–6× before its appearance starts stretching.
|
|
4264
|
+
* Small annotations (the overwhelming majority) never come near it.
|
|
4265
|
+
*/
|
|
4266
|
+
const APPEARANCE_PIXEL_CLAMP = 16e6;
|
|
4267
|
+
/**
|
|
4146
4268
|
* Maps an `AnnotationAppearanceMode` onto the PDFium appearance-mode int and
|
|
4147
4269
|
* the `EPDFAnnot_GetAvailableAppearanceModes` bit it occupies.
|
|
4148
4270
|
* N -> mode 0 / bit 1, R -> mode 1 / bit 2, D -> mode 2 / bit 4.
|
|
@@ -4241,13 +4363,15 @@ var AnnotationAppearanceReader = class {
|
|
|
4241
4363
|
fn.EPDFAnnot_GenerateFormFieldAP(annotPtr);
|
|
4242
4364
|
if (!fn.EPDFAnnot_HasAppearanceStream(annotPtr, modeInt)) return null;
|
|
4243
4365
|
} else return null;
|
|
4366
|
+
const rectArea = Math.max(1, (rect.right - rect.left) * (rect.top - rect.bottom));
|
|
4367
|
+
const effScale = Math.min(scale, Math.sqrt(APPEARANCE_PIXEL_CLAMP / rectArea));
|
|
4244
4368
|
return rasterize(this.runtime, {
|
|
4245
4369
|
rect,
|
|
4246
4370
|
page,
|
|
4247
4371
|
rotation,
|
|
4248
4372
|
viewport: {
|
|
4249
4373
|
kind: "scale",
|
|
4250
|
-
scale
|
|
4374
|
+
scale: effScale
|
|
4251
4375
|
},
|
|
4252
4376
|
...maxOutputPixels !== void 0 ? { maxOutputPixels } : {},
|
|
4253
4377
|
background: "transparent",
|
|
@@ -4362,7 +4486,9 @@ function setAnnotFlags(fn, annotPtr, partial) {
|
|
|
4362
4486
|
if (!fn.FPDFAnnot_SetFlags(annotPtr, next)) throw new EngineError(EngineErrorCode.Unknown, "FPDFAnnot_SetFlags returned false");
|
|
4363
4487
|
}
|
|
4364
4488
|
/**
|
|
4365
|
-
* Write `/Rect`
|
|
4489
|
+
* Write `/Rect` without touching `/AP`. Appearance preservation/regeneration
|
|
4490
|
+
* belongs to AnnotationMutator; a geometry primitive must not silently rewrite
|
|
4491
|
+
* an existing appearance `/BBox`. FS_RECTF layout is
|
|
4366
4492
|
* `{ left, top, right, bottom }` (top > bottom in PDF coords).
|
|
4367
4493
|
*/
|
|
4368
4494
|
function setAnnotRect(fn, mem, annotPtr, rect) {
|
|
@@ -4372,12 +4498,40 @@ function setAnnotRect(fn, mem, annotPtr, rect) {
|
|
|
4372
4498
|
mem.poke(buf, "f32", rect.top, 4);
|
|
4373
4499
|
mem.poke(buf, "f32", rect.right, 8);
|
|
4374
4500
|
mem.poke(buf, "f32", rect.bottom, 12);
|
|
4375
|
-
if (!fn.
|
|
4501
|
+
if (!fn.EPDFAnnot_SetRect(annotPtr, buf)) throw new EngineError(EngineErrorCode.Unknown, "EPDFAnnot_SetRect returned false");
|
|
4376
4502
|
} finally {
|
|
4377
4503
|
mem.free(buf);
|
|
4378
4504
|
}
|
|
4379
4505
|
}
|
|
4380
4506
|
/**
|
|
4507
|
+
* Write a UTF-16 string entry into an annotation dictionary. The write
|
|
4508
|
+
* side of `readAnnotString`.
|
|
4509
|
+
*/
|
|
4510
|
+
function writeAnnotString(fn, mem, annotPtr, key, value) {
|
|
4511
|
+
const ptr = mem.writeU16String(value);
|
|
4512
|
+
try {
|
|
4513
|
+
fn.FPDFAnnot_SetStringValue(annotPtr, key, ptr);
|
|
4514
|
+
} finally {
|
|
4515
|
+
mem.free(ptr);
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
/**
|
|
4519
|
+
* Three-state string write: `null` REMOVES the dictionary entry.
|
|
4520
|
+
*
|
|
4521
|
+
* Clearing goes through `EPDFAnnot_RemoveKey` because
|
|
4522
|
+
* `FPDFAnnot_SetStringValue` with a NULL value writes an EMPTY string —
|
|
4523
|
+
* it never removes. True removal is what keeps the read side honest:
|
|
4524
|
+
* `readAnnotString` returns `null` iff the key is absent, so a cleared
|
|
4525
|
+
* field reads back as `null`, not `''`.
|
|
4526
|
+
*/
|
|
4527
|
+
function writeAnnotStringOrClear(fn, mem, annotPtr, key, value) {
|
|
4528
|
+
if (value === null) {
|
|
4529
|
+
fn.EPDFAnnot_RemoveKey(annotPtr, key);
|
|
4530
|
+
return;
|
|
4531
|
+
}
|
|
4532
|
+
writeAnnotString(fn, mem, annotPtr, key, value);
|
|
4533
|
+
}
|
|
4534
|
+
/**
|
|
4381
4535
|
* Set `/BS /S` (border style code) and `/BS /W` (width) in one call via
|
|
4382
4536
|
* `EPDFAnnot_SetBorderStyle`. `styleCode` is a raw
|
|
4383
4537
|
* `FPDF_ANNOT_BORDER_STYLE` enum value — the string<->code mapping lives
|
|
@@ -4576,8 +4730,8 @@ function setCalloutLine(fn, mem, annotPtr, points) {
|
|
|
4576
4730
|
//#region src/features/annotations/internal/write/writeAnnotationBase.ts
|
|
4577
4731
|
/**
|
|
4578
4732
|
* Write the annotation-wide base fields shared by every Draft
|
|
4579
|
-
* (contents/nm). The kind-specific writer calls this BEFORE its
|
|
4580
|
-
* field writes; order doesn't actually matter at the PDF level, but
|
|
4733
|
+
* (contents/subject/nm). The kind-specific writer calls this BEFORE its
|
|
4734
|
+
* own field writes; order doesn't actually matter at the PDF level, but
|
|
4581
4735
|
* keeping the base first makes the writers symmetric with the readers.
|
|
4582
4736
|
*
|
|
4583
4737
|
* `/T` (author display) is stamped by the mutator from
|
|
@@ -4588,13 +4742,14 @@ function setCalloutLine(fn, mem, annotPtr, points) {
|
|
|
4588
4742
|
* annotation; the writer does not.
|
|
4589
4743
|
*/
|
|
4590
4744
|
function applyAnnotationBaseDraft(fn, mem, annotPtr, draft) {
|
|
4591
|
-
if (draft.contents !== void 0)
|
|
4592
|
-
if (draft.
|
|
4745
|
+
if (draft.contents !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "Contents", draft.contents);
|
|
4746
|
+
if (draft.subject !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "Subj", draft.subject);
|
|
4747
|
+
if (draft.nm !== void 0 && draft.nm.length > 0) writeAnnotString(fn, mem, annotPtr, "NM", draft.nm);
|
|
4593
4748
|
if (draft.flags !== void 0) setAnnotFlags(fn, annotPtr, draft.flags);
|
|
4594
4749
|
}
|
|
4595
4750
|
/**
|
|
4596
4751
|
* Write the annotation-wide base fields shared by every Patch
|
|
4597
|
-
* (contents). /T is bound at creation and not patchable — see
|
|
4752
|
+
* (contents/subject). /T is bound at creation and not patchable — see
|
|
4598
4753
|
* `AnnotationPatchBase`. /NM is monotonic per annotation; the mutator
|
|
4599
4754
|
* may stamp /NM opportunistically on a weak annotation via
|
|
4600
4755
|
* `writeAnnotationNm`.
|
|
@@ -4605,7 +4760,8 @@ function applyAnnotationBaseDraft(fn, mem, annotPtr, draft) {
|
|
|
4605
4760
|
* "..." -> set the entry to that value
|
|
4606
4761
|
*/
|
|
4607
4762
|
function applyAnnotationBasePatch(fn, mem, annotPtr, patch) {
|
|
4608
|
-
if (patch.contents !== void 0)
|
|
4763
|
+
if (patch.contents !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "Contents", patch.contents);
|
|
4764
|
+
if (patch.subject !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "Subj", patch.subject);
|
|
4609
4765
|
if (patch.flags !== void 0) setAnnotFlags(fn, annotPtr, patch.flags);
|
|
4610
4766
|
}
|
|
4611
4767
|
/**
|
|
@@ -4616,7 +4772,7 @@ function applyAnnotationBasePatch(fn, mem, annotPtr, patch) {
|
|
|
4616
4772
|
*/
|
|
4617
4773
|
function writeAnnotationAuthor(fn, mem, annotPtr, displayName) {
|
|
4618
4774
|
if (displayName.length === 0) return;
|
|
4619
|
-
|
|
4775
|
+
writeAnnotString(fn, mem, annotPtr, "T", displayName);
|
|
4620
4776
|
}
|
|
4621
4777
|
/**
|
|
4622
4778
|
* Stamp /NM on an annotation that didn't have one. Used by the mutator
|
|
@@ -4625,7 +4781,7 @@ function writeAnnotationAuthor(fn, mem, annotPtr, displayName) {
|
|
|
4625
4781
|
*/
|
|
4626
4782
|
function writeAnnotationNm(fn, mem, annotPtr, nm) {
|
|
4627
4783
|
if (nm.length === 0) return;
|
|
4628
|
-
|
|
4784
|
+
writeAnnotString(fn, mem, annotPtr, "NM", nm);
|
|
4629
4785
|
}
|
|
4630
4786
|
/**
|
|
4631
4787
|
* Stamp /M (modification date) on an annotation. /M is a standard
|
|
@@ -4642,22 +4798,7 @@ function writeAnnotationNm(fn, mem, annotPtr, nm) {
|
|
|
4642
4798
|
* Format: `D:YYYYMMDDHHmmSSOHH'mm'` (see formatPdfDate).
|
|
4643
4799
|
*/
|
|
4644
4800
|
function writeAnnotationModified(fn, mem, annotPtr, now = /* @__PURE__ */ new Date()) {
|
|
4645
|
-
|
|
4646
|
-
}
|
|
4647
|
-
function writeString(fn, mem, annotPtr, key, value) {
|
|
4648
|
-
const ptr = mem.writeU16String(value);
|
|
4649
|
-
try {
|
|
4650
|
-
fn.FPDFAnnot_SetStringValue(annotPtr, key, ptr);
|
|
4651
|
-
} finally {
|
|
4652
|
-
mem.free(ptr);
|
|
4653
|
-
}
|
|
4654
|
-
}
|
|
4655
|
-
function writeStringOrClear(fn, mem, annotPtr, key, value) {
|
|
4656
|
-
if (value === null) {
|
|
4657
|
-
fn.FPDFAnnot_SetStringValue(annotPtr, key, NULL_PTR);
|
|
4658
|
-
return;
|
|
4659
|
-
}
|
|
4660
|
-
writeString(fn, mem, annotPtr, key, value);
|
|
4801
|
+
writeAnnotString(fn, mem, annotPtr, "M", formatPdfDate(now));
|
|
4661
4802
|
}
|
|
4662
4803
|
//#endregion
|
|
4663
4804
|
//#region src/features/annotations/internal/identity/captureOrStampStableId.ts
|
|
@@ -4829,50 +4970,229 @@ function readAnnotationFromPtr(fn, mem, annotPtr, pageObjectNumber, index, revis
|
|
|
4829
4970
|
return reader(fn, mem, annotPtr, base, subtypeCode, ctx);
|
|
4830
4971
|
}
|
|
4831
4972
|
//#endregion
|
|
4832
|
-
//#region src/features/annotations/internal/write/
|
|
4833
|
-
/** Default `/C` colour when a caret draft omits it (engine-wide default mark). */
|
|
4834
|
-
const DEFAULT_CARET_COLOR = {
|
|
4835
|
-
r: 255,
|
|
4836
|
-
g: 0,
|
|
4837
|
-
b: 0
|
|
4838
|
-
};
|
|
4839
|
-
/** Default opacity, set explicitly so reads always round-trip the same value. */
|
|
4840
|
-
const DEFAULT_OPACITY$4 = 1;
|
|
4973
|
+
//#region src/features/annotations/internal/write/writeEmbedMetadata.ts
|
|
4841
4974
|
/**
|
|
4842
|
-
*
|
|
4843
|
-
*
|
|
4844
|
-
*
|
|
4845
|
-
*
|
|
4846
|
-
*
|
|
4847
|
-
*
|
|
4975
|
+
* Write the EmbedPDF-namespaced /EMBD_Metadata annotation dictionary.
|
|
4976
|
+
*
|
|
4977
|
+
* This module owns ONLY the vendor-extension dictionary. Standard PDF
|
|
4978
|
+
* base fields (/T, /M, /NM, /Contents) are handled by
|
|
4979
|
+
* `writers/annotations/base.ts` — /EMBD_Metadata is a separate
|
|
4980
|
+
* dictionary nested under the annotation that carries cloud-side
|
|
4981
|
+
* identity bookkeeping which has no PDF-spec equivalent.
|
|
4982
|
+
*
|
|
4983
|
+
* Schema (v1):
|
|
4984
|
+
* /EMBD_Metadata <<
|
|
4985
|
+
* /SchemaVersion 1 % EmbedPDF metadata schema version
|
|
4986
|
+
* /UserID (acme/alice) % identity that created the annotation
|
|
4987
|
+
* /GroupID (engineering) % group the annotation belongs to
|
|
4988
|
+
* /CreatedBy (acme/alice) % same as UserID on create
|
|
4989
|
+
* /UpdatedBy (acme/alice) % last editor; refreshed on every update
|
|
4990
|
+
* >>
|
|
4991
|
+
*
|
|
4992
|
+
* Read side: see `readers/annotations/embed-metadata.ts` (commit 12).
|
|
4993
|
+
*
|
|
4994
|
+
* PDFium bindings used: the `EPDFAnnot_*EmbedMetadata*` extension API
|
|
4995
|
+
* shipped in the EmbedPDF PDFium fork. Values are written as UTF-16LE
|
|
4996
|
+
* strings via the same `mem.writeU16String` pattern the base writer
|
|
4997
|
+
* uses for /T.
|
|
4848
4998
|
*/
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
if (draft.intent !== void 0) setIntent(fn, annotPtr, caretIntentToName(draft.intent));
|
|
4855
|
-
if (draft.rectDifferences != null) setRectangleDifferences(fn, annotPtr, draft.rectDifferences);
|
|
4856
|
-
}
|
|
4999
|
+
const KEY_SCHEMA_VERSION = "SchemaVersion";
|
|
5000
|
+
const KEY_USER_ID = "UserID";
|
|
5001
|
+
const KEY_GROUP_ID = "GroupID";
|
|
5002
|
+
const KEY_CREATED_BY = "CreatedBy";
|
|
5003
|
+
const KEY_UPDATED_BY = "UpdatedBy";
|
|
4857
5004
|
/**
|
|
4858
|
-
*
|
|
4859
|
-
*
|
|
5005
|
+
* Stamp /EMBD_Metadata on a freshly created annotation. No-op if the
|
|
5006
|
+
* actor has no identity fields — the worker still emits the standard
|
|
5007
|
+
* /M timestamp via the base writer; only the vendor dictionary is
|
|
5008
|
+
* skipped.
|
|
5009
|
+
*
|
|
5010
|
+
* actor.userId → /UserID, /CreatedBy, /UpdatedBy
|
|
5011
|
+
* actor.groupId → /GroupID
|
|
5012
|
+
*
|
|
5013
|
+
* /SchemaVersion is written whenever any field is written.
|
|
4860
5014
|
*/
|
|
4861
|
-
function
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
if (
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
5015
|
+
function applyEmbedMetadataOnCreate(fn, mem, annotPtr, actor) {
|
|
5016
|
+
if (!actor || !actor.userId && !actor.groupId) return;
|
|
5017
|
+
fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_SCHEMA_VERSION, 1);
|
|
5018
|
+
if (actor.userId) {
|
|
5019
|
+
setMetadataString(fn, mem, annotPtr, KEY_USER_ID, actor.userId);
|
|
5020
|
+
setMetadataString(fn, mem, annotPtr, KEY_CREATED_BY, actor.userId);
|
|
5021
|
+
setMetadataString(fn, mem, annotPtr, KEY_UPDATED_BY, actor.userId);
|
|
5022
|
+
}
|
|
5023
|
+
if (actor.groupId) setMetadataString(fn, mem, annotPtr, KEY_GROUP_ID, actor.groupId);
|
|
4869
5024
|
}
|
|
4870
5025
|
/**
|
|
4871
|
-
*
|
|
4872
|
-
*
|
|
5026
|
+
* Apply /EMBD_Metadata refreshes on update. Two fields can change on
|
|
5027
|
+
* update; everything else (UserID, CreatedBy) is immutable.
|
|
5028
|
+
*
|
|
5029
|
+
* actor.userId → /UpdatedBy (refreshed every update where the
|
|
5030
|
+
* caller has an identity)
|
|
5031
|
+
* actor.groupId → /GroupID (reassigned only when the patch
|
|
5032
|
+
* explicitly contains a new groupId; route runs
|
|
5033
|
+
* `checkSetGroup` first)
|
|
5034
|
+
*
|
|
5035
|
+
* If the target annotation has no EMBD_Metadata yet (created before
|
|
5036
|
+
* the cloud started stamping, or by a non-cloud writer), we seed
|
|
5037
|
+
* /SchemaVersion so subsequent readers can find the marker.
|
|
5038
|
+
*
|
|
5039
|
+
* No-op when the actor has neither userId nor groupId.
|
|
4873
5040
|
*/
|
|
4874
|
-
function
|
|
4875
|
-
|
|
5041
|
+
function applyEmbedMetadataOnUpdate(fn, mem, annotPtr, actor) {
|
|
5042
|
+
if (!actor || !actor.userId && !actor.groupId) return;
|
|
5043
|
+
if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_SCHEMA_VERSION, 1);
|
|
5044
|
+
if (actor.userId) setMetadataString(fn, mem, annotPtr, KEY_UPDATED_BY, actor.userId);
|
|
5045
|
+
if (actor.groupId) setMetadataString(fn, mem, annotPtr, KEY_GROUP_ID, actor.groupId);
|
|
5046
|
+
}
|
|
5047
|
+
function setMetadataString(fn, mem, annotPtr, key, value) {
|
|
5048
|
+
const ptr = mem.writeU16String(value);
|
|
5049
|
+
try {
|
|
5050
|
+
fn.EPDFAnnot_SetEmbedMetadataString(annotPtr, key, ptr);
|
|
5051
|
+
} finally {
|
|
5052
|
+
mem.free(ptr);
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
5055
|
+
//#endregion
|
|
5056
|
+
//#region src/features/annotations/internal/write/writeAnnotationTransformMetadata.ts
|
|
5057
|
+
/**
|
|
5058
|
+
* Write the EmbedPDF transform keys under /EMBD_Metadata. This is the seam
|
|
5059
|
+
* PDFium's native AP generator reads to bake a ROTATED appearance:
|
|
5060
|
+
*
|
|
5061
|
+
* /EMBD_Metadata <<
|
|
5062
|
+
* /Rotation 45 % degrees, PDF convention (CCW)
|
|
5063
|
+
* /UnrotatedRect [x0 y0 x1 y1] % BOX kinds only — the logical box
|
|
5064
|
+
* >>
|
|
5065
|
+
*
|
|
5066
|
+
* The family split (see the v3 plan):
|
|
5067
|
+
* - BOX kinds (square/circle/free-text): `/Rotation` + `/UnrotatedRect`.
|
|
5068
|
+
* With BOTH present the AP generator emits an `/AP /Matrix` that rotates a
|
|
5069
|
+
* box-sized appearance about the box centre, and `/Rect` is the enclosing
|
|
5070
|
+
* AABB. This is the portable, externally-correct rotation.
|
|
5071
|
+
* - VERTEX kinds (line/polyline/polygon/ink): `/Rotation` ONLY (advisory).
|
|
5072
|
+
* The points are already rotated (they are the visual), so a lone
|
|
5073
|
+
* `/Rotation` is INERT for the AP generator (it ignores `/Rotation` with no
|
|
5074
|
+
* `/UnrotatedRect`) — it just records the applied angle so EmbedPDF can
|
|
5075
|
+
* reconstruct an oriented selection box + offer reset.
|
|
5076
|
+
*
|
|
5077
|
+
* Both fields follow the engine-wide tri-state law ("a patch touches what it
|
|
5078
|
+
* states, preserves what it omits"):
|
|
5079
|
+
* - `undefined` → the key is UNTOUCHED (a rect-only move keeps its rotation).
|
|
5080
|
+
* - `null` or `0` → CLEAR just that key via `EPDFAnnot_ClearEmbedMetadataKey`
|
|
5081
|
+
* (0 ≡ no rotation is the canonical identity, not a sentinel; never clear
|
|
5082
|
+
* the whole dict — identity fields UserID/GroupID/CreatedBy/UpdatedBy must
|
|
5083
|
+
* survive).
|
|
5084
|
+
* - a value → SET the key. Setting a nonzero rotation with no
|
|
5085
|
+
* `/UnrotatedRect` (neither in the patch nor already on the annotation)
|
|
5086
|
+
* is an unsatisfiable state for the AP generator and throws `InvalidArg`.
|
|
5087
|
+
*
|
|
5088
|
+
* MUST run BEFORE `EPDFAnnot_GenerateAppearance` so the bake sees the rotation.
|
|
5089
|
+
* `/SchemaVersion` is seeded (stays 1) if this is the first key in the dict.
|
|
5090
|
+
*/
|
|
5091
|
+
const KEY_ROTATION = "Rotation";
|
|
5092
|
+
const KEY_UNROTATED_RECT = "UnrotatedRect";
|
|
5093
|
+
/** Seed `/SchemaVersion` when we are about to create the dict by writing the
|
|
5094
|
+
* first transform key, so the marker readers look for is always present. */
|
|
5095
|
+
function ensureSchemaVersion(fn, annotPtr) {
|
|
5096
|
+
if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, "SchemaVersion", 1);
|
|
5097
|
+
}
|
|
5098
|
+
function setRotation(fn, annotPtr, rotation) {
|
|
5099
|
+
ensureSchemaVersion(fn, annotPtr);
|
|
5100
|
+
fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_ROTATION, rotation);
|
|
5101
|
+
}
|
|
5102
|
+
function setUnrotatedRect(fn, mem, annotPtr, rect) {
|
|
5103
|
+
ensureSchemaVersion(fn, annotPtr);
|
|
5104
|
+
const buf = mem.alloc(16);
|
|
5105
|
+
try {
|
|
5106
|
+
mem.poke(buf, "f32", rect.left, 0);
|
|
5107
|
+
mem.poke(buf, "f32", rect.top, 4);
|
|
5108
|
+
mem.poke(buf, "f32", rect.right, 8);
|
|
5109
|
+
mem.poke(buf, "f32", rect.bottom, 12);
|
|
5110
|
+
fn.EPDFAnnot_SetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT, buf);
|
|
5111
|
+
} finally {
|
|
5112
|
+
mem.free(buf);
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
/**
|
|
5116
|
+
* Write transform metadata for a BOX kind (square/circle/free-text/stamp),
|
|
5117
|
+
* tri-state per field: `undefined` never touches the document (safe to call on
|
|
5118
|
+
* every patch), `null`/`0` clears, a value sets. A nonzero rotation with no
|
|
5119
|
+
* unrotated box anywhere (patch or annotation) is unsatisfiable — the AP
|
|
5120
|
+
* generator needs the pair — and throws instead of writing a broken state.
|
|
5121
|
+
*/
|
|
5122
|
+
function writeBoxTransformMetadata(fn, mem, annotPtr, t) {
|
|
5123
|
+
if (t.rotation !== void 0) if (t.rotation !== null && t.rotation !== 0) {
|
|
5124
|
+
if (!(t.unrotatedRect != null || t.unrotatedRect === void 0 && readAnnotationUnrotatedRect(fn, mem, annotPtr) != null)) throw new EngineError(EngineErrorCode.InvalidArg, "rotation requires unrotatedRect: supply the pair together (or rely on an existing /EMBD_Metadata/UnrotatedRect)");
|
|
5125
|
+
setRotation(fn, annotPtr, t.rotation);
|
|
5126
|
+
} else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
|
|
5127
|
+
if (t.unrotatedRect !== void 0) if (t.unrotatedRect !== null) setUnrotatedRect(fn, mem, annotPtr, t.unrotatedRect);
|
|
5128
|
+
else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
|
|
5129
|
+
}
|
|
5130
|
+
/**
|
|
5131
|
+
* Write the advisory `/Rotation` scalar for a VERTEX kind
|
|
5132
|
+
* (line/polyline/polygon/ink), tri-state: `undefined` never touches the
|
|
5133
|
+
* document, `null`/`0` clears, a value sets. Whenever it does write, any
|
|
5134
|
+
* `/UnrotatedRect` is defensively cleared — that key is an impossible state on
|
|
5135
|
+
* a vertex kind and must never accidentally drive the AP generator.
|
|
5136
|
+
*/
|
|
5137
|
+
function writeVertexTransformMetadata(fn, annotPtr, t) {
|
|
5138
|
+
if (t.rotation === void 0) return;
|
|
5139
|
+
if (t.rotation !== null && t.rotation !== 0) setRotation(fn, annotPtr, t.rotation);
|
|
5140
|
+
else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
|
|
5141
|
+
fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
|
|
5142
|
+
}
|
|
5143
|
+
//#endregion
|
|
5144
|
+
//#region src/features/annotations/internal/write/writeCaretAnnotation.ts
|
|
5145
|
+
/** Default `/C` colour when a caret draft omits it (engine-wide default mark). */
|
|
5146
|
+
const DEFAULT_CARET_COLOR = {
|
|
5147
|
+
r: 255,
|
|
5148
|
+
g: 0,
|
|
5149
|
+
b: 0
|
|
5150
|
+
};
|
|
5151
|
+
/** Default opacity, set explicitly so reads always round-trip the same value. */
|
|
5152
|
+
const DEFAULT_OPACITY$4 = 1;
|
|
5153
|
+
/**
|
|
5154
|
+
* Apply a caret draft to a freshly-created annotation. Caret carries no
|
|
5155
|
+
* geometry of its own beyond `/Rect`. Order:
|
|
5156
|
+
* 1. base author-metadata (contents/nm/flags)
|
|
5157
|
+
* 2. `/Rect` (required — supplied by the caller; never derived)
|
|
5158
|
+
* 3. `/C` color + `/CA` opacity
|
|
5159
|
+
* 4. `/RD` rectangle differences (optional)
|
|
5160
|
+
*/
|
|
5161
|
+
function applyCaretDraft(fn, mem, annotPtr, draft) {
|
|
5162
|
+
applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
|
|
5163
|
+
setAnnotRect(fn, mem, annotPtr, draft.rect);
|
|
5164
|
+
writeBoxTransformMetadata(fn, mem, annotPtr, {
|
|
5165
|
+
rotation: draft.rotation,
|
|
5166
|
+
unrotatedRect: draft.unrotatedRect
|
|
5167
|
+
});
|
|
5168
|
+
setAnnotColor(fn, annotPtr, draft.color ?? DEFAULT_CARET_COLOR);
|
|
5169
|
+
setAnnotOpacity(fn, annotPtr, draft.opacity ?? DEFAULT_OPACITY$4);
|
|
5170
|
+
if (draft.intent !== void 0) setIntent(fn, annotPtr, caretIntentToName(draft.intent));
|
|
5171
|
+
if (draft.rectDifferences != null) setRectangleDifferences(fn, annotPtr, draft.rectDifferences);
|
|
5172
|
+
}
|
|
5173
|
+
/**
|
|
5174
|
+
* Apply a caret patch to an existing annotation. Only present fields are
|
|
5175
|
+
* touched.
|
|
5176
|
+
*/
|
|
5177
|
+
function applyCaretPatch(fn, mem, annotPtr, patch) {
|
|
5178
|
+
applyAnnotationBasePatch(fn, mem, annotPtr, patch);
|
|
5179
|
+
if (patch.rect !== void 0) setAnnotRect(fn, mem, annotPtr, patch.rect);
|
|
5180
|
+
if (patch.rotation !== void 0 || patch.unrotatedRect !== void 0) writeBoxTransformMetadata(fn, mem, annotPtr, {
|
|
5181
|
+
rotation: patch.rotation,
|
|
5182
|
+
unrotatedRect: patch.unrotatedRect
|
|
5183
|
+
});
|
|
5184
|
+
if (patch.color !== void 0) setAnnotColor(fn, annotPtr, patch.color);
|
|
5185
|
+
if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
|
|
5186
|
+
if (patch.intent !== void 0) setIntent(fn, annotPtr, caretIntentToName(patch.intent));
|
|
5187
|
+
if (patch.rectDifferences === null) clearRectangleDifferences(fn, annotPtr);
|
|
5188
|
+
else if (patch.rectDifferences !== void 0) setRectangleDifferences(fn, annotPtr, patch.rectDifferences);
|
|
5189
|
+
}
|
|
5190
|
+
/**
|
|
5191
|
+
* Type-narrowing predicate used by the writer registry to pick the caret
|
|
5192
|
+
* writer for a draft/patch's `subtype`.
|
|
5193
|
+
*/
|
|
5194
|
+
function isCaretSubtype(subtype) {
|
|
5195
|
+
return subtype === "caret";
|
|
4876
5196
|
}
|
|
4877
5197
|
//#endregion
|
|
4878
5198
|
//#region src/features/annotations/internal/write/writeFileAttachmentAnnotation.ts
|
|
@@ -5039,177 +5359,6 @@ function applyFilledStylePatch(fn, mem, annotPtr, patch) {
|
|
|
5039
5359
|
applyGeometryStylePatch(fn, mem, annotPtr, patch);
|
|
5040
5360
|
}
|
|
5041
5361
|
//#endregion
|
|
5042
|
-
//#region src/features/annotations/internal/write/writeEmbedMetadata.ts
|
|
5043
|
-
/**
|
|
5044
|
-
* Write the EmbedPDF-namespaced /EMBD_Metadata annotation dictionary.
|
|
5045
|
-
*
|
|
5046
|
-
* This module owns ONLY the vendor-extension dictionary. Standard PDF
|
|
5047
|
-
* base fields (/T, /M, /NM, /Contents) are handled by
|
|
5048
|
-
* `writers/annotations/base.ts` — /EMBD_Metadata is a separate
|
|
5049
|
-
* dictionary nested under the annotation that carries cloud-side
|
|
5050
|
-
* identity bookkeeping which has no PDF-spec equivalent.
|
|
5051
|
-
*
|
|
5052
|
-
* Schema (v1):
|
|
5053
|
-
* /EMBD_Metadata <<
|
|
5054
|
-
* /SchemaVersion 1 % EmbedPDF metadata schema version
|
|
5055
|
-
* /UserID (acme/alice) % identity that created the annotation
|
|
5056
|
-
* /GroupID (engineering) % group the annotation belongs to
|
|
5057
|
-
* /CreatedBy (acme/alice) % same as UserID on create
|
|
5058
|
-
* /UpdatedBy (acme/alice) % last editor; refreshed on every update
|
|
5059
|
-
* >>
|
|
5060
|
-
*
|
|
5061
|
-
* Read side: see `readers/annotations/embed-metadata.ts` (commit 12).
|
|
5062
|
-
*
|
|
5063
|
-
* PDFium bindings used: the `EPDFAnnot_*EmbedMetadata*` extension API
|
|
5064
|
-
* shipped in the EmbedPDF PDFium fork. Values are written as UTF-16LE
|
|
5065
|
-
* strings via the same `mem.writeU16String` pattern the base writer
|
|
5066
|
-
* uses for /T.
|
|
5067
|
-
*/
|
|
5068
|
-
const KEY_SCHEMA_VERSION = "SchemaVersion";
|
|
5069
|
-
const KEY_USER_ID = "UserID";
|
|
5070
|
-
const KEY_GROUP_ID = "GroupID";
|
|
5071
|
-
const KEY_CREATED_BY = "CreatedBy";
|
|
5072
|
-
const KEY_UPDATED_BY = "UpdatedBy";
|
|
5073
|
-
/**
|
|
5074
|
-
* Stamp /EMBD_Metadata on a freshly created annotation. No-op if the
|
|
5075
|
-
* actor has no identity fields — the worker still emits the standard
|
|
5076
|
-
* /M timestamp via the base writer; only the vendor dictionary is
|
|
5077
|
-
* skipped.
|
|
5078
|
-
*
|
|
5079
|
-
* actor.userId → /UserID, /CreatedBy, /UpdatedBy
|
|
5080
|
-
* actor.groupId → /GroupID
|
|
5081
|
-
*
|
|
5082
|
-
* /SchemaVersion is written whenever any field is written.
|
|
5083
|
-
*/
|
|
5084
|
-
function applyEmbedMetadataOnCreate(fn, mem, annotPtr, actor) {
|
|
5085
|
-
if (!actor || !actor.userId && !actor.groupId) return;
|
|
5086
|
-
fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_SCHEMA_VERSION, 1);
|
|
5087
|
-
if (actor.userId) {
|
|
5088
|
-
setMetadataString(fn, mem, annotPtr, KEY_USER_ID, actor.userId);
|
|
5089
|
-
setMetadataString(fn, mem, annotPtr, KEY_CREATED_BY, actor.userId);
|
|
5090
|
-
setMetadataString(fn, mem, annotPtr, KEY_UPDATED_BY, actor.userId);
|
|
5091
|
-
}
|
|
5092
|
-
if (actor.groupId) setMetadataString(fn, mem, annotPtr, KEY_GROUP_ID, actor.groupId);
|
|
5093
|
-
}
|
|
5094
|
-
/**
|
|
5095
|
-
* Apply /EMBD_Metadata refreshes on update. Two fields can change on
|
|
5096
|
-
* update; everything else (UserID, CreatedBy) is immutable.
|
|
5097
|
-
*
|
|
5098
|
-
* actor.userId → /UpdatedBy (refreshed every update where the
|
|
5099
|
-
* caller has an identity)
|
|
5100
|
-
* actor.groupId → /GroupID (reassigned only when the patch
|
|
5101
|
-
* explicitly contains a new groupId; route runs
|
|
5102
|
-
* `checkSetGroup` first)
|
|
5103
|
-
*
|
|
5104
|
-
* If the target annotation has no EMBD_Metadata yet (created before
|
|
5105
|
-
* the cloud started stamping, or by a non-cloud writer), we seed
|
|
5106
|
-
* /SchemaVersion so subsequent readers can find the marker.
|
|
5107
|
-
*
|
|
5108
|
-
* No-op when the actor has neither userId nor groupId.
|
|
5109
|
-
*/
|
|
5110
|
-
function applyEmbedMetadataOnUpdate(fn, mem, annotPtr, actor) {
|
|
5111
|
-
if (!actor || !actor.userId && !actor.groupId) return;
|
|
5112
|
-
if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_SCHEMA_VERSION, 1);
|
|
5113
|
-
if (actor.userId) setMetadataString(fn, mem, annotPtr, KEY_UPDATED_BY, actor.userId);
|
|
5114
|
-
if (actor.groupId) setMetadataString(fn, mem, annotPtr, KEY_GROUP_ID, actor.groupId);
|
|
5115
|
-
}
|
|
5116
|
-
function setMetadataString(fn, mem, annotPtr, key, value) {
|
|
5117
|
-
const ptr = mem.writeU16String(value);
|
|
5118
|
-
try {
|
|
5119
|
-
fn.EPDFAnnot_SetEmbedMetadataString(annotPtr, key, ptr);
|
|
5120
|
-
} finally {
|
|
5121
|
-
mem.free(ptr);
|
|
5122
|
-
}
|
|
5123
|
-
}
|
|
5124
|
-
//#endregion
|
|
5125
|
-
//#region src/features/annotations/internal/write/writeAnnotationTransformMetadata.ts
|
|
5126
|
-
/**
|
|
5127
|
-
* Write the EmbedPDF transform keys under /EMBD_Metadata. This is the seam
|
|
5128
|
-
* PDFium's native AP generator reads to bake a ROTATED appearance:
|
|
5129
|
-
*
|
|
5130
|
-
* /EMBD_Metadata <<
|
|
5131
|
-
* /Rotation 45 % degrees, PDF convention (CCW)
|
|
5132
|
-
* /UnrotatedRect [x0 y0 x1 y1] % BOX kinds only — the logical box
|
|
5133
|
-
* >>
|
|
5134
|
-
*
|
|
5135
|
-
* The family split (see the v3 plan):
|
|
5136
|
-
* - BOX kinds (square/circle/free-text): `/Rotation` + `/UnrotatedRect`.
|
|
5137
|
-
* With BOTH present the AP generator emits an `/AP /Matrix` that rotates a
|
|
5138
|
-
* box-sized appearance about the box centre, and `/Rect` is the enclosing
|
|
5139
|
-
* AABB. This is the portable, externally-correct rotation.
|
|
5140
|
-
* - VERTEX kinds (line/polyline/polygon/ink): `/Rotation` ONLY (advisory).
|
|
5141
|
-
* The points are already rotated (they are the visual), so a lone
|
|
5142
|
-
* `/Rotation` is INERT for the AP generator (it ignores `/Rotation` with no
|
|
5143
|
-
* `/UnrotatedRect`) — it just records the applied angle so EmbedPDF can
|
|
5144
|
-
* reconstruct an oriented selection box + offer reset.
|
|
5145
|
-
*
|
|
5146
|
-
* Both fields follow the engine-wide tri-state law ("a patch touches what it
|
|
5147
|
-
* states, preserves what it omits"):
|
|
5148
|
-
* - `undefined` → the key is UNTOUCHED (a rect-only move keeps its rotation).
|
|
5149
|
-
* - `null` or `0` → CLEAR just that key via `EPDFAnnot_ClearEmbedMetadataKey`
|
|
5150
|
-
* (0 ≡ no rotation is the canonical identity, not a sentinel; never clear
|
|
5151
|
-
* the whole dict — identity fields UserID/GroupID/CreatedBy/UpdatedBy must
|
|
5152
|
-
* survive).
|
|
5153
|
-
* - a value → SET the key. Setting a nonzero rotation with no
|
|
5154
|
-
* `/UnrotatedRect` (neither in the patch nor already on the annotation)
|
|
5155
|
-
* is an unsatisfiable state for the AP generator and throws `InvalidArg`.
|
|
5156
|
-
*
|
|
5157
|
-
* MUST run BEFORE `EPDFAnnot_GenerateAppearance` so the bake sees the rotation.
|
|
5158
|
-
* `/SchemaVersion` is seeded (stays 1) if this is the first key in the dict.
|
|
5159
|
-
*/
|
|
5160
|
-
const KEY_ROTATION = "Rotation";
|
|
5161
|
-
const KEY_UNROTATED_RECT = "UnrotatedRect";
|
|
5162
|
-
/** Seed `/SchemaVersion` when we are about to create the dict by writing the
|
|
5163
|
-
* first transform key, so the marker readers look for is always present. */
|
|
5164
|
-
function ensureSchemaVersion(fn, annotPtr) {
|
|
5165
|
-
if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, "SchemaVersion", 1);
|
|
5166
|
-
}
|
|
5167
|
-
function setRotation(fn, annotPtr, rotation) {
|
|
5168
|
-
ensureSchemaVersion(fn, annotPtr);
|
|
5169
|
-
fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_ROTATION, rotation);
|
|
5170
|
-
}
|
|
5171
|
-
function setUnrotatedRect(fn, mem, annotPtr, rect) {
|
|
5172
|
-
ensureSchemaVersion(fn, annotPtr);
|
|
5173
|
-
const buf = mem.alloc(16);
|
|
5174
|
-
try {
|
|
5175
|
-
mem.poke(buf, "f32", rect.left, 0);
|
|
5176
|
-
mem.poke(buf, "f32", rect.top, 4);
|
|
5177
|
-
mem.poke(buf, "f32", rect.right, 8);
|
|
5178
|
-
mem.poke(buf, "f32", rect.bottom, 12);
|
|
5179
|
-
fn.EPDFAnnot_SetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT, buf);
|
|
5180
|
-
} finally {
|
|
5181
|
-
mem.free(buf);
|
|
5182
|
-
}
|
|
5183
|
-
}
|
|
5184
|
-
/**
|
|
5185
|
-
* Write transform metadata for a BOX kind (square/circle/free-text/stamp),
|
|
5186
|
-
* tri-state per field: `undefined` never touches the document (safe to call on
|
|
5187
|
-
* every patch), `null`/`0` clears, a value sets. A nonzero rotation with no
|
|
5188
|
-
* unrotated box anywhere (patch or annotation) is unsatisfiable — the AP
|
|
5189
|
-
* generator needs the pair — and throws instead of writing a broken state.
|
|
5190
|
-
*/
|
|
5191
|
-
function writeBoxTransformMetadata(fn, mem, annotPtr, t) {
|
|
5192
|
-
if (t.rotation !== void 0) if (t.rotation !== null && t.rotation !== 0) {
|
|
5193
|
-
if (!(t.unrotatedRect != null || t.unrotatedRect === void 0 && readAnnotationUnrotatedRect(fn, mem, annotPtr) != null)) throw new EngineError(EngineErrorCode.InvalidArg, "rotation requires unrotatedRect: supply the pair together (or rely on an existing /EMBD_Metadata/UnrotatedRect)");
|
|
5194
|
-
setRotation(fn, annotPtr, t.rotation);
|
|
5195
|
-
} else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
|
|
5196
|
-
if (t.unrotatedRect !== void 0) if (t.unrotatedRect !== null) setUnrotatedRect(fn, mem, annotPtr, t.unrotatedRect);
|
|
5197
|
-
else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
|
|
5198
|
-
}
|
|
5199
|
-
/**
|
|
5200
|
-
* Write the advisory `/Rotation` scalar for a VERTEX kind
|
|
5201
|
-
* (line/polyline/polygon/ink), tri-state: `undefined` never touches the
|
|
5202
|
-
* document, `null`/`0` clears, a value sets. Whenever it does write, any
|
|
5203
|
-
* `/UnrotatedRect` is defensively cleared — that key is an impossible state on
|
|
5204
|
-
* a vertex kind and must never accidentally drive the AP generator.
|
|
5205
|
-
*/
|
|
5206
|
-
function writeVertexTransformMetadata(fn, annotPtr, t) {
|
|
5207
|
-
if (t.rotation === void 0) return;
|
|
5208
|
-
if (t.rotation !== null && t.rotation !== 0) setRotation(fn, annotPtr, t.rotation);
|
|
5209
|
-
else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
|
|
5210
|
-
fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
|
|
5211
|
-
}
|
|
5212
|
-
//#endregion
|
|
5213
5362
|
//#region src/features/annotations/internal/write/writeFreeTextAnnotation.ts
|
|
5214
5363
|
/**
|
|
5215
5364
|
* Default `/DA` colour for free text: black (border + default text). Unlike
|
|
@@ -5979,13 +6128,18 @@ const DEFAULT_OPACITY = 1;
|
|
|
5979
6128
|
* Apply a text (sticky-note) draft. The visual is entirely generator-owned:
|
|
5980
6129
|
* the mutator's closing `regenerateAppearance` bakes the 20×20 note icon
|
|
5981
6130
|
* from `/C` + `/Name` (GenerateTextAP), so this writer only records state.
|
|
6131
|
+
* `/State` + `/StateModel` are dictionary-only (ISO 32000 §12.5.6.3) and
|
|
6132
|
+
* never reach the generator.
|
|
5982
6133
|
*/
|
|
5983
6134
|
function applyTextDraft(fn, mem, annotPtr, draft) {
|
|
6135
|
+
if (draft.state !== void 0 && draft.stateModel === void 0) throw new EngineError(EngineErrorCode.InvalidArg, "text draft: state requires stateModel (ISO 32000 §12.5.6.3)");
|
|
5984
6136
|
applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
|
|
5985
6137
|
setAnnotRect(fn, mem, annotPtr, draft.rect);
|
|
5986
6138
|
setAnnotColor(fn, annotPtr, draft.color ?? DEFAULT_NOTE_COLOR);
|
|
5987
6139
|
setAnnotOpacity(fn, annotPtr, draft.opacity ?? DEFAULT_OPACITY);
|
|
5988
6140
|
setNoteIcon(fn, annotPtr, draft.icon ?? "note");
|
|
6141
|
+
if (draft.stateModel !== void 0) writeAnnotString(fn, mem, annotPtr, "StateModel", stateModelToPdf(draft.stateModel));
|
|
6142
|
+
if (draft.state !== void 0) writeAnnotString(fn, mem, annotPtr, "State", stateToPdf(draft.state));
|
|
5989
6143
|
}
|
|
5990
6144
|
function applyTextPatch(fn, mem, annotPtr, patch) {
|
|
5991
6145
|
applyAnnotationBasePatch(fn, mem, annotPtr, patch);
|
|
@@ -5993,6 +6147,8 @@ function applyTextPatch(fn, mem, annotPtr, patch) {
|
|
|
5993
6147
|
if (patch.color !== void 0) setAnnotColor(fn, annotPtr, patch.color);
|
|
5994
6148
|
if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
|
|
5995
6149
|
if (patch.icon !== void 0) setNoteIcon(fn, annotPtr, patch.icon);
|
|
6150
|
+
if (patch.stateModel !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "StateModel", patch.stateModel === null ? null : stateModelToPdf(patch.stateModel));
|
|
6151
|
+
if (patch.state !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "State", patch.state === null ? null : stateToPdf(patch.state));
|
|
5996
6152
|
}
|
|
5997
6153
|
function isTextSubtype(subtype) {
|
|
5998
6154
|
return subtype === "text";
|
|
@@ -6970,17 +7126,35 @@ var RedactionApplier = class {
|
|
|
6970
7126
|
}
|
|
6971
7127
|
applyAllOnPage(pagePtr) {
|
|
6972
7128
|
const { fn, mem } = this.runtime;
|
|
7129
|
+
const hasRedactions = this.pageHasRedactAnnotations(pagePtr);
|
|
6973
7130
|
return withScratch(mem, 4, (countPtr) => {
|
|
6974
|
-
if (!fn.EPDFPage_ApplyRedactions(pagePtr, countPtr))
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
7131
|
+
if (!fn.EPDFPage_ApplyRedactions(pagePtr, countPtr)) {
|
|
7132
|
+
if (hasRedactions) throw new EngineError(EngineErrorCode.Unknown, "native page redaction apply failed");
|
|
7133
|
+
return {
|
|
7134
|
+
status: "unchanged",
|
|
7135
|
+
removed: 0
|
|
7136
|
+
};
|
|
7137
|
+
}
|
|
6978
7138
|
return {
|
|
6979
7139
|
status: "applied",
|
|
6980
7140
|
removed: readI32(mem, countPtr) >>> 0
|
|
6981
7141
|
};
|
|
6982
7142
|
});
|
|
6983
7143
|
}
|
|
7144
|
+
pageHasRedactAnnotations(pagePtr) {
|
|
7145
|
+
const { fn } = this.runtime;
|
|
7146
|
+
const count = fn.FPDFPage_GetAnnotCount(pagePtr);
|
|
7147
|
+
for (let index = 0; index < count; index++) {
|
|
7148
|
+
const annotPtr = fn.FPDFPage_GetAnnot(pagePtr, index);
|
|
7149
|
+
if (!annotPtr) continue;
|
|
7150
|
+
try {
|
|
7151
|
+
if (subtypeFromCode(fn.FPDFAnnot_GetSubtype(annotPtr)) === "redact") return true;
|
|
7152
|
+
} finally {
|
|
7153
|
+
fn.FPDFPage_CloseAnnot(annotPtr);
|
|
7154
|
+
}
|
|
7155
|
+
}
|
|
7156
|
+
return false;
|
|
7157
|
+
}
|
|
6984
7158
|
applyRefsOnPage(pagePtr, refs) {
|
|
6985
7159
|
const { fn, mem } = this.runtime;
|
|
6986
7160
|
let removed = 0;
|
|
@@ -7006,23 +7180,26 @@ var RedactionApplier = class {
|
|
|
7006
7180
|
//#region src/features/text/PageTextReader.ts
|
|
7007
7181
|
/**
|
|
7008
7182
|
* Per-page slow-path text reader. Acquires a `pagePtr` from the
|
|
7009
|
-
* `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`),
|
|
7010
|
-
*
|
|
7011
|
-
*
|
|
7183
|
+
* `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`), extracts
|
|
7184
|
+
* the page text, and releases everything in reverse order.
|
|
7185
|
+
*
|
|
7186
|
+
* Index spaces (see engine-core `text/charmap.ts`): `charCount` is the
|
|
7187
|
+
* CHARACTER space (`FPDFText_CountChars` — the space geometry runs tile);
|
|
7188
|
+
* `text` is the TEXT projection. The two diverge when a character
|
|
7189
|
+
* contributes zero text units (non-printing) or two (supplementary plane);
|
|
7190
|
+
* `charMap` anchors encode exactly those deviations.
|
|
7012
7191
|
*
|
|
7013
|
-
*
|
|
7014
|
-
*
|
|
7015
|
-
*
|
|
7016
|
-
* trailing NUL terminator).
|
|
7017
|
-
* - `mem.readU16String` walks the buffer up to the NUL and decodes
|
|
7018
|
-
* to a JS string, faithfully preserving surrogate pairs.
|
|
7192
|
+
* Both come from the fork's batched extension calls, which share one
|
|
7193
|
+
* encoding authority (`FX_UTF16Encode`) so text and map can never
|
|
7194
|
+
* disagree:
|
|
7019
7195
|
*
|
|
7020
|
-
*
|
|
7021
|
-
*
|
|
7022
|
-
*
|
|
7023
|
-
*
|
|
7024
|
-
*
|
|
7025
|
-
*
|
|
7196
|
+
* - `EPDFText_GetTextFull` — true UTF-16LE extraction; supplementary
|
|
7197
|
+
* characters arrive as surrogate pairs instead of being silently
|
|
7198
|
+
* dropped by `FPDFText_GetText`'s UCS-2 encoder. Called once with an
|
|
7199
|
+
* upper-bound buffer (2 units per character + NUL), so no measure
|
|
7200
|
+
* round-trip.
|
|
7201
|
+
* - `EPDFText_GetCharToTextMap` — the anchors, measure-then-fill. Zero
|
|
7202
|
+
* anchors is the common (identity) page and skips the fill call.
|
|
7026
7203
|
*/
|
|
7027
7204
|
var PageTextReader = class {
|
|
7028
7205
|
constructor(runtime, session) {
|
|
@@ -7031,7 +7208,7 @@ var PageTextReader = class {
|
|
|
7031
7208
|
}
|
|
7032
7209
|
read(pageObjectNumber, signal) {
|
|
7033
7210
|
throwIfAborted(signal);
|
|
7034
|
-
const { fn
|
|
7211
|
+
const { fn } = this.runtime;
|
|
7035
7212
|
const pool = this.session.pagePool();
|
|
7036
7213
|
const pagePtr = pool.acquire(pageObjectNumber);
|
|
7037
7214
|
try {
|
|
@@ -7040,10 +7217,18 @@ var PageTextReader = class {
|
|
|
7040
7217
|
if (!textPagePtr) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `FPDFText_LoadPage returned null for page object ${pageObjectNumber}`);
|
|
7041
7218
|
try {
|
|
7042
7219
|
throwIfAborted(signal);
|
|
7043
|
-
const charCount = fn.FPDFText_CountChars(textPagePtr);
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7220
|
+
const charCount = Math.max(fn.FPDFText_CountChars(textPagePtr), 0);
|
|
7221
|
+
const text = readTextFull(this.runtime, textPagePtr, charCount);
|
|
7222
|
+
const charMap = readCharMap(this.runtime, textPagePtr, pageObjectNumber);
|
|
7223
|
+
const violation = charMapViolation(charCount, text.length, charMap);
|
|
7224
|
+
if (violation !== null) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `char map invariant violated for page object ${pageObjectNumber}: ${violation}`);
|
|
7225
|
+
return charMap.length > 0 ? {
|
|
7226
|
+
text,
|
|
7227
|
+
charCount,
|
|
7228
|
+
charMap
|
|
7229
|
+
} : {
|
|
7230
|
+
text,
|
|
7231
|
+
charCount
|
|
7047
7232
|
};
|
|
7048
7233
|
} finally {
|
|
7049
7234
|
fn.FPDFText_ClosePage(textPagePtr);
|
|
@@ -7053,21 +7238,65 @@ var PageTextReader = class {
|
|
|
7053
7238
|
}
|
|
7054
7239
|
}
|
|
7055
7240
|
};
|
|
7056
|
-
|
|
7241
|
+
/**
|
|
7242
|
+
* Full-fidelity page text in one call: the buffer is sized at the maximum
|
|
7243
|
+
* possible expansion (every character a surrogate pair, plus the NUL), so
|
|
7244
|
+
* the returned requirement always fits and the measure round-trip is
|
|
7245
|
+
* skipped. The return value counts UTF-16 units INCLUDING the terminator.
|
|
7246
|
+
*/
|
|
7247
|
+
function readTextFull(runtime, textPagePtr, charCount) {
|
|
7057
7248
|
if (charCount <= 0) return "";
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7249
|
+
const { fn, mem } = runtime;
|
|
7250
|
+
const bufUnits = charCount * 2 + 1;
|
|
7251
|
+
return withScratch(mem, bufUnits * 2, (buf) => {
|
|
7252
|
+
const written = fn.EPDFText_GetTextFull(textPagePtr, buf, bufUnits);
|
|
7253
|
+
if (written <= 1) return "";
|
|
7254
|
+
return decodeUtf16Le(mem.readBytes(buf, (written - 1) * 2));
|
|
7255
|
+
});
|
|
7256
|
+
}
|
|
7257
|
+
function decodeUtf16Le(bytes) {
|
|
7258
|
+
const units = new Uint16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 1);
|
|
7259
|
+
let out = "";
|
|
7260
|
+
const CHUNK = 8192;
|
|
7261
|
+
for (let i = 0; i < units.length; i += CHUNK) out += String.fromCharCode(...units.subarray(i, Math.min(i + CHUNK, units.length)));
|
|
7262
|
+
return out;
|
|
7263
|
+
}
|
|
7264
|
+
/** Anchor struct layout: two little-endian i32s (see EPDF_CHAR_MAP_ANCHOR). */
|
|
7265
|
+
const ANCHOR_BYTES = 8;
|
|
7266
|
+
function readCharMap(runtime, textPagePtr, pageObjectNumber) {
|
|
7267
|
+
const { fn, mem } = runtime;
|
|
7268
|
+
const count = fn.EPDFText_GetCharToTextMap(textPagePtr, NULL_PTR, 0);
|
|
7269
|
+
if (count < 0) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `EPDFText_GetCharToTextMap failed for page object ${pageObjectNumber}`);
|
|
7270
|
+
if (count === 0) return [];
|
|
7271
|
+
return withScratch(mem, count * ANCHOR_BYTES, (buf) => {
|
|
7272
|
+
fn.EPDFText_GetCharToTextMap(textPagePtr, buf, count);
|
|
7273
|
+
const bytes = mem.readBytes(buf, count * ANCHOR_BYTES);
|
|
7274
|
+
const words = new Int32Array(bytes.buffer, bytes.byteOffset, count * 2);
|
|
7275
|
+
const anchors = new Array(count);
|
|
7276
|
+
for (let i = 0; i < count; i++) anchors[i] = [words[i * 2], words[i * 2 + 1]];
|
|
7277
|
+
return anchors;
|
|
7061
7278
|
});
|
|
7062
7279
|
}
|
|
7063
7280
|
//#endregion
|
|
7064
7281
|
//#region src/features/geometry/PageGeometryReader.ts
|
|
7065
7282
|
/**
|
|
7283
|
+
* Mid-object orientation drift that forces a run split (~0.5°). Real content
|
|
7284
|
+
* keeps one char matrix per text object; this guards the exotic cases so a
|
|
7285
|
+
* run's `rotation` is always representative of every glyph in it.
|
|
7286
|
+
*/
|
|
7287
|
+
const ROTATION_SPLIT_TOLERANCE = .0087;
|
|
7288
|
+
/**
|
|
7066
7289
|
* Geometry-only text layout reader.
|
|
7067
7290
|
*
|
|
7068
7291
|
* Emits geometry in PDF user space (y-up edges) — the canonical engine
|
|
7069
7292
|
* geometry. The viewer converts to content/view space via the page geometry
|
|
7070
7293
|
* matrix; this reader applies NO Y-flip or device transform.
|
|
7294
|
+
*
|
|
7295
|
+
* One `EPDFText_GetCharGeometry` call per glyph supplies boxes, oriented
|
|
7296
|
+
* cells, and flags together; `buildRunsFromRawGlyphs` then groups glyphs
|
|
7297
|
+
* into the upright/rotated run union. Upright runs are byte-identical to the
|
|
7298
|
+
* pre-orientation reader (quads the native layer offers are deliberately
|
|
7299
|
+
* dropped — the dominant case never pays for orientation).
|
|
7071
7300
|
*/
|
|
7072
7301
|
var PageGeometryReader = class {
|
|
7073
7302
|
constructor(runtime, session) {
|
|
@@ -7076,7 +7305,7 @@ var PageGeometryReader = class {
|
|
|
7076
7305
|
}
|
|
7077
7306
|
read(pageObjectNumber, signal) {
|
|
7078
7307
|
throwIfAborted(signal);
|
|
7079
|
-
const { fn } = this.runtime;
|
|
7308
|
+
const { fn, mem } = this.runtime;
|
|
7080
7309
|
const pool = this.session.pagePool();
|
|
7081
7310
|
const pagePtr = pool.acquire(pageObjectNumber);
|
|
7082
7311
|
try {
|
|
@@ -7086,12 +7315,27 @@ var PageGeometryReader = class {
|
|
|
7086
7315
|
try {
|
|
7087
7316
|
throwIfAborted(signal);
|
|
7088
7317
|
const glyphCount = Math.max(fn.FPDFText_CountChars(textPagePtr), 0);
|
|
7089
|
-
const
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7318
|
+
const records = [];
|
|
7319
|
+
const geometryPtr = mem.alloc(EPDF_CHAR_GEOMETRY_LAYOUT.bytes);
|
|
7320
|
+
try {
|
|
7321
|
+
let prevObjectKey = null;
|
|
7322
|
+
for (let i = 0; i < glyphCount; i++) {
|
|
7323
|
+
throwIfAborted(signal);
|
|
7324
|
+
const objectKey = fn.FPDFText_GetTextObject(textPagePtr, i);
|
|
7325
|
+
const record = {
|
|
7326
|
+
objectKey,
|
|
7327
|
+
...this.readGlyphRaw(textPagePtr, i, geometryPtr)
|
|
7328
|
+
};
|
|
7329
|
+
if (objectKey !== prevObjectKey) {
|
|
7330
|
+
record.fontSize = fn.FPDFText_GetFontSize(textPagePtr, i);
|
|
7331
|
+
prevObjectKey = objectKey;
|
|
7332
|
+
}
|
|
7333
|
+
records.push(record);
|
|
7334
|
+
}
|
|
7335
|
+
} finally {
|
|
7336
|
+
mem.free(geometryPtr);
|
|
7093
7337
|
}
|
|
7094
|
-
return { runs:
|
|
7338
|
+
return { runs: buildRunsFromRawGlyphs(records) };
|
|
7095
7339
|
} finally {
|
|
7096
7340
|
fn.FPDFText_ClosePage(textPagePtr);
|
|
7097
7341
|
}
|
|
@@ -7099,96 +7343,206 @@ var PageGeometryReader = class {
|
|
|
7099
7343
|
pool.release(pageObjectNumber);
|
|
7100
7344
|
}
|
|
7101
7345
|
}
|
|
7102
|
-
|
|
7103
|
-
const { fn } = this.runtime;
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7346
|
+
readGlyphRaw(textPagePtr, charIndex, geometryPtr) {
|
|
7347
|
+
const { fn, mem } = this.runtime;
|
|
7348
|
+
if (!fn.EPDFText_GetCharGeometry(textPagePtr, charIndex, geometryPtr)) return emptyRawGlyph();
|
|
7349
|
+
const { offsets, flags: bits } = EPDF_CHAR_GEOMETRY_LAYOUT;
|
|
7350
|
+
const native = readI32(mem, geometryPtr, offsets.flags) >>> 0;
|
|
7351
|
+
if (native & bits.empty) return emptyRawGlyph();
|
|
7352
|
+
const upright = Boolean(native & bits.upright);
|
|
7353
|
+
const raw = {
|
|
7354
|
+
flags: native & bits.space ? 1 : 0,
|
|
7355
|
+
looseBox: normalizePdfRect(readRectF(mem, geometryPtr, offsets.looseBox)),
|
|
7356
|
+
upright,
|
|
7357
|
+
rotation: 0,
|
|
7358
|
+
ascentFlip: false
|
|
7359
|
+
};
|
|
7360
|
+
if (native & bits.hasTightBox) raw.tightBox = normalizePdfRect(readRectF(mem, geometryPtr, offsets.tightBox));
|
|
7361
|
+
if (native & bits.hasLooseQuad) {
|
|
7362
|
+
raw.looseQuad = readQuad(mem, geometryPtr, offsets.looseQuad);
|
|
7363
|
+
if (native & bits.hasTightQuad) raw.tightQuad = readQuad(mem, geometryPtr, offsets.tightQuad);
|
|
7364
|
+
if (!upright) {
|
|
7365
|
+
const a = readF32(mem, geometryPtr, offsets.matrix);
|
|
7366
|
+
const b = readF32(mem, geometryPtr, offsets.matrix + 4);
|
|
7367
|
+
const c = readF32(mem, geometryPtr, offsets.matrix + 8);
|
|
7368
|
+
const d = readF32(mem, geometryPtr, offsets.matrix + 12);
|
|
7369
|
+
raw.rotation = Math.atan2(b, a);
|
|
7370
|
+
raw.ascentFlip = a * d - b * c < 0;
|
|
7121
7371
|
}
|
|
7122
|
-
current.glyphs.push(toSlimGlyph(glyph));
|
|
7123
|
-
if (glyph.isEmpty) continue;
|
|
7124
|
-
bounds.left = Math.min(bounds.left, glyph.looseBox.left);
|
|
7125
|
-
bounds.bottom = Math.min(bounds.bottom, glyph.looseBox.bottom);
|
|
7126
|
-
bounds.right = Math.max(bounds.right, glyph.looseBox.right);
|
|
7127
|
-
bounds.top = Math.max(bounds.top, glyph.looseBox.top);
|
|
7128
|
-
current.rect = { ...bounds };
|
|
7129
7372
|
}
|
|
7130
|
-
return
|
|
7373
|
+
return raw;
|
|
7131
7374
|
}
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7375
|
+
};
|
|
7376
|
+
/**
|
|
7377
|
+
* Group per-glyph records into the run union. Pure — exported for tests.
|
|
7378
|
+
*
|
|
7379
|
+
* Runs split on text-object change (the legacy rule) and on orientation
|
|
7380
|
+
* change between classifiable glyphs (θ drift / mixed orientation). A run's
|
|
7381
|
+
* variant is decided by its FIRST classifiable glyph:
|
|
7382
|
+
* - empty glyphs never classify (they adopt the run's variant);
|
|
7383
|
+
* - real glyphs WITHOUT an oriented cell (singular matrix, synthesized
|
|
7384
|
+
* /ActualText pieces) classify as upright — box-only data degrades to
|
|
7385
|
+
* exactly the legacy behavior;
|
|
7386
|
+
* - real glyphs with a non-upright matrix classify as rotated.
|
|
7387
|
+
* Runs that never see a classifiable glyph emit as upright (degenerate,
|
|
7388
|
+
* legacy behavior).
|
|
7389
|
+
*/
|
|
7390
|
+
function buildRunsFromRawGlyphs(records) {
|
|
7391
|
+
const runs = [];
|
|
7392
|
+
let buffer = [];
|
|
7393
|
+
let charStart = 0;
|
|
7394
|
+
let fontSize;
|
|
7395
|
+
let objectKey = null;
|
|
7396
|
+
let cls;
|
|
7397
|
+
const flush = (nextStart) => {
|
|
7398
|
+
if (buffer.length > 0) {
|
|
7399
|
+
runs.push(materializeRun(buffer, charStart, fontSize, cls));
|
|
7400
|
+
buffer = [];
|
|
7401
|
+
cls = void 0;
|
|
7402
|
+
}
|
|
7403
|
+
charStart = nextStart;
|
|
7404
|
+
};
|
|
7405
|
+
for (let i = 0; i < records.length; i++) {
|
|
7406
|
+
const record = records[i];
|
|
7407
|
+
if (record.objectKey !== objectKey) {
|
|
7408
|
+
flush(i);
|
|
7409
|
+
objectKey = record.objectKey;
|
|
7410
|
+
fontSize = record.fontSize;
|
|
7411
|
+
}
|
|
7412
|
+
const recordClass = classOf(record);
|
|
7413
|
+
if (recordClass) {
|
|
7414
|
+
if (cls && !sameClass(cls, recordClass)) flush(i);
|
|
7415
|
+
if (!cls) cls = recordClass;
|
|
7173
7416
|
}
|
|
7417
|
+
buffer.push(record);
|
|
7418
|
+
}
|
|
7419
|
+
flush(records.length);
|
|
7420
|
+
return runs;
|
|
7421
|
+
}
|
|
7422
|
+
function classOf(record) {
|
|
7423
|
+
if (record.flags & 2) return void 0;
|
|
7424
|
+
if (record.looseQuad && !record.upright) return {
|
|
7425
|
+
upright: false,
|
|
7426
|
+
rotation: record.rotation,
|
|
7427
|
+
ascentFlip: record.ascentFlip
|
|
7428
|
+
};
|
|
7429
|
+
return { upright: true };
|
|
7430
|
+
}
|
|
7431
|
+
function sameClass(a, b) {
|
|
7432
|
+
if (a.upright || b.upright) return a.upright === b.upright;
|
|
7433
|
+
if (a.ascentFlip !== b.ascentFlip) return false;
|
|
7434
|
+
const delta = a.rotation - b.rotation;
|
|
7435
|
+
return Math.abs(Math.atan2(Math.sin(delta), Math.cos(delta))) <= ROTATION_SPLIT_TOLERANCE;
|
|
7436
|
+
}
|
|
7437
|
+
function materializeRun(buffer, charStart, fontSize, cls) {
|
|
7438
|
+
if (cls && !cls.upright) {
|
|
7439
|
+
const glyphs = buffer.map((g) => g.looseQuad ? {
|
|
7440
|
+
looseQuad: g.looseQuad,
|
|
7441
|
+
flags: g.flags,
|
|
7442
|
+
...g.tightQuad ? { tightQuad: g.tightQuad } : {}
|
|
7443
|
+
} : {
|
|
7444
|
+
looseQuad: zeroQuad(),
|
|
7445
|
+
flags: g.flags
|
|
7446
|
+
});
|
|
7447
|
+
return {
|
|
7448
|
+
rect: runBounds(buffer, (g) => g.looseQuad ? pdfQuadBounds(g.looseQuad) : ZERO_RECT),
|
|
7449
|
+
charStart,
|
|
7450
|
+
glyphs,
|
|
7451
|
+
rotation: cls.rotation,
|
|
7452
|
+
ascentFlip: cls.ascentFlip,
|
|
7453
|
+
...fontSize !== void 0 ? { fontSize } : {}
|
|
7454
|
+
};
|
|
7455
|
+
}
|
|
7456
|
+
const glyphs = buffer.map((g) => ({
|
|
7457
|
+
looseBox: g.looseBox,
|
|
7458
|
+
flags: g.flags,
|
|
7459
|
+
...g.tightBox ? { tightBox: g.tightBox } : {}
|
|
7460
|
+
}));
|
|
7461
|
+
return {
|
|
7462
|
+
rect: runBounds(buffer, (g) => g.looseBox),
|
|
7463
|
+
charStart,
|
|
7464
|
+
glyphs,
|
|
7465
|
+
...fontSize !== void 0 ? { fontSize } : {}
|
|
7466
|
+
};
|
|
7467
|
+
}
|
|
7468
|
+
/** Legacy run-rect algorithm: seed from the first glyph, expand on non-empty. */
|
|
7469
|
+
function runBounds(buffer, boundsOf) {
|
|
7470
|
+
const seed = boundsOf(buffer[0]);
|
|
7471
|
+
const rect = {
|
|
7472
|
+
left: seed.left,
|
|
7473
|
+
bottom: seed.bottom,
|
|
7474
|
+
right: seed.right,
|
|
7475
|
+
top: seed.top
|
|
7476
|
+
};
|
|
7477
|
+
for (const g of buffer) {
|
|
7478
|
+
if (g.flags & 2) continue;
|
|
7479
|
+
const b = boundsOf(g);
|
|
7480
|
+
rect.left = Math.min(rect.left, b.left);
|
|
7481
|
+
rect.bottom = Math.min(rect.bottom, b.bottom);
|
|
7482
|
+
rect.right = Math.max(rect.right, b.right);
|
|
7483
|
+
rect.top = Math.max(rect.top, b.top);
|
|
7174
7484
|
}
|
|
7485
|
+
return rect;
|
|
7486
|
+
}
|
|
7487
|
+
const ZERO_RECT = {
|
|
7488
|
+
left: 0,
|
|
7489
|
+
bottom: 0,
|
|
7490
|
+
right: 0,
|
|
7491
|
+
top: 0
|
|
7175
7492
|
};
|
|
7176
|
-
function
|
|
7493
|
+
function zeroQuad() {
|
|
7494
|
+
return {
|
|
7495
|
+
p1: {
|
|
7496
|
+
x: 0,
|
|
7497
|
+
y: 0
|
|
7498
|
+
},
|
|
7499
|
+
p2: {
|
|
7500
|
+
x: 0,
|
|
7501
|
+
y: 0
|
|
7502
|
+
},
|
|
7503
|
+
p3: {
|
|
7504
|
+
x: 0,
|
|
7505
|
+
y: 0
|
|
7506
|
+
},
|
|
7507
|
+
p4: {
|
|
7508
|
+
x: 0,
|
|
7509
|
+
y: 0
|
|
7510
|
+
}
|
|
7511
|
+
};
|
|
7512
|
+
}
|
|
7513
|
+
function emptyRawGlyph() {
|
|
7177
7514
|
return {
|
|
7515
|
+
flags: 2,
|
|
7178
7516
|
looseBox: {
|
|
7179
7517
|
left: 0,
|
|
7180
7518
|
bottom: 0,
|
|
7181
7519
|
right: 0,
|
|
7182
7520
|
top: 0
|
|
7183
7521
|
},
|
|
7184
|
-
|
|
7522
|
+
upright: true,
|
|
7523
|
+
rotation: 0,
|
|
7524
|
+
ascentFlip: false
|
|
7185
7525
|
};
|
|
7186
7526
|
}
|
|
7187
|
-
function
|
|
7527
|
+
function readQuad(mem, ptr, byteOffset) {
|
|
7528
|
+
const f = (offset) => readF32(mem, ptr, byteOffset + offset);
|
|
7188
7529
|
return {
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7530
|
+
p1: {
|
|
7531
|
+
x: f(0),
|
|
7532
|
+
y: f(4)
|
|
7533
|
+
},
|
|
7534
|
+
p2: {
|
|
7535
|
+
x: f(8),
|
|
7536
|
+
y: f(12)
|
|
7537
|
+
},
|
|
7538
|
+
p3: {
|
|
7539
|
+
x: f(16),
|
|
7540
|
+
y: f(20)
|
|
7541
|
+
},
|
|
7542
|
+
p4: {
|
|
7543
|
+
x: f(24),
|
|
7544
|
+
y: f(28)
|
|
7545
|
+
}
|
|
7192
7546
|
};
|
|
7193
7547
|
}
|
|
7194
7548
|
//#endregion
|
|
@@ -8561,7 +8915,7 @@ function emptyResult(index, status) {
|
|
|
8561
8915
|
//#endregion
|
|
8562
8916
|
//#region src/features/search/internal/pageCorpusCache.ts
|
|
8563
8917
|
/**
|
|
8564
|
-
* Per-session, per-page search corpus: the page's
|
|
8918
|
+
* Per-session, per-page search corpus: the page's text snapshot plus its
|
|
8565
8919
|
* DEFAULT fold (the one literal queries with default options search).
|
|
8566
8920
|
* This is the local engine's in-memory equivalent of the server's corpus
|
|
8567
8921
|
* artifacts — same fold version, same shape, built lazily on first search
|
|
@@ -8586,19 +8940,20 @@ function acquirePageCorpus(runtime, session, pageObjectNumber, signal) {
|
|
|
8586
8940
|
cache.set(session, pages);
|
|
8587
8941
|
}
|
|
8588
8942
|
const hit = pages.get(pageObjectNumber);
|
|
8589
|
-
if (hit && hit.seq === seq) return hit
|
|
8590
|
-
const
|
|
8591
|
-
const
|
|
8592
|
-
pages.delete(pageObjectNumber);
|
|
8593
|
-
pages.set(pageObjectNumber, {
|
|
8943
|
+
if (hit && hit.seq === seq) return hit;
|
|
8944
|
+
const snapshot = new PageTextReader(runtime, session).read(pageObjectNumber, signal);
|
|
8945
|
+
const entry = {
|
|
8594
8946
|
seq,
|
|
8595
|
-
folded
|
|
8596
|
-
|
|
8947
|
+
folded: foldText(snapshot.text),
|
|
8948
|
+
snapshot
|
|
8949
|
+
};
|
|
8950
|
+
pages.delete(pageObjectNumber);
|
|
8951
|
+
pages.set(pageObjectNumber, entry);
|
|
8597
8952
|
if (pages.size > MAX_CACHED_PAGES) {
|
|
8598
8953
|
const oldest = pages.keys().next().value;
|
|
8599
8954
|
if (oldest !== void 0) pages.delete(oldest);
|
|
8600
8955
|
}
|
|
8601
|
-
return
|
|
8956
|
+
return entry;
|
|
8602
8957
|
}
|
|
8603
8958
|
//#endregion
|
|
8604
8959
|
//#region src/features/search/internal/searchCursor.ts
|
|
@@ -8707,19 +9062,23 @@ var SearchReader = class {
|
|
|
8707
9062
|
throwIfAborted(signal);
|
|
8708
9063
|
const pon = order[scanned].pageObjectNumber;
|
|
8709
9064
|
const corpus = acquirePageCorpus(this.runtime, this.session, pon, signal);
|
|
9065
|
+
const text = corpus.snapshot.text;
|
|
8710
9066
|
let ranges;
|
|
8711
|
-
if (query.regex) ranges = matchRegex(
|
|
8712
|
-
else if (query.matchCase || query.matchDiacritics) ranges = matchLiteral(foldText(
|
|
8713
|
-
else ranges = matchLiteral(corpus, query);
|
|
9067
|
+
if (query.regex) ranges = matchRegex(text, query);
|
|
9068
|
+
else if (query.matchCase || query.matchDiacritics) ranges = matchLiteral(foldText(text, foldOptionsFor(query)), query);
|
|
9069
|
+
else ranges = matchLiteral(corpus.folded, query);
|
|
8714
9070
|
if (ranges.length > 0) {
|
|
8715
|
-
const
|
|
8716
|
-
for (const range of ranges)
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
9071
|
+
const layout = buildPageTextLayout(new PageGeometryReader(this.runtime, this.session).read(pon, signal));
|
|
9072
|
+
for (const range of ranges) {
|
|
9073
|
+
const chars = charRangeForTextOffsets(corpus.snapshot, range.start, range.start + range.length);
|
|
9074
|
+
matches.push({
|
|
9075
|
+
pageObjectNumber: pon,
|
|
9076
|
+
charStart: chars.start,
|
|
9077
|
+
charCount: chars.end - chars.start,
|
|
9078
|
+
segments: textSegmentsForRange(layout, chars.start, chars.end - chars.start),
|
|
9079
|
+
...mode === "full" ? { snippet: buildSnippet(text, range) } : {}
|
|
9080
|
+
});
|
|
9081
|
+
}
|
|
8723
9082
|
}
|
|
8724
9083
|
scanned++;
|
|
8725
9084
|
pagesThisSlice++;
|
|
@@ -9000,9 +9359,10 @@ function validatePatchValue(key, value) {
|
|
|
9000
9359
|
* worker wire shape from @embedpdf/engine-core.
|
|
9001
9360
|
*/
|
|
9002
9361
|
var WorkerHost = class {
|
|
9003
|
-
constructor(runtime, post) {
|
|
9362
|
+
constructor(runtime, post, options = {}) {
|
|
9004
9363
|
this.runtime = runtime;
|
|
9005
9364
|
this.post = post;
|
|
9365
|
+
this.options = options;
|
|
9006
9366
|
_defineProperty(this, "sessions", /* @__PURE__ */ new Map());
|
|
9007
9367
|
_defineProperty(this, "baseDocuments", void 0);
|
|
9008
9368
|
_defineProperty(this, "aborts", /* @__PURE__ */ new Map());
|
|
@@ -9028,6 +9388,10 @@ var WorkerHost = class {
|
|
|
9028
9388
|
this.aborts.get(msg.jobId)?.abort();
|
|
9029
9389
|
return;
|
|
9030
9390
|
}
|
|
9391
|
+
if (msg.kind === "pages.renderEncoded" || msg.kind === "document.renderPageFileEncoded" || msg.kind === "annotations.renderAppearancesEncoded") {
|
|
9392
|
+
this.receiveEncoded(msg);
|
|
9393
|
+
return;
|
|
9394
|
+
}
|
|
9031
9395
|
const ctrl = new AbortController();
|
|
9032
9396
|
this.aborts.set(msg.jobId, ctrl);
|
|
9033
9397
|
let resultPack;
|
|
@@ -9131,6 +9495,9 @@ var WorkerHost = class {
|
|
|
9131
9495
|
case "pages.insert":
|
|
9132
9496
|
resultPack = this.handlePagesInsert(msg, ctrl.signal);
|
|
9133
9497
|
break;
|
|
9498
|
+
case "pages.insertBlank":
|
|
9499
|
+
resultPack = this.handlePagesInsertBlank(msg, ctrl.signal);
|
|
9500
|
+
break;
|
|
9134
9501
|
case "attachments.list":
|
|
9135
9502
|
resultPack = this.handleAttachmentsList(msg, ctrl.signal);
|
|
9136
9503
|
break;
|
|
@@ -9423,6 +9790,17 @@ var WorkerHost = class {
|
|
|
9423
9790
|
result
|
|
9424
9791
|
}, req.artifactPath);
|
|
9425
9792
|
}
|
|
9793
|
+
handlePagesInsertBlank(req, signal) {
|
|
9794
|
+
const session = this.requireSession(req);
|
|
9795
|
+
const result = new PagesInserter(this.runtime, session).insertBlank({
|
|
9796
|
+
size: req.size,
|
|
9797
|
+
count: req.count
|
|
9798
|
+
}, req.destIndex, signal);
|
|
9799
|
+
return this.finishMutation(session, {
|
|
9800
|
+
tag: "pages.insertBlank",
|
|
9801
|
+
result
|
|
9802
|
+
}, req.artifactPath);
|
|
9803
|
+
}
|
|
9426
9804
|
handleAttachmentsList(req, signal) {
|
|
9427
9805
|
const session = this.requireSession(req);
|
|
9428
9806
|
return wirePack({
|
|
@@ -9515,6 +9893,105 @@ var WorkerHost = class {
|
|
|
9515
9893
|
raster
|
|
9516
9894
|
}, [raster.data]);
|
|
9517
9895
|
}
|
|
9896
|
+
async receiveEncoded(msg) {
|
|
9897
|
+
if (!this.options.imageEncoder) {
|
|
9898
|
+
this.post(wirePack({
|
|
9899
|
+
kind: "reject",
|
|
9900
|
+
jobId: msg.jobId,
|
|
9901
|
+
error: serializeError(new EngineError(EngineErrorCode.NotImplemented, "this engine has no image encoder (the *.renderEncoded kinds are cloud-server surface)"))
|
|
9902
|
+
}, EMPTY_TRANSFER));
|
|
9903
|
+
return;
|
|
9904
|
+
}
|
|
9905
|
+
const ctrl = new AbortController();
|
|
9906
|
+
this.aborts.set(msg.jobId, ctrl);
|
|
9907
|
+
try {
|
|
9908
|
+
let resultPack;
|
|
9909
|
+
switch (msg.kind) {
|
|
9910
|
+
case "pages.renderEncoded":
|
|
9911
|
+
resultPack = await this.handlePagesRenderEncoded(msg, ctrl.signal);
|
|
9912
|
+
break;
|
|
9913
|
+
case "document.renderPageFileEncoded":
|
|
9914
|
+
resultPack = await this.handleDocumentRenderPageFileEncoded(msg, ctrl.signal);
|
|
9915
|
+
break;
|
|
9916
|
+
case "annotations.renderAppearancesEncoded":
|
|
9917
|
+
resultPack = await this.handleAnnotationsRenderAppearancesEncoded(msg, ctrl.signal);
|
|
9918
|
+
break;
|
|
9919
|
+
}
|
|
9920
|
+
this.post(wirePack({
|
|
9921
|
+
kind: "resolve",
|
|
9922
|
+
jobId: msg.jobId,
|
|
9923
|
+
result: resultPack.payload
|
|
9924
|
+
}, resultPack.transfer));
|
|
9925
|
+
} catch (err) {
|
|
9926
|
+
this.post(wirePack({
|
|
9927
|
+
kind: "reject",
|
|
9928
|
+
jobId: msg.jobId,
|
|
9929
|
+
error: serializeError(err)
|
|
9930
|
+
}, EMPTY_TRANSFER));
|
|
9931
|
+
} finally {
|
|
9932
|
+
this.aborts.delete(msg.jobId);
|
|
9933
|
+
}
|
|
9934
|
+
}
|
|
9935
|
+
async encodeRaster(raster, encode, signal) {
|
|
9936
|
+
const encoder = this.options.imageEncoder;
|
|
9937
|
+
if (!encoder) throw new EngineError(EngineErrorCode.NotImplemented, "this engine has no image encoder (the *.renderEncoded kinds are cloud-server surface)");
|
|
9938
|
+
const { bytes, contentType } = await encoder.encode(raster, encode);
|
|
9939
|
+
if (signal.aborted) throw new EngineError(EngineErrorCode.Aborted, "aborted during image encode");
|
|
9940
|
+
return {
|
|
9941
|
+
contentType,
|
|
9942
|
+
width: raster.width,
|
|
9943
|
+
height: raster.height,
|
|
9944
|
+
bytes
|
|
9945
|
+
};
|
|
9946
|
+
}
|
|
9947
|
+
async handlePagesRenderEncoded(req, signal) {
|
|
9948
|
+
const inner = this.handlePagesRender({
|
|
9949
|
+
...req,
|
|
9950
|
+
kind: "pages.render"
|
|
9951
|
+
}, signal);
|
|
9952
|
+
if (inner.payload.tag !== "pages.render") throw new EngineError(EngineErrorCode.WireFormat, `unexpected ${inner.payload.tag}`);
|
|
9953
|
+
const image = await this.encodeRaster(inner.payload.raster, req.encode, signal);
|
|
9954
|
+
return wirePack({
|
|
9955
|
+
tag: "pages.renderEncoded",
|
|
9956
|
+
image
|
|
9957
|
+
}, [image.bytes.buffer]);
|
|
9958
|
+
}
|
|
9959
|
+
async handleDocumentRenderPageFileEncoded(req, signal) {
|
|
9960
|
+
const inner = this.handleDocumentRenderPageFile({
|
|
9961
|
+
...req,
|
|
9962
|
+
kind: "document.renderPageFile"
|
|
9963
|
+
}, signal);
|
|
9964
|
+
if (inner.payload.tag !== "document.renderPageFile") throw new EngineError(EngineErrorCode.WireFormat, `unexpected ${inner.payload.tag}`);
|
|
9965
|
+
const image = await this.encodeRaster(inner.payload.raster, req.encode, signal);
|
|
9966
|
+
return wirePack({
|
|
9967
|
+
tag: "document.renderPageFileEncoded",
|
|
9968
|
+
pageObjectNumber: inner.payload.pageObjectNumber,
|
|
9969
|
+
pageCount: inner.payload.pageCount,
|
|
9970
|
+
image
|
|
9971
|
+
}, [image.bytes.buffer]);
|
|
9972
|
+
}
|
|
9973
|
+
async handleAnnotationsRenderAppearancesEncoded(req, signal) {
|
|
9974
|
+
const inner = this.handleAnnotationsRenderAppearances({
|
|
9975
|
+
...req,
|
|
9976
|
+
kind: "annotations.renderAppearances"
|
|
9977
|
+
}, signal);
|
|
9978
|
+
if (inner.payload.tag !== "annotations.renderAppearances") throw new EngineError(EngineErrorCode.WireFormat, `unexpected ${inner.payload.tag}`);
|
|
9979
|
+
const { pageState, appearances } = inner.payload.result;
|
|
9980
|
+
const encoded = [];
|
|
9981
|
+
for (const a of appearances) encoded.push({
|
|
9982
|
+
ref: a.ref,
|
|
9983
|
+
mode: a.mode,
|
|
9984
|
+
rect: a.rect,
|
|
9985
|
+
image: await this.encodeRaster(a.raster, req.encode, signal)
|
|
9986
|
+
});
|
|
9987
|
+
return wirePack({
|
|
9988
|
+
tag: "annotations.renderAppearancesEncoded",
|
|
9989
|
+
result: {
|
|
9990
|
+
pageState,
|
|
9991
|
+
appearances: encoded
|
|
9992
|
+
}
|
|
9993
|
+
}, encoded.map((e) => e.image.bytes.buffer));
|
|
9994
|
+
}
|
|
9518
9995
|
handleDocumentSaveBuffer(req) {
|
|
9519
9996
|
const session = this.requireSession(req);
|
|
9520
9997
|
const saved = new DocumentSaver(this.runtime, session).saveStandaloneToBuffer(req.mode);
|
|
@@ -9846,6 +10323,6 @@ function passwordRequiredProbe() {
|
|
|
9846
10323
|
};
|
|
9847
10324
|
}
|
|
9848
10325
|
//#endregion
|
|
9849
|
-
export { ALL_STANDARD_SECURITY_PERMISSIONS, ActionReadBudgetTracker, AnnotationAppearanceReader, AnnotationMutator, AnnotationReader, AttachmentMutator, AttachmentReader, BaseDocumentRegistry, CloseStack, DocumentActionsReader, PagesMutator as DocumentPagesMutator, PagesMutator, DocumentSaver, DocumentSession, EventHub, FontRegistrar, FormMutator, FormReader, FormsEffectsApplier, LocalRevisionAuthority, MetadataMutator, MetadataReader, PageGeometryReader, PagesReader as PageLayoutReader, PagesReader, PagePtrPool, PageRenderReader as PageRasterReader, PageRenderReader, PageTextReader, PagesExtractor, PagesFlattener, PagesInserter, RawAnnotationReader, RedactionApplier, SearchReader, SecurityReader, SessionEventPublisher, WorkerHost, computeMutationImpact, destroyLibrary, disposeFormModel, ensureInitialized, formatPdfDate, generateUuid, hasAllStandardSecurityPermissions, normalizeU32, openFatMemoryDocument, openLayerDocument, pdfDateToIso, readActionModel, readAttachmentFileInfo, throwIfAborted, writeAttachmentFilePayload };
|
|
10326
|
+
export { ALL_STANDARD_SECURITY_PERMISSIONS, ActionReadBudgetTracker, AnnotationAppearanceReader, AnnotationMutator, AnnotationReader, AttachmentMutator, AttachmentReader, BaseDocumentRegistry, CloseStack, DocumentActionsReader, PagesMutator as DocumentPagesMutator, PagesMutator, DocumentSaver, DocumentSession, EPDF_CHAR_GEOMETRY_LAYOUT, EventHub, FontRegistrar, FormMutator, FormReader, FormsEffectsApplier, LocalRevisionAuthority, MetadataMutator, MetadataReader, PageGeometryReader, PagesReader as PageLayoutReader, PagesReader, PagePtrPool, PageRenderReader as PageRasterReader, PageRenderReader, PageTextReader, PagesExtractor, PagesFlattener, PagesInserter, RawAnnotationReader, RedactionApplier, SearchReader, SecurityReader, SessionEventPublisher, WorkerHost, buildRunsFromRawGlyphs, computeMutationImpact, destroyLibrary, disposeFormModel, ensureInitialized, formatPdfDate, generateUuid, hasAllStandardSecurityPermissions, normalizeU32, openFatMemoryDocument, openLayerDocument, pdfDateToIso, readActionModel, readAttachmentFileInfo, throwIfAborted, writeAttachmentFilePayload };
|
|
9850
10327
|
|
|
9851
10328
|
//# sourceMappingURL=index.js.map
|