@embedpdf/engine-services 3.0.0-next.1 → 3.0.0-next.11
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 +1391 -710
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +156 -22
- package/dist/index.d.ts +156 -22
- package/dist/index.js +1391 -712
- 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, decodeSubmitFormFlags, 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`. */
|
|
@@ -1188,11 +1211,143 @@ function readI32(mem, ptr, byteOffset = 0) {
|
|
|
1188
1211
|
return Number(mem.peek(ptr, "i32", byteOffset));
|
|
1189
1212
|
}
|
|
1190
1213
|
//#endregion
|
|
1214
|
+
//#region src/features/destinations/destinationViewCodes.ts
|
|
1215
|
+
/**
|
|
1216
|
+
* `PDFDEST_VIEW_*` fit-type codes (public/fpdf_doc.h) — the shared
|
|
1217
|
+
* vocabulary of `FPDFDest_GetView` (read) and `EPDFDest_CreateView`
|
|
1218
|
+
* (write). XYZ is listed for completeness but never goes through
|
|
1219
|
+
* CreateView (it has its own null-aware constructor).
|
|
1220
|
+
*/
|
|
1221
|
+
const DEST_VIEW = {
|
|
1222
|
+
unknown: 0,
|
|
1223
|
+
xyz: 1,
|
|
1224
|
+
fit: 2,
|
|
1225
|
+
fitH: 3,
|
|
1226
|
+
fitV: 4,
|
|
1227
|
+
fitR: 5,
|
|
1228
|
+
fitB: 6,
|
|
1229
|
+
fitBH: 7,
|
|
1230
|
+
fitBV: 8
|
|
1231
|
+
};
|
|
1232
|
+
/** Wire destination kind → PDFDEST_VIEW code, for the non-XYZ kinds. */
|
|
1233
|
+
const VIEW_CODE_BY_KIND = {
|
|
1234
|
+
fit: DEST_VIEW.fit,
|
|
1235
|
+
fitH: DEST_VIEW.fitH,
|
|
1236
|
+
fitV: DEST_VIEW.fitV,
|
|
1237
|
+
fitR: DEST_VIEW.fitR,
|
|
1238
|
+
fitB: DEST_VIEW.fitB,
|
|
1239
|
+
fitBH: DEST_VIEW.fitBH,
|
|
1240
|
+
fitBV: DEST_VIEW.fitBV
|
|
1241
|
+
};
|
|
1242
|
+
//#endregion
|
|
1243
|
+
//#region src/features/destinations/readDestination.ts
|
|
1244
|
+
const MAX_VIEW_PARAMS = 4;
|
|
1245
|
+
/**
|
|
1246
|
+
* Materialise one explicit `PdfDestination` from a live `destPtr`. PDFium
|
|
1247
|
+
* already resolved named destinations before handing us the pointer, so
|
|
1248
|
+
* only explicit arrays arrive here (the rule `PdfDestination` documents).
|
|
1249
|
+
*
|
|
1250
|
+
* Returns `null` when the destination is unreadable: no visible indirect page
|
|
1251
|
+
* in the document, or an unknown fit type. Coordinates stay raw PDF user
|
|
1252
|
+
* space — no conversion at this layer.
|
|
1253
|
+
*
|
|
1254
|
+
* Shared by the link reader, `goto` action-node payloads, and the document
|
|
1255
|
+
* `openDestination`; the outline/bookmark port reuses it too.
|
|
1256
|
+
*/
|
|
1257
|
+
function readDestination(fn, mem, docPtr, destPtr) {
|
|
1258
|
+
const pageObjectNumber = fn.EPDFDest_GetPageObjectNumber(docPtr, destPtr);
|
|
1259
|
+
if (pageObjectNumber === 0) return null;
|
|
1260
|
+
const view = withScratchN(mem, [4, MAX_VIEW_PARAMS * 4], ([countPtr, paramsPtr]) => {
|
|
1261
|
+
const code = fn.FPDFDest_GetView(destPtr, countPtr, paramsPtr);
|
|
1262
|
+
const count = Math.min(readI32(mem, countPtr), MAX_VIEW_PARAMS);
|
|
1263
|
+
const params = [];
|
|
1264
|
+
for (let i = 0; i < count; i++) params.push(readF32(mem, paramsPtr, i * 4));
|
|
1265
|
+
return {
|
|
1266
|
+
code,
|
|
1267
|
+
params
|
|
1268
|
+
};
|
|
1269
|
+
});
|
|
1270
|
+
const at = (i) => i < view.params.length ? view.params[i] : null;
|
|
1271
|
+
switch (view.code) {
|
|
1272
|
+
case DEST_VIEW.xyz: return {
|
|
1273
|
+
kind: "xyz",
|
|
1274
|
+
pageObjectNumber,
|
|
1275
|
+
...readXyzLocation(fn, mem, destPtr)
|
|
1276
|
+
};
|
|
1277
|
+
case DEST_VIEW.fit: return {
|
|
1278
|
+
kind: "fit",
|
|
1279
|
+
pageObjectNumber
|
|
1280
|
+
};
|
|
1281
|
+
case DEST_VIEW.fitH: return {
|
|
1282
|
+
kind: "fitH",
|
|
1283
|
+
pageObjectNumber,
|
|
1284
|
+
top: at(0)
|
|
1285
|
+
};
|
|
1286
|
+
case DEST_VIEW.fitV: return {
|
|
1287
|
+
kind: "fitV",
|
|
1288
|
+
pageObjectNumber,
|
|
1289
|
+
left: at(0)
|
|
1290
|
+
};
|
|
1291
|
+
case DEST_VIEW.fitR:
|
|
1292
|
+
if (view.params.length < 4) return {
|
|
1293
|
+
kind: "fit",
|
|
1294
|
+
pageObjectNumber
|
|
1295
|
+
};
|
|
1296
|
+
return {
|
|
1297
|
+
kind: "fitR",
|
|
1298
|
+
pageObjectNumber,
|
|
1299
|
+
left: view.params[0],
|
|
1300
|
+
bottom: view.params[1],
|
|
1301
|
+
right: view.params[2],
|
|
1302
|
+
top: view.params[3]
|
|
1303
|
+
};
|
|
1304
|
+
case DEST_VIEW.fitB: return {
|
|
1305
|
+
kind: "fitB",
|
|
1306
|
+
pageObjectNumber
|
|
1307
|
+
};
|
|
1308
|
+
case DEST_VIEW.fitBH: return {
|
|
1309
|
+
kind: "fitBH",
|
|
1310
|
+
pageObjectNumber,
|
|
1311
|
+
top: at(0)
|
|
1312
|
+
};
|
|
1313
|
+
case DEST_VIEW.fitBV: return {
|
|
1314
|
+
kind: "fitBV",
|
|
1315
|
+
pageObjectNumber,
|
|
1316
|
+
left: at(0)
|
|
1317
|
+
};
|
|
1318
|
+
default: return null;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/** `/XYZ` null-aware axes via `FPDFDest_GetLocationInPage` (FPDF_BOOL is 4 bytes). */
|
|
1322
|
+
function readXyzLocation(fn, mem, destPtr) {
|
|
1323
|
+
return withScratchN(mem, [
|
|
1324
|
+
4,
|
|
1325
|
+
4,
|
|
1326
|
+
4,
|
|
1327
|
+
4,
|
|
1328
|
+
4,
|
|
1329
|
+
4
|
|
1330
|
+
], ([hasLeft, hasTop, hasZoom, left, top, zoom]) => {
|
|
1331
|
+
if (!fn.FPDFDest_GetLocationInPage(destPtr, hasLeft, hasTop, hasZoom, left, top, zoom)) return {
|
|
1332
|
+
left: null,
|
|
1333
|
+
top: null,
|
|
1334
|
+
zoom: null
|
|
1335
|
+
};
|
|
1336
|
+
return {
|
|
1337
|
+
left: readI32(mem, hasLeft) ? readF32(mem, left) : null,
|
|
1338
|
+
top: readI32(mem, hasTop) ? readF32(mem, top) : null,
|
|
1339
|
+
zoom: readI32(mem, hasZoom) ? readF32(mem, zoom) : null
|
|
1340
|
+
};
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
//#endregion
|
|
1191
1344
|
//#region src/features/actions/ActionModelReader.ts
|
|
1192
1345
|
const DEFAULT_ACTION_READ_BUDGET = {
|
|
1193
1346
|
maxModels: 512,
|
|
1194
1347
|
maxNodes: 16384,
|
|
1195
|
-
maxScriptCodeUnits: 16e6
|
|
1348
|
+
maxScriptCodeUnits: 16e6,
|
|
1349
|
+
maxTargetEntries: 4096,
|
|
1350
|
+
maxPayloadCodeUnits: 262144
|
|
1196
1351
|
};
|
|
1197
1352
|
const ACTION_TYPE_BY_CODE = {
|
|
1198
1353
|
0: "unknown",
|
|
@@ -1220,22 +1375,20 @@ const WARNING_MALFORMED_NEXT = 2;
|
|
|
1220
1375
|
const WARNING_INCOMPLETE = 4;
|
|
1221
1376
|
const INVALID_NODE_U32 = 4294967295;
|
|
1222
1377
|
/** Decode one `EPDF_ACTION_TYPE_*` code — the shared vocabulary consumers
|
|
1223
|
-
*
|
|
1378
|
+
* use so action-shaped surfaces cannot drift. */
|
|
1224
1379
|
function actionTypeFromCode(code) {
|
|
1225
1380
|
return ACTION_TYPE_BY_CODE[code] ?? "unknown";
|
|
1226
1381
|
}
|
|
1227
|
-
/**
|
|
1228
|
-
*
|
|
1229
|
-
function isValidActionNodeId(raw) {
|
|
1230
|
-
return raw !== -1 && raw >>> 0 !== INVALID_NODE_U32;
|
|
1231
|
-
}
|
|
1232
|
-
/** Mutable aggregate budget shared by every action model in one read job. */
|
|
1382
|
+
/** Mutable aggregate budget shared by every action model in one read job.
|
|
1383
|
+
* Payload budgets are RESERVED before the backing buffer is allocated. */
|
|
1233
1384
|
var ActionReadBudgetTracker = class {
|
|
1234
1385
|
constructor(limits = DEFAULT_ACTION_READ_BUDGET) {
|
|
1235
1386
|
this.limits = limits;
|
|
1236
1387
|
_defineProperty(this, "models", 0);
|
|
1237
1388
|
_defineProperty(this, "nodes", 0);
|
|
1238
1389
|
_defineProperty(this, "scriptCodeUnits", 0);
|
|
1390
|
+
_defineProperty(this, "targetEntries", 0);
|
|
1391
|
+
_defineProperty(this, "payloadCodeUnits", 0);
|
|
1239
1392
|
}
|
|
1240
1393
|
addModel(nodeCount) {
|
|
1241
1394
|
this.models += 1;
|
|
@@ -1246,11 +1399,23 @@ var ActionReadBudgetTracker = class {
|
|
|
1246
1399
|
this.scriptCodeUnits += script.length;
|
|
1247
1400
|
this.assertWithinBudget();
|
|
1248
1401
|
}
|
|
1402
|
+
/** Charge target entries BEFORE the target array is walked. */
|
|
1403
|
+
reserveTargets(count) {
|
|
1404
|
+
this.targetEntries += count;
|
|
1405
|
+
this.assertWithinBudget();
|
|
1406
|
+
}
|
|
1407
|
+
/** Charge payload text BEFORE the scratch buffer for it is allocated. */
|
|
1408
|
+
reservePayloadBytes(length) {
|
|
1409
|
+
this.payloadCodeUnits += length;
|
|
1410
|
+
this.assertWithinBudget();
|
|
1411
|
+
}
|
|
1249
1412
|
assertWithinBudget() {
|
|
1250
|
-
if (this.models > this.limits.maxModels || this.nodes > this.limits.maxNodes || this.scriptCodeUnits > this.limits.maxScriptCodeUnits) throw new EngineError(EngineErrorCode.MalformedPdf, "aggregate PDF action read budget exceeded", { details: {
|
|
1413
|
+
if (this.models > this.limits.maxModels || this.nodes > this.limits.maxNodes || this.scriptCodeUnits > this.limits.maxScriptCodeUnits || this.targetEntries > this.limits.maxTargetEntries || this.payloadCodeUnits > this.limits.maxPayloadCodeUnits) throw new EngineError(EngineErrorCode.MalformedPdf, "aggregate PDF action read budget exceeded", { details: {
|
|
1251
1414
|
models: this.models,
|
|
1252
1415
|
nodes: this.nodes,
|
|
1253
1416
|
scriptCodeUnits: this.scriptCodeUnits,
|
|
1417
|
+
targetEntries: this.targetEntries,
|
|
1418
|
+
payloadCodeUnits: this.payloadCodeUnits,
|
|
1254
1419
|
limits: this.limits
|
|
1255
1420
|
} });
|
|
1256
1421
|
}
|
|
@@ -1258,8 +1423,17 @@ var ActionReadBudgetTracker = class {
|
|
|
1258
1423
|
/**
|
|
1259
1424
|
* Convert and close one detached native action model. This is the only
|
|
1260
1425
|
* native-model walker used by document, page, field, and annotation reads.
|
|
1426
|
+
*
|
|
1427
|
+
* `docPtr` resolves `goto` destinations (page identity + named-destination
|
|
1428
|
+
* lookup); every caller reads within an open document session.
|
|
1429
|
+
*
|
|
1430
|
+
* Payload rule: a node whose payload cannot be materialised (a goto without
|
|
1431
|
+
* a resolvable destination, a javascript node with no `/JS`, a withheld
|
|
1432
|
+
* target list) degrades to `{ type: 'unknown' }` with its original `/S` on
|
|
1433
|
+
* `subtype`, and the tree gains the `'payload-dropped'` warning — impossible
|
|
1434
|
+
* states stay unrepresentable while malformed documents stay readable.
|
|
1261
1435
|
*/
|
|
1262
|
-
function readActionModel(fn, mem, modelPtr, budget) {
|
|
1436
|
+
function readActionModel(fn, mem, docPtr, modelPtr, budget) {
|
|
1263
1437
|
if (modelPtr === NULL_PTR) return null;
|
|
1264
1438
|
try {
|
|
1265
1439
|
const nodeCount = fn.EPDFAction_GetNodeCount(modelPtr);
|
|
@@ -1269,13 +1443,61 @@ function readActionModel(fn, mem, modelPtr, budget) {
|
|
|
1269
1443
|
const incomplete = !fn.EPDFAction_IsComplete(modelPtr);
|
|
1270
1444
|
const rootId = normalizeOptionalNodeId(fn.EPDFAction_GetRootNode(modelPtr), nodeCount);
|
|
1271
1445
|
const visiting = /* @__PURE__ */ new Set();
|
|
1446
|
+
let payloadDropped = false;
|
|
1447
|
+
/** Probe the two-call length first and charge the budget BEFORE the
|
|
1448
|
+
* scratch buffer for the value is allocated. */
|
|
1449
|
+
const readPayloadString = (call) => {
|
|
1450
|
+
const length = call(NULL_PTR, 0);
|
|
1451
|
+
if (length <= 0) return null;
|
|
1452
|
+
budget.reservePayloadBytes(length);
|
|
1453
|
+
return readUtf8String(mem, call);
|
|
1454
|
+
};
|
|
1455
|
+
const readOutBool = (read) => withScratchN(mem, [4], ([outPtr]) => read(outPtr) ? readI32(mem, outPtr) !== 0 : null);
|
|
1456
|
+
const readTargetObjectNumber = (nodeId, index) => withScratchN(mem, [4], ([outPtr]) => {
|
|
1457
|
+
if (!fn.EPDFAction_GetNodeTargetObjectNumber(modelPtr, nodeId, index, outPtr)) return null;
|
|
1458
|
+
const value = readI32(mem, outPtr) >>> 0;
|
|
1459
|
+
return value === 0 ? null : value;
|
|
1460
|
+
});
|
|
1461
|
+
const readTargets = (nodeId, count) => {
|
|
1462
|
+
const targets = [];
|
|
1463
|
+
for (let index = 0; index < count; index++) {
|
|
1464
|
+
const name = readPayloadString((buf, capacity) => fn.EPDFAction_GetNodeTargetName(modelPtr, nodeId, index, buf, capacity));
|
|
1465
|
+
if (name !== null) {
|
|
1466
|
+
targets.push({
|
|
1467
|
+
kind: "name",
|
|
1468
|
+
name
|
|
1469
|
+
});
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
const objectNumber = readTargetObjectNumber(nodeId, index);
|
|
1473
|
+
if (objectNumber === null) return null;
|
|
1474
|
+
targets.push({
|
|
1475
|
+
kind: "objectNumber",
|
|
1476
|
+
objectNumber
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
return targets;
|
|
1480
|
+
};
|
|
1481
|
+
const readResetFormState = (nodeId) => withScratchN(mem, [4, 4], ([hasFieldsPtr, excludePtr]) => {
|
|
1482
|
+
if (!fn.EPDFAction_GetNodeResetForm(modelPtr, nodeId, hasFieldsPtr, excludePtr)) return null;
|
|
1483
|
+
return {
|
|
1484
|
+
hasFields: readI32(mem, hasFieldsPtr) !== 0,
|
|
1485
|
+
exclude: readI32(mem, excludePtr) !== 0
|
|
1486
|
+
};
|
|
1487
|
+
});
|
|
1488
|
+
const readSubmitFormState = (nodeId) => withScratchN(mem, [4, 4], ([hasFieldsPtr, flagsPtr]) => {
|
|
1489
|
+
if (!fn.EPDFAction_GetNodeSubmitForm(modelPtr, nodeId, hasFieldsPtr, flagsPtr)) return null;
|
|
1490
|
+
return {
|
|
1491
|
+
hasFields: readI32(mem, hasFieldsPtr) !== 0,
|
|
1492
|
+
flags: readI32(mem, flagsPtr) >>> 0
|
|
1493
|
+
};
|
|
1494
|
+
});
|
|
1272
1495
|
const readNode = (nodeId) => {
|
|
1273
1496
|
if (visiting.has(nodeId)) throw malformedActionModel("native action model contains a cycle", { nodeId });
|
|
1274
1497
|
visiting.add(nodeId);
|
|
1275
1498
|
try {
|
|
1499
|
+
const type = actionTypeFromCode(fn.EPDFAction_GetNodeType(modelPtr, nodeId));
|
|
1276
1500
|
const subtype = readUtf8String(mem, (buf, capacity) => fn.EPDFAction_GetNodeSubtype(modelPtr, nodeId, buf, capacity)) ?? "";
|
|
1277
|
-
const script = fn.EPDFAction_NodeHasJavaScript(modelPtr, nodeId) ? readUtf16String(mem, (buf, capacity) => fn.EPDFAction_GetNodeJavaScript(modelPtr, nodeId, buf, capacity)) ?? "" : void 0;
|
|
1278
|
-
if (script !== void 0) budget.addScript(script);
|
|
1279
1501
|
const nextCount = fn.EPDFAction_GetNextCount(modelPtr, nodeId);
|
|
1280
1502
|
if (!Number.isInteger(nextCount) || nextCount < 0) throw malformedActionModel("invalid action child count", {
|
|
1281
1503
|
nodeId,
|
|
@@ -1286,21 +1508,157 @@ function readActionModel(fn, mem, modelPtr, budget) {
|
|
|
1286
1508
|
const childId = normalizeNodeId(fn.EPDFAction_GetNextAt(modelPtr, nodeId, index), nodeCount);
|
|
1287
1509
|
next.push(readNode(childId));
|
|
1288
1510
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1511
|
+
const degraded = () => {
|
|
1512
|
+
payloadDropped = true;
|
|
1513
|
+
return {
|
|
1514
|
+
type: "unknown",
|
|
1515
|
+
subtype,
|
|
1516
|
+
next
|
|
1517
|
+
};
|
|
1294
1518
|
};
|
|
1519
|
+
switch (type) {
|
|
1520
|
+
case "javascript": {
|
|
1521
|
+
if (!fn.EPDFAction_NodeHasJavaScript(modelPtr, nodeId)) return degraded();
|
|
1522
|
+
const script = readUtf16String(mem, (buf, capacity) => fn.EPDFAction_GetNodeJavaScript(modelPtr, nodeId, buf, capacity)) ?? "";
|
|
1523
|
+
budget.addScript(script);
|
|
1524
|
+
return {
|
|
1525
|
+
type,
|
|
1526
|
+
subtype,
|
|
1527
|
+
script,
|
|
1528
|
+
next
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
case "rendition": {
|
|
1532
|
+
if (!fn.EPDFAction_NodeHasJavaScript(modelPtr, nodeId)) return {
|
|
1533
|
+
type,
|
|
1534
|
+
subtype,
|
|
1535
|
+
next
|
|
1536
|
+
};
|
|
1537
|
+
const script = readUtf16String(mem, (buf, capacity) => fn.EPDFAction_GetNodeJavaScript(modelPtr, nodeId, buf, capacity)) ?? "";
|
|
1538
|
+
budget.addScript(script);
|
|
1539
|
+
return {
|
|
1540
|
+
type,
|
|
1541
|
+
subtype,
|
|
1542
|
+
script,
|
|
1543
|
+
next
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
case "goto": {
|
|
1547
|
+
const destPtr = fn.EPDFAction_GetNodeDest(docPtr, modelPtr, nodeId);
|
|
1548
|
+
const destination = destPtr !== NULL_PTR ? readDestination(fn, mem, docPtr, destPtr) : null;
|
|
1549
|
+
return destination ? {
|
|
1550
|
+
type,
|
|
1551
|
+
subtype,
|
|
1552
|
+
destination,
|
|
1553
|
+
next
|
|
1554
|
+
} : degraded();
|
|
1555
|
+
}
|
|
1556
|
+
case "uri": {
|
|
1557
|
+
const uri = readPayloadString((buf, capacity) => fn.EPDFAction_GetNodeURI(docPtr, modelPtr, nodeId, buf, capacity));
|
|
1558
|
+
if (uri === null) return degraded();
|
|
1559
|
+
return {
|
|
1560
|
+
type,
|
|
1561
|
+
subtype,
|
|
1562
|
+
uri,
|
|
1563
|
+
isMap: readOutBool((outPtr) => fn.EPDFAction_GetNodeURIIsMap(modelPtr, nodeId, outPtr)) ?? false,
|
|
1564
|
+
next
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
case "named": {
|
|
1568
|
+
const name = readPayloadString((buf, capacity) => fn.EPDFAction_GetNodeName(modelPtr, nodeId, buf, capacity));
|
|
1569
|
+
return name === null ? degraded() : {
|
|
1570
|
+
type,
|
|
1571
|
+
subtype,
|
|
1572
|
+
name,
|
|
1573
|
+
next
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
case "hide": {
|
|
1577
|
+
const count = fn.EPDFAction_GetNodeTargetCount(modelPtr, nodeId);
|
|
1578
|
+
if (!Number.isInteger(count) || count < 0) return degraded();
|
|
1579
|
+
budget.reserveTargets(count);
|
|
1580
|
+
const targets = readTargets(nodeId, count);
|
|
1581
|
+
if (targets === null) return degraded();
|
|
1582
|
+
return {
|
|
1583
|
+
type,
|
|
1584
|
+
subtype,
|
|
1585
|
+
targets,
|
|
1586
|
+
hide: readOutBool((outPtr) => fn.EPDFAction_GetNodeHideFlag(modelPtr, nodeId, outPtr)) ?? true,
|
|
1587
|
+
next
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
case "reset-form": {
|
|
1591
|
+
const state = readResetFormState(nodeId);
|
|
1592
|
+
const count = fn.EPDFAction_GetNodeTargetCount(modelPtr, nodeId);
|
|
1593
|
+
if (state === null || !Number.isInteger(count) || count < 0) return degraded();
|
|
1594
|
+
budget.reserveTargets(count);
|
|
1595
|
+
const fields = state.hasFields ? readTargets(nodeId, count) : null;
|
|
1596
|
+
if (state.hasFields && fields === null) return degraded();
|
|
1597
|
+
return {
|
|
1598
|
+
type,
|
|
1599
|
+
subtype,
|
|
1600
|
+
fields,
|
|
1601
|
+
exclude: state.exclude,
|
|
1602
|
+
next
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
case "submit-form": {
|
|
1606
|
+
if (typeof fn.EPDFAction_GetNodeSubmitForm !== "function") return {
|
|
1607
|
+
type,
|
|
1608
|
+
subtype,
|
|
1609
|
+
next
|
|
1610
|
+
};
|
|
1611
|
+
const state = readSubmitFormState(nodeId);
|
|
1612
|
+
if (state === null) return degraded();
|
|
1613
|
+
const url = readPayloadString((buf, capacity) => fn.EPDFAction_GetNodeSubmitFormURL(modelPtr, nodeId, buf, capacity));
|
|
1614
|
+
if (url === null) return degraded();
|
|
1615
|
+
const count = fn.EPDFAction_GetNodeTargetCount(modelPtr, nodeId);
|
|
1616
|
+
if (!Number.isInteger(count) || count < 0) return degraded();
|
|
1617
|
+
budget.reserveTargets(count);
|
|
1618
|
+
const fields = state.hasFields ? readTargets(nodeId, count) : null;
|
|
1619
|
+
if (state.hasFields && fields === null) return degraded();
|
|
1620
|
+
const charSet = readPayloadString((buf, capacity) => fn.EPDFAction_GetNodeSubmitFormCharSet(modelPtr, nodeId, buf, capacity));
|
|
1621
|
+
return {
|
|
1622
|
+
type,
|
|
1623
|
+
subtype,
|
|
1624
|
+
payload: {
|
|
1625
|
+
url,
|
|
1626
|
+
fields,
|
|
1627
|
+
flags: decodeSubmitFormFlags(state.flags),
|
|
1628
|
+
...charSet === null ? {} : { charSet }
|
|
1629
|
+
},
|
|
1630
|
+
next
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
case "goto-remote":
|
|
1634
|
+
case "goto-embedded":
|
|
1635
|
+
case "launch": {
|
|
1636
|
+
const filePath = readPayloadString((buf, capacity) => fn.EPDFAction_GetNodeFilePath(modelPtr, nodeId, buf, capacity));
|
|
1637
|
+
return filePath === null ? degraded() : {
|
|
1638
|
+
type,
|
|
1639
|
+
subtype,
|
|
1640
|
+
filePath,
|
|
1641
|
+
next
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
default: return {
|
|
1645
|
+
type,
|
|
1646
|
+
subtype,
|
|
1647
|
+
next
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1295
1650
|
} finally {
|
|
1296
1651
|
visiting.delete(nodeId);
|
|
1297
1652
|
}
|
|
1298
1653
|
};
|
|
1654
|
+
const root = rootId === null ? null : readNode(rootId);
|
|
1655
|
+
const warnings = warningsFromFlags(warningFlags, incomplete);
|
|
1656
|
+
if (payloadDropped) warnings.push("payload-dropped");
|
|
1299
1657
|
return {
|
|
1300
|
-
root
|
|
1658
|
+
root,
|
|
1301
1659
|
incomplete,
|
|
1302
1660
|
warningFlags,
|
|
1303
|
-
warnings
|
|
1661
|
+
warnings
|
|
1304
1662
|
};
|
|
1305
1663
|
} finally {
|
|
1306
1664
|
fn.EPDFAction_CloseModel(modelPtr);
|
|
@@ -1380,8 +1738,8 @@ var PagesReader = class {
|
|
|
1380
1738
|
};
|
|
1381
1739
|
function readPageActions(fn, mem, docPtr, pageObjectNumber, budget) {
|
|
1382
1740
|
const actions = {};
|
|
1383
|
-
const open = readActionModel(fn, mem, fn.EPDFDoc_GetPageActionModel(docPtr, pageObjectNumber, 0), budget);
|
|
1384
|
-
const close = readActionModel(fn, mem, fn.EPDFDoc_GetPageActionModel(docPtr, pageObjectNumber, 1), budget);
|
|
1741
|
+
const open = readActionModel(fn, mem, docPtr, fn.EPDFDoc_GetPageActionModel(docPtr, pageObjectNumber, 0), budget);
|
|
1742
|
+
const close = readActionModel(fn, mem, docPtr, fn.EPDFDoc_GetPageActionModel(docPtr, pageObjectNumber, 1), budget);
|
|
1385
1743
|
if (open) actions.open = open;
|
|
1386
1744
|
if (close) actions.close = close;
|
|
1387
1745
|
return open || close ? actions : void 0;
|
|
@@ -1705,6 +2063,42 @@ var PagesInserter = class {
|
|
|
1705
2063
|
cache: null
|
|
1706
2064
|
};
|
|
1707
2065
|
}
|
|
2066
|
+
/**
|
|
2067
|
+
* Create `count` blank pages of `size` at `destIndex`. Same mutation
|
|
2068
|
+
* contract as `insert`, but native creation (`FPDFPage_New`) instead of a
|
|
2069
|
+
* deep copy: no source document, so none of the retain-until-close
|
|
2070
|
+
* lifetime hazard above. Each page gets an empty content stream
|
|
2071
|
+
* (`FPDFPage_GenerateContent`) so the saved PDF renders identically in
|
|
2072
|
+
* third-party viewers.
|
|
2073
|
+
*/
|
|
2074
|
+
insertBlank(spec, destIndex, signal) {
|
|
2075
|
+
throwIfAborted(signal);
|
|
2076
|
+
const { size } = spec;
|
|
2077
|
+
const count = spec.count ?? 1;
|
|
2078
|
+
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}`);
|
|
2079
|
+
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}]`);
|
|
2080
|
+
const { fn } = this.runtime;
|
|
2081
|
+
const destPtr = this.session.requireDocPtr();
|
|
2082
|
+
const beforeCount = fn.FPDF_GetPageCount(destPtr);
|
|
2083
|
+
const at = destIndex ?? beforeCount;
|
|
2084
|
+
if (!Number.isInteger(at) || at < 0 || at > beforeCount) throw new EngineError(EngineErrorCode.InvalidArg, `pages.insertBlank destIndex ${at} out of range [0, ${beforeCount}]`);
|
|
2085
|
+
for (let i = 0; i < count; i++) {
|
|
2086
|
+
const pagePtr = fn.FPDFPage_New(destPtr, at + i, size.width, size.height);
|
|
2087
|
+
if (!pagePtr) {
|
|
2088
|
+
for (let j = 0; j < i; j++) fn.FPDFPage_Delete(destPtr, at);
|
|
2089
|
+
throw new EngineError(EngineErrorCode.Unknown, `FPDFPage_New rejected page ${i + 1}/${count} at index ${at + i}`);
|
|
2090
|
+
}
|
|
2091
|
+
fn.FPDFPage_GenerateContent(pagePtr);
|
|
2092
|
+
fn.FPDF_ClosePage(pagePtr);
|
|
2093
|
+
}
|
|
2094
|
+
this.session.refreshPageRegistry();
|
|
2095
|
+
const layout = new PagesReader(this.runtime, this.session).read(signal);
|
|
2096
|
+
return {
|
|
2097
|
+
insertedPageObjectNumbers: layout.pages.slice(at, at + count).map((page) => page.pageObjectNumber),
|
|
2098
|
+
layout,
|
|
2099
|
+
cache: null
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
1708
2102
|
};
|
|
1709
2103
|
//#endregion
|
|
1710
2104
|
//#region src/features/annotations/internal/read/annotationReadContext.ts
|
|
@@ -2153,6 +2547,49 @@ function readQuadPoints(fn, mem, annotPtr) {
|
|
|
2153
2547
|
});
|
|
2154
2548
|
}
|
|
2155
2549
|
//#endregion
|
|
2550
|
+
//#region src/features/annotations/internal/read/readAnnotationTransformMetadata.ts
|
|
2551
|
+
/**
|
|
2552
|
+
* Read-side twin of `writers/.../writeAnnotationTransformMetadata.ts`. Decodes
|
|
2553
|
+
* the EmbedPDF rotation keys from /EMBD_Metadata back onto the DTO:
|
|
2554
|
+
*
|
|
2555
|
+
* - `Rotation` — degrees, PDF convention. Returned for EVERY rotatable
|
|
2556
|
+
* kind (box AND vertex); the plugin converts it back to its CW-content
|
|
2557
|
+
* `rot` once at the repository seam.
|
|
2558
|
+
* - `UnrotatedRect` — the logical box. BOX kinds only (square/circle/
|
|
2559
|
+
* free-text); a vertex annotation never carries one (its points are the
|
|
2560
|
+
* visual), so vertex readers don't ask for it.
|
|
2561
|
+
*
|
|
2562
|
+
* Both are absent on un-rotated annotations (and on everything authored before
|
|
2563
|
+
* rotation shipped), so both reads return `undefined` and the DTO field stays
|
|
2564
|
+
* unset.
|
|
2565
|
+
*/
|
|
2566
|
+
const KEY_ROTATION$1 = "Rotation";
|
|
2567
|
+
const KEY_UNROTATED_RECT$1 = "UnrotatedRect";
|
|
2568
|
+
/** The advisory/portable rotation scalar (degrees, PDF convention), or
|
|
2569
|
+
* `undefined` when absent or zero. */
|
|
2570
|
+
function readAnnotationRotation(fn, mem, annotPtr) {
|
|
2571
|
+
if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) return void 0;
|
|
2572
|
+
return withScratch(mem, 4, (buf) => {
|
|
2573
|
+
if (!fn.EPDFAnnot_GetEmbedMetadataNumber(annotPtr, KEY_ROTATION$1, buf)) return void 0;
|
|
2574
|
+
const v = readF32(mem, buf);
|
|
2575
|
+
return v ? v : void 0;
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
/** The logical (unrotated) box of a rotated BOX annotation, or `undefined`. */
|
|
2579
|
+
function readAnnotationUnrotatedRect(fn, mem, annotPtr) {
|
|
2580
|
+
if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) return void 0;
|
|
2581
|
+
return withScratch(mem, 16, (buf) => {
|
|
2582
|
+
if (!fn.EPDFAnnot_GetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT$1, buf)) return void 0;
|
|
2583
|
+
const r = readRectF(mem, buf);
|
|
2584
|
+
return {
|
|
2585
|
+
left: r.left,
|
|
2586
|
+
bottom: r.bottom,
|
|
2587
|
+
right: r.right,
|
|
2588
|
+
top: r.top
|
|
2589
|
+
};
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
//#endregion
|
|
2156
2593
|
//#region src/features/annotations/internal/textEditIntent.ts
|
|
2157
2594
|
/** Adobe-compatible `/IT` names, normalized at the engine-core boundary. */
|
|
2158
2595
|
const IT_REPLACE = "Replace";
|
|
@@ -2187,13 +2624,17 @@ function readCaret(fn, mem, annotPtr, base) {
|
|
|
2187
2624
|
const opacity = ca == null ? 1 : Math.max(0, Math.min(1, ca));
|
|
2188
2625
|
const rd = readRectangleDifferences(fn, mem, annotPtr);
|
|
2189
2626
|
const intent = caretIntentFromName(readIntent(fn, mem, annotPtr));
|
|
2627
|
+
const rotation = readAnnotationRotation(fn, mem, annotPtr);
|
|
2628
|
+
const unrotatedRect = readAnnotationUnrotatedRect(fn, mem, annotPtr);
|
|
2190
2629
|
return {
|
|
2191
2630
|
...base,
|
|
2192
2631
|
subtype: "caret",
|
|
2193
2632
|
intent,
|
|
2194
2633
|
color,
|
|
2195
2634
|
opacity,
|
|
2196
|
-
rectDifferences: rd
|
|
2635
|
+
rectDifferences: rd,
|
|
2636
|
+
...rotation != null ? { rotation } : {},
|
|
2637
|
+
...unrotatedRect ? { unrotatedRect } : {}
|
|
2197
2638
|
};
|
|
2198
2639
|
}
|
|
2199
2640
|
//#endregion
|
|
@@ -2611,49 +3052,6 @@ function readFilledStyleExtras(fn, mem, annotPtr) {
|
|
|
2611
3052
|
};
|
|
2612
3053
|
}
|
|
2613
3054
|
//#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
3055
|
//#region src/features/annotations/internal/read/readFreeTextAnnotation.ts
|
|
2658
3056
|
/** Default `/DA` colour (black) when an annotation has no default appearance. */
|
|
2659
3057
|
const DEFAULT_FREETEXT_COLOR$1 = {
|
|
@@ -2759,167 +3157,67 @@ function readLine(fn, mem, annotPtr, base) {
|
|
|
2759
3157
|
};
|
|
2760
3158
|
}
|
|
2761
3159
|
//#endregion
|
|
2762
|
-
//#region src/features/annotations/internal/destinationViewCodes.ts
|
|
2763
|
-
/**
|
|
2764
|
-
* `PDFDEST_VIEW_*` fit-type codes (public/fpdf_doc.h) — the shared
|
|
2765
|
-
* vocabulary of `FPDFDest_GetView` (read) and `EPDFDest_CreateView`
|
|
2766
|
-
* (write). XYZ is listed for completeness but never goes through
|
|
2767
|
-
* CreateView (it has its own null-aware constructor).
|
|
2768
|
-
*/
|
|
2769
|
-
const DEST_VIEW = {
|
|
2770
|
-
unknown: 0,
|
|
2771
|
-
xyz: 1,
|
|
2772
|
-
fit: 2,
|
|
2773
|
-
fitH: 3,
|
|
2774
|
-
fitV: 4,
|
|
2775
|
-
fitR: 5,
|
|
2776
|
-
fitB: 6,
|
|
2777
|
-
fitBH: 7,
|
|
2778
|
-
fitBV: 8
|
|
2779
|
-
};
|
|
2780
|
-
/** Wire destination kind → PDFDEST_VIEW code, for the non-XYZ kinds. */
|
|
2781
|
-
const VIEW_CODE_BY_KIND = {
|
|
2782
|
-
fit: DEST_VIEW.fit,
|
|
2783
|
-
fitH: DEST_VIEW.fitH,
|
|
2784
|
-
fitV: DEST_VIEW.fitV,
|
|
2785
|
-
fitR: DEST_VIEW.fitR,
|
|
2786
|
-
fitB: DEST_VIEW.fitB,
|
|
2787
|
-
fitBH: DEST_VIEW.fitBH,
|
|
2788
|
-
fitBV: DEST_VIEW.fitBV
|
|
2789
|
-
};
|
|
2790
|
-
//#endregion
|
|
2791
|
-
//#region src/features/annotations/internal/read/readDestination.ts
|
|
2792
|
-
const MAX_VIEW_PARAMS = 4;
|
|
2793
|
-
/**
|
|
2794
|
-
* Materialise one explicit `PdfDestination` from a live `destPtr`. PDFium
|
|
2795
|
-
* already resolved named destinations before handing us the pointer, so
|
|
2796
|
-
* only explicit arrays arrive here (the rule `PdfDestination` documents).
|
|
2797
|
-
*
|
|
2798
|
-
* Returns `null` when the destination is unreadable: no visible indirect page
|
|
2799
|
-
* in the document, or an unknown fit type. Coordinates stay raw PDF user
|
|
2800
|
-
* space — no conversion at this layer.
|
|
2801
|
-
*
|
|
2802
|
-
* Shared by the link reader today; the outline/bookmark port reuses it.
|
|
2803
|
-
*/
|
|
2804
|
-
function readDestination(fn, mem, docPtr, destPtr) {
|
|
2805
|
-
const pageObjectNumber = fn.EPDFDest_GetPageObjectNumber(docPtr, destPtr);
|
|
2806
|
-
if (pageObjectNumber === 0) return null;
|
|
2807
|
-
const view = withScratchN(mem, [4, MAX_VIEW_PARAMS * 4], ([countPtr, paramsPtr]) => {
|
|
2808
|
-
const code = fn.FPDFDest_GetView(destPtr, countPtr, paramsPtr);
|
|
2809
|
-
const count = Math.min(readI32(mem, countPtr), MAX_VIEW_PARAMS);
|
|
2810
|
-
const params = [];
|
|
2811
|
-
for (let i = 0; i < count; i++) params.push(readF32(mem, paramsPtr, i * 4));
|
|
2812
|
-
return {
|
|
2813
|
-
code,
|
|
2814
|
-
params
|
|
2815
|
-
};
|
|
2816
|
-
});
|
|
2817
|
-
const at = (i) => i < view.params.length ? view.params[i] : null;
|
|
2818
|
-
switch (view.code) {
|
|
2819
|
-
case DEST_VIEW.xyz: return {
|
|
2820
|
-
kind: "xyz",
|
|
2821
|
-
pageObjectNumber,
|
|
2822
|
-
...readXyzLocation(fn, mem, destPtr)
|
|
2823
|
-
};
|
|
2824
|
-
case DEST_VIEW.fit: return {
|
|
2825
|
-
kind: "fit",
|
|
2826
|
-
pageObjectNumber
|
|
2827
|
-
};
|
|
2828
|
-
case DEST_VIEW.fitH: return {
|
|
2829
|
-
kind: "fitH",
|
|
2830
|
-
pageObjectNumber,
|
|
2831
|
-
top: at(0)
|
|
2832
|
-
};
|
|
2833
|
-
case DEST_VIEW.fitV: return {
|
|
2834
|
-
kind: "fitV",
|
|
2835
|
-
pageObjectNumber,
|
|
2836
|
-
left: at(0)
|
|
2837
|
-
};
|
|
2838
|
-
case DEST_VIEW.fitR:
|
|
2839
|
-
if (view.params.length < 4) return {
|
|
2840
|
-
kind: "fit",
|
|
2841
|
-
pageObjectNumber
|
|
2842
|
-
};
|
|
2843
|
-
return {
|
|
2844
|
-
kind: "fitR",
|
|
2845
|
-
pageObjectNumber,
|
|
2846
|
-
left: view.params[0],
|
|
2847
|
-
bottom: view.params[1],
|
|
2848
|
-
right: view.params[2],
|
|
2849
|
-
top: view.params[3]
|
|
2850
|
-
};
|
|
2851
|
-
case DEST_VIEW.fitB: return {
|
|
2852
|
-
kind: "fitB",
|
|
2853
|
-
pageObjectNumber
|
|
2854
|
-
};
|
|
2855
|
-
case DEST_VIEW.fitBH: return {
|
|
2856
|
-
kind: "fitBH",
|
|
2857
|
-
pageObjectNumber,
|
|
2858
|
-
top: at(0)
|
|
2859
|
-
};
|
|
2860
|
-
case DEST_VIEW.fitBV: return {
|
|
2861
|
-
kind: "fitBV",
|
|
2862
|
-
pageObjectNumber,
|
|
2863
|
-
left: at(0)
|
|
2864
|
-
};
|
|
2865
|
-
default: return null;
|
|
2866
|
-
}
|
|
2867
|
-
}
|
|
2868
|
-
/** `/XYZ` null-aware axes via `FPDFDest_GetLocationInPage` (FPDF_BOOL is 4 bytes). */
|
|
2869
|
-
function readXyzLocation(fn, mem, destPtr) {
|
|
2870
|
-
return withScratchN(mem, [
|
|
2871
|
-
4,
|
|
2872
|
-
4,
|
|
2873
|
-
4,
|
|
2874
|
-
4,
|
|
2875
|
-
4,
|
|
2876
|
-
4
|
|
2877
|
-
], ([hasLeft, hasTop, hasZoom, left, top, zoom]) => {
|
|
2878
|
-
if (!fn.FPDFDest_GetLocationInPage(destPtr, hasLeft, hasTop, hasZoom, left, top, zoom)) return {
|
|
2879
|
-
left: null,
|
|
2880
|
-
top: null,
|
|
2881
|
-
zoom: null
|
|
2882
|
-
};
|
|
2883
|
-
return {
|
|
2884
|
-
left: readI32(mem, hasLeft) ? readF32(mem, left) : null,
|
|
2885
|
-
top: readI32(mem, hasTop) ? readF32(mem, top) : null,
|
|
2886
|
-
zoom: readI32(mem, hasZoom) ? readF32(mem, zoom) : null
|
|
2887
|
-
};
|
|
2888
|
-
});
|
|
2889
|
-
}
|
|
2890
|
-
//#endregion
|
|
2891
3160
|
//#region src/features/annotations/internal/read/readLinkAnnotation.ts
|
|
2892
3161
|
/**
|
|
2893
3162
|
* Link reader: rect/flags/relationship ride the base (a link grouped to an
|
|
2894
3163
|
* annotation is plain `inReplyTo` + `replyType: 'group'`); this module only
|
|
2895
3164
|
* materialises the normalized target.
|
|
2896
3165
|
*
|
|
2897
|
-
* ONE truth,
|
|
2898
|
-
*
|
|
2899
|
-
*
|
|
2900
|
-
*
|
|
2901
|
-
* and Named-blind) is deliberately not consulted; `FPDFLink_GetDest`
|
|
2902
|
-
* remains only for the data-only direct `/Dest`, which is not an action.
|
|
3166
|
+
* ONE truth, one projection: the target is a pure function of the SAME
|
|
3167
|
+
* payload-carrying action tree the scripting plane reads
|
|
3168
|
+
* (`base.actions.activate`) — no second native read exists any more, so the
|
|
3169
|
+
* two action-shaped surfaces cannot drift by construction.
|
|
2903
3170
|
*
|
|
2904
|
-
* `/A`
|
|
2905
|
-
*
|
|
2906
|
-
*
|
|
3171
|
+
* `/A` precedence, pinned: an activate action that exists but cannot be
|
|
3172
|
+
* executed (an `incomplete` tree, a degraded/unreadable root) projects
|
|
3173
|
+
* `unsupported` — a broken action is a dead link, never an invitation to
|
|
3174
|
+
* guess. The direct `/Dest` (which is data, not an action) is consulted
|
|
3175
|
+
* ONLY when no `/A` exists at all.
|
|
2907
3176
|
*/
|
|
2908
3177
|
function readLink(fn, mem, annotPtr, base, _rawSubtypeCode, ctx) {
|
|
2909
3178
|
return {
|
|
2910
3179
|
...base,
|
|
2911
3180
|
subtype: "link",
|
|
2912
|
-
target: readLinkTarget(fn, mem, annotPtr, ctx)
|
|
3181
|
+
target: readLinkTarget(fn, mem, annotPtr, base, ctx)
|
|
2913
3182
|
};
|
|
2914
3183
|
}
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
3184
|
+
/**
|
|
3185
|
+
* Project the navigation view from a payload-carrying activate tree. Pure —
|
|
3186
|
+
* shared with any consumer that wants the root-level navigation reading of a
|
|
3187
|
+
* tree (the link plugin's no-actions-plugin fallback uses the same law).
|
|
3188
|
+
*/
|
|
3189
|
+
function linkTargetFromActionTree(tree) {
|
|
3190
|
+
if (tree.incomplete) return { kind: "unsupported" };
|
|
3191
|
+
const root = tree.root;
|
|
3192
|
+
if (!root) return { kind: "unsupported" };
|
|
3193
|
+
switch (root.type) {
|
|
3194
|
+
case "goto": return {
|
|
3195
|
+
kind: "goto",
|
|
3196
|
+
destination: root.destination
|
|
3197
|
+
};
|
|
3198
|
+
case "uri": return {
|
|
3199
|
+
kind: "uri",
|
|
3200
|
+
uri: root.uri
|
|
3201
|
+
};
|
|
3202
|
+
case "goto-remote": return {
|
|
3203
|
+
kind: "goto-remote",
|
|
3204
|
+
file: root.filePath
|
|
3205
|
+
};
|
|
3206
|
+
case "launch": return {
|
|
3207
|
+
kind: "launch",
|
|
3208
|
+
path: root.filePath
|
|
3209
|
+
};
|
|
3210
|
+
case "javascript": return { kind: "javascript" };
|
|
3211
|
+
case "named": return {
|
|
3212
|
+
kind: "named",
|
|
3213
|
+
name: root.name
|
|
3214
|
+
};
|
|
3215
|
+
default: return { kind: "unsupported" };
|
|
2922
3216
|
}
|
|
3217
|
+
}
|
|
3218
|
+
function readLinkTarget(fn, mem, annotPtr, base, ctx) {
|
|
3219
|
+
const activate = base.actions?.activate;
|
|
3220
|
+
if (activate) return linkTargetFromActionTree(activate);
|
|
2923
3221
|
const linkPtr = fn.FPDFAnnot_GetLink(annotPtr);
|
|
2924
3222
|
if (!linkPtr) return null;
|
|
2925
3223
|
const destPtr = fn.FPDFLink_GetDest(ctx.docPtr, linkPtr);
|
|
@@ -2930,43 +3228,6 @@ function readLinkTarget(fn, mem, annotPtr, ctx) {
|
|
|
2930
3228
|
destination
|
|
2931
3229
|
} : { kind: "unsupported" };
|
|
2932
3230
|
}
|
|
2933
|
-
/** Classify the root action node and fetch its payload — the projection. */
|
|
2934
|
-
function readRootTarget(fn, mem, modelPtr, node, ctx) {
|
|
2935
|
-
switch (actionTypeFromCode(fn.EPDFAction_GetNodeType(modelPtr, node))) {
|
|
2936
|
-
case "goto": {
|
|
2937
|
-
const destPtr = fn.EPDFAction_GetNodeDest(ctx.docPtr, modelPtr, node);
|
|
2938
|
-
const destination = destPtr ? readDestination(fn, mem, ctx.docPtr, destPtr) : null;
|
|
2939
|
-
return destination ? {
|
|
2940
|
-
kind: "goto",
|
|
2941
|
-
destination
|
|
2942
|
-
} : { kind: "unsupported" };
|
|
2943
|
-
}
|
|
2944
|
-
case "uri": {
|
|
2945
|
-
const uri = readUtf8String(mem, (buf, capacity) => fn.EPDFAction_GetNodeURI(ctx.docPtr, modelPtr, node, buf, capacity));
|
|
2946
|
-
return uri == null ? { kind: "unsupported" } : {
|
|
2947
|
-
kind: "uri",
|
|
2948
|
-
uri
|
|
2949
|
-
};
|
|
2950
|
-
}
|
|
2951
|
-
case "goto-remote": return {
|
|
2952
|
-
kind: "goto-remote",
|
|
2953
|
-
file: readUtf8String(mem, (buf, capacity) => fn.EPDFAction_GetNodeFilePath(modelPtr, node, buf, capacity)) ?? ""
|
|
2954
|
-
};
|
|
2955
|
-
case "launch": return {
|
|
2956
|
-
kind: "launch",
|
|
2957
|
-
path: readUtf8String(mem, (buf, capacity) => fn.EPDFAction_GetNodeFilePath(modelPtr, node, buf, capacity)) ?? ""
|
|
2958
|
-
};
|
|
2959
|
-
case "javascript": return { kind: "javascript" };
|
|
2960
|
-
case "named": {
|
|
2961
|
-
const name = readUtf8String(mem, (buf, capacity) => fn.EPDFAction_GetNodeName(modelPtr, node, buf, capacity));
|
|
2962
|
-
return name == null ? { kind: "unsupported" } : {
|
|
2963
|
-
kind: "named",
|
|
2964
|
-
name
|
|
2965
|
-
};
|
|
2966
|
-
}
|
|
2967
|
-
default: return { kind: "unsupported" };
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
3231
|
//#endregion
|
|
2971
3232
|
//#region src/features/annotations/internal/read/readRedactAnnotation.ts
|
|
2972
3233
|
/** Default `/C` marking outline (red) — the redaction marking convention and
|
|
@@ -3106,6 +3367,47 @@ function readStampName(fn, mem, annotPtr) {
|
|
|
3106
3367
|
return readAnnotString(fn, mem, annotPtr, "Name");
|
|
3107
3368
|
}
|
|
3108
3369
|
//#endregion
|
|
3370
|
+
//#region src/features/annotations/internal/annotationState.ts
|
|
3371
|
+
/**
|
|
3372
|
+
* `/State` + `/StateModel` spelling maps — ISO 32000 §12.5.6.3, Table 174.
|
|
3373
|
+
*
|
|
3374
|
+
* Known values are wire-stable lowercase (the house vocabulary — same
|
|
3375
|
+
* rationale as `AnnotationReplyType` / `AnnotationBorderStyle`); the PDF
|
|
3376
|
+
* stores the capitalized Table 174 spellings. Unknown values — custom
|
|
3377
|
+
* Acrobat state models and their states — pass through VERBATIM in both
|
|
3378
|
+
* directions, so foreign review workflows survive a round trip.
|
|
3379
|
+
*/
|
|
3380
|
+
const STATE_MODEL_TO_PDF = {
|
|
3381
|
+
review: "Review",
|
|
3382
|
+
marked: "Marked"
|
|
3383
|
+
};
|
|
3384
|
+
const STATE_TO_PDF = {
|
|
3385
|
+
accepted: "Accepted",
|
|
3386
|
+
rejected: "Rejected",
|
|
3387
|
+
cancelled: "Cancelled",
|
|
3388
|
+
completed: "Completed",
|
|
3389
|
+
none: "None",
|
|
3390
|
+
marked: "Marked",
|
|
3391
|
+
unmarked: "Unmarked"
|
|
3392
|
+
};
|
|
3393
|
+
const PDF_TO_STATE_MODEL = invert(STATE_MODEL_TO_PDF);
|
|
3394
|
+
const PDF_TO_STATE = invert(STATE_TO_PDF);
|
|
3395
|
+
function stateModelToPdf(value) {
|
|
3396
|
+
return STATE_MODEL_TO_PDF[value] ?? value;
|
|
3397
|
+
}
|
|
3398
|
+
function stateModelFromPdf(value) {
|
|
3399
|
+
return PDF_TO_STATE_MODEL[value] ?? value;
|
|
3400
|
+
}
|
|
3401
|
+
function stateToPdf(value) {
|
|
3402
|
+
return STATE_TO_PDF[value] ?? value;
|
|
3403
|
+
}
|
|
3404
|
+
function stateFromPdf(value) {
|
|
3405
|
+
return PDF_TO_STATE[value] ?? value;
|
|
3406
|
+
}
|
|
3407
|
+
function invert(map) {
|
|
3408
|
+
return Object.fromEntries(Object.entries(map).map(([wire, pdf]) => [pdf, wire]));
|
|
3409
|
+
}
|
|
3410
|
+
//#endregion
|
|
3109
3411
|
//#region src/features/annotations/internal/read/readTextAnnotation.ts
|
|
3110
3412
|
/** Default `/C` — matches the generator's yellow note fill and the writer default. */
|
|
3111
3413
|
const DEFAULT_NOTE_COLOR$1 = {
|
|
@@ -3120,12 +3422,16 @@ function readText(fn, mem, annotPtr, base) {
|
|
|
3120
3422
|
const ca = readAnnotOpacity(fn, mem, annotPtr);
|
|
3121
3423
|
const opacity = ca == null ? 1 : Math.max(0, Math.min(1, ca));
|
|
3122
3424
|
const icon = NOTE_CODE_TO_ICON[fn.EPDFAnnot_GetName(annotPtr)] ?? DEFAULT_NOTE_ICON;
|
|
3425
|
+
const stateRaw = readAnnotString(fn, mem, annotPtr, "State");
|
|
3426
|
+
const stateModelRaw = readAnnotString(fn, mem, annotPtr, "StateModel");
|
|
3123
3427
|
return {
|
|
3124
3428
|
...base,
|
|
3125
3429
|
subtype: "text",
|
|
3126
3430
|
icon,
|
|
3127
3431
|
color,
|
|
3128
|
-
opacity
|
|
3432
|
+
opacity,
|
|
3433
|
+
state: stateRaw === null ? null : stateFromPdf(stateRaw),
|
|
3434
|
+
stateModel: stateModelRaw === null ? null : stateModelFromPdf(stateModelRaw)
|
|
3129
3435
|
};
|
|
3130
3436
|
}
|
|
3131
3437
|
//#endregion
|
|
@@ -3431,7 +3737,7 @@ function readOptions(runtime, model, fieldIndex) {
|
|
|
3431
3737
|
return options;
|
|
3432
3738
|
}
|
|
3433
3739
|
/** Read one field of the native model into its per-family DTO. */
|
|
3434
|
-
function readFieldAt(runtime, model, fieldIndex, actionBudget = new ActionReadBudgetTracker()) {
|
|
3740
|
+
function readFieldAt(runtime, model, fieldIndex, docPtr, actionBudget = new ActionReadBudgetTracker()) {
|
|
3435
3741
|
const { fn } = runtime;
|
|
3436
3742
|
const fieldObjectNumber = fn.EPDFForm_GetFieldObjNum(model, fieldIndex);
|
|
3437
3743
|
const name = readWide(runtime, (buf, cap) => fn.EPDFForm_GetFieldName(model, fieldIndex, buf, cap));
|
|
@@ -3439,7 +3745,7 @@ function readFieldAt(runtime, model, fieldIndex, actionBudget = new ActionReadBu
|
|
|
3439
3745
|
const family = FAMILY_BY_CODE[fn.EPDFForm_GetFieldFamily(model, fieldIndex)] ?? "unknown";
|
|
3440
3746
|
const valueEntry = readValueEntry(runtime, model, fieldIndex, false);
|
|
3441
3747
|
const defaultValueEntry = readValueEntry(runtime, model, fieldIndex, true);
|
|
3442
|
-
const actions = readFieldActions(runtime, model, fieldIndex, actionBudget);
|
|
3748
|
+
const actions = readFieldActions(runtime, model, fieldIndex, docPtr, actionBudget);
|
|
3443
3749
|
const base = {
|
|
3444
3750
|
ref: fieldObjectNumber > 0 ? {
|
|
3445
3751
|
kind: "objectNumber",
|
|
@@ -3538,12 +3844,12 @@ function readFieldAt(runtime, model, fieldIndex, actionBudget = new ActionReadBu
|
|
|
3538
3844
|
}
|
|
3539
3845
|
}
|
|
3540
3846
|
/** Read the whole native model into a detached {@link FormSnapshot}. */
|
|
3541
|
-
function readFormSnapshot(runtime, model) {
|
|
3847
|
+
function readFormSnapshot(runtime, model, docPtr) {
|
|
3542
3848
|
const { fn } = runtime;
|
|
3543
3849
|
const count = fn.EPDFForm_CountFields(model);
|
|
3544
3850
|
const fields = [];
|
|
3545
3851
|
const actionBudget = new ActionReadBudgetTracker();
|
|
3546
|
-
for (let i = 0; i < count; i++) fields.push(readFieldAt(runtime, model, i, actionBudget));
|
|
3852
|
+
for (let i = 0; i < count; i++) fields.push(readFieldAt(runtime, model, i, docPtr, actionBudget));
|
|
3547
3853
|
const calculationOrder = [];
|
|
3548
3854
|
const calculationCount = fn.EPDFForm_CountCalculationOrder(model);
|
|
3549
3855
|
for (let index = 0; index < calculationCount; index++) {
|
|
@@ -3579,7 +3885,7 @@ function entryScalar(entry) {
|
|
|
3579
3885
|
if (entry.kind === "array") return entry.values[0] ?? "";
|
|
3580
3886
|
return "";
|
|
3581
3887
|
}
|
|
3582
|
-
function readFieldActions(runtime, model, fieldIndex, budget) {
|
|
3888
|
+
function readFieldActions(runtime, model, fieldIndex, docPtr, budget) {
|
|
3583
3889
|
const { fn, mem } = runtime;
|
|
3584
3890
|
const entries = [
|
|
3585
3891
|
["keystroke", 0],
|
|
@@ -3589,7 +3895,7 @@ function readFieldActions(runtime, model, fieldIndex, budget) {
|
|
|
3589
3895
|
];
|
|
3590
3896
|
const actions = {};
|
|
3591
3897
|
for (const [key, event] of entries) {
|
|
3592
|
-
const action = readActionModel(fn, mem, fn.EPDFForm_GetFieldActionModel(model, fieldIndex, event), budget);
|
|
3898
|
+
const action = readActionModel(fn, mem, docPtr, fn.EPDFForm_GetFieldActionModel(model, fieldIndex, event), budget);
|
|
3593
3899
|
if (action) actions[key] = action;
|
|
3594
3900
|
}
|
|
3595
3901
|
return Object.keys(actions).length > 0 ? actions : void 0;
|
|
@@ -3835,18 +4141,19 @@ function blendModeFromCode(code) {
|
|
|
3835
4141
|
* per-subtype reader builds its DTO by extending this with its own
|
|
3836
4142
|
* fields and `subtype: '...'` discriminator.
|
|
3837
4143
|
*/
|
|
3838
|
-
function readAnnotationBase(fn, mem, annotPtr, pageObjectNumber, index, revision, actionBudget = new ActionReadBudgetTracker()) {
|
|
4144
|
+
function readAnnotationBase(fn, mem, docPtr, annotPtr, pageObjectNumber, index, revision, actionBudget = new ActionReadBudgetTracker()) {
|
|
3839
4145
|
const identity = readAnnotationIdentity(fn, mem, annotPtr, pageObjectNumber, index, revision);
|
|
3840
4146
|
const rect = readAnnotRect(fn, mem, annotPtr);
|
|
3841
4147
|
const flags = readAnnotFlags(fn, annotPtr);
|
|
3842
4148
|
const contents = readAnnotString(fn, mem, annotPtr, "Contents");
|
|
4149
|
+
const subject = readAnnotString(fn, mem, annotPtr, "Subj");
|
|
3843
4150
|
const author = readAnnotString(fn, mem, annotPtr, "T");
|
|
3844
4151
|
const createdRaw = readAnnotString(fn, mem, annotPtr, "CreationDate");
|
|
3845
4152
|
const modifiedRaw = readAnnotString(fn, mem, annotPtr, "M");
|
|
3846
4153
|
const blendMode = blendModeFromCode(fn.EPDFAnnot_GetBlendMode(annotPtr));
|
|
3847
4154
|
const embd = readEmbedMetadata(fn, mem, annotPtr);
|
|
3848
4155
|
const relationship = readAnnotationRelationship(fn, mem, annotPtr, pageObjectNumber);
|
|
3849
|
-
const actions = readAnnotationActions(fn, mem, annotPtr, actionBudget);
|
|
4156
|
+
const actions = readAnnotationActions(fn, mem, docPtr, annotPtr, actionBudget);
|
|
3850
4157
|
return {
|
|
3851
4158
|
ref: identity.ref,
|
|
3852
4159
|
pageObjectNumber,
|
|
@@ -3856,6 +4163,7 @@ function readAnnotationBase(fn, mem, annotPtr, pageObjectNumber, index, revision
|
|
|
3856
4163
|
flags,
|
|
3857
4164
|
rect,
|
|
3858
4165
|
contents,
|
|
4166
|
+
subject,
|
|
3859
4167
|
author,
|
|
3860
4168
|
created: createdRaw ? pdfDateToIso(createdRaw) : null,
|
|
3861
4169
|
modified: modifiedRaw ? pdfDateToIso(modifiedRaw) : null,
|
|
@@ -3869,7 +4177,7 @@ function readAnnotationBase(fn, mem, annotPtr, pageObjectNumber, index, revision
|
|
|
3869
4177
|
...actions ? { actions } : {}
|
|
3870
4178
|
};
|
|
3871
4179
|
}
|
|
3872
|
-
function readAnnotationActions(fn, mem, annotPtr, budget) {
|
|
4180
|
+
function readAnnotationActions(fn, mem, docPtr, annotPtr, budget) {
|
|
3873
4181
|
const events = [
|
|
3874
4182
|
["activate", 0],
|
|
3875
4183
|
["cursorEnter", 1],
|
|
@@ -3885,7 +4193,7 @@ function readAnnotationActions(fn, mem, annotPtr, budget) {
|
|
|
3885
4193
|
];
|
|
3886
4194
|
const actions = {};
|
|
3887
4195
|
for (const [key, event] of events) {
|
|
3888
|
-
const action = readActionModel(fn, mem, fn.EPDFAnnot_GetActionModel(annotPtr, event), budget);
|
|
4196
|
+
const action = readActionModel(fn, mem, docPtr, fn.EPDFAnnot_GetActionModel(annotPtr, event), budget);
|
|
3889
4197
|
if (action) actions[key] = action;
|
|
3890
4198
|
}
|
|
3891
4199
|
return Object.keys(actions).length > 0 ? actions : void 0;
|
|
@@ -3916,7 +4224,7 @@ function collectPageAnnotations(input) {
|
|
|
3916
4224
|
const annotPtr = getAnnotPtrAt(i);
|
|
3917
4225
|
if (!annotPtr) continue;
|
|
3918
4226
|
try {
|
|
3919
|
-
const base = readAnnotationBase(fn, mem, annotPtr, pageObjectNumber, i, revision, actionBudget);
|
|
4227
|
+
const base = readAnnotationBase(fn, mem, readCtx.docPtr, annotPtr, pageObjectNumber, i, revision, actionBudget);
|
|
3920
4228
|
const subtypeCode = fn.FPDFAnnot_GetSubtype(annotPtr);
|
|
3921
4229
|
const { reader } = pickReader(subtypeCode);
|
|
3922
4230
|
const dto = reader(fn, mem, annotPtr, base, subtypeCode, readCtx);
|
|
@@ -4130,19 +4438,31 @@ const ANNOT_SUBTYPE_WIDGET = 20;
|
|
|
4130
4438
|
/** `FPDF_ANNOT_FREETEXT` — free-text annotation subtype code. */
|
|
4131
4439
|
const ANNOT_SUBTYPE_FREETEXT = 3;
|
|
4132
4440
|
/**
|
|
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
|
|
4441
|
+
* BOX-family subtypes (free-text 3, square 5, circle 6, stamp 13, caret 14):
|
|
4442
|
+
* the kinds whose v3 writers put rotation in the AP `/Matrix` +
|
|
4443
|
+
* `/EMBD_Metadata` `/UnrotatedRect`. Only these are eligible for
|
|
4444
|
+
* rotation-stripped appearance rendering — vertex kinds
|
|
4445
|
+
* (line/polyline/polygon/ink) pre-rotate their geometry, so their rasters must
|
|
4446
|
+
* stay on the classic path. This set MUST cover every kind whose reader
|
|
4447
|
+
* surfaces the rotation pair: `fromDTO` (plugin-annotation repository) mirrors
|
|
4448
|
+
* this exact condition to re-apply the stripped rotation as `apRot`.
|
|
4138
4449
|
*/
|
|
4139
4450
|
const BOX_FAMILY_SUBTYPES = /* @__PURE__ */ new Set([
|
|
4140
4451
|
3,
|
|
4141
4452
|
5,
|
|
4142
4453
|
6,
|
|
4143
|
-
13
|
|
4454
|
+
13,
|
|
4455
|
+
14
|
|
4144
4456
|
]);
|
|
4145
4457
|
/**
|
|
4458
|
+
* Per-appearance output-pixel ceiling (see the clamp in `renderOne`).
|
|
4459
|
+
* 16 M px ≈ a 4000² raster ≈ 64 MB of transient RGBA — safely inside the
|
|
4460
|
+
* wasm heap for the one-at-a-time batch loop, while a page-sized annotation
|
|
4461
|
+
* stays crisp to roughly 5–6× before its appearance starts stretching.
|
|
4462
|
+
* Small annotations (the overwhelming majority) never come near it.
|
|
4463
|
+
*/
|
|
4464
|
+
const APPEARANCE_PIXEL_CLAMP = 16e6;
|
|
4465
|
+
/**
|
|
4146
4466
|
* Maps an `AnnotationAppearanceMode` onto the PDFium appearance-mode int and
|
|
4147
4467
|
* the `EPDFAnnot_GetAvailableAppearanceModes` bit it occupies.
|
|
4148
4468
|
* N -> mode 0 / bit 1, R -> mode 1 / bit 2, D -> mode 2 / bit 4.
|
|
@@ -4241,13 +4561,15 @@ var AnnotationAppearanceReader = class {
|
|
|
4241
4561
|
fn.EPDFAnnot_GenerateFormFieldAP(annotPtr);
|
|
4242
4562
|
if (!fn.EPDFAnnot_HasAppearanceStream(annotPtr, modeInt)) return null;
|
|
4243
4563
|
} else return null;
|
|
4564
|
+
const rectArea = Math.max(1, (rect.right - rect.left) * (rect.top - rect.bottom));
|
|
4565
|
+
const effScale = Math.min(scale, Math.sqrt(APPEARANCE_PIXEL_CLAMP / rectArea));
|
|
4244
4566
|
return rasterize(this.runtime, {
|
|
4245
4567
|
rect,
|
|
4246
4568
|
page,
|
|
4247
4569
|
rotation,
|
|
4248
4570
|
viewport: {
|
|
4249
4571
|
kind: "scale",
|
|
4250
|
-
scale
|
|
4572
|
+
scale: effScale
|
|
4251
4573
|
},
|
|
4252
4574
|
...maxOutputPixels !== void 0 ? { maxOutputPixels } : {},
|
|
4253
4575
|
background: "transparent",
|
|
@@ -4362,7 +4684,9 @@ function setAnnotFlags(fn, annotPtr, partial) {
|
|
|
4362
4684
|
if (!fn.FPDFAnnot_SetFlags(annotPtr, next)) throw new EngineError(EngineErrorCode.Unknown, "FPDFAnnot_SetFlags returned false");
|
|
4363
4685
|
}
|
|
4364
4686
|
/**
|
|
4365
|
-
* Write `/Rect`
|
|
4687
|
+
* Write `/Rect` without touching `/AP`. Appearance preservation/regeneration
|
|
4688
|
+
* belongs to AnnotationMutator; a geometry primitive must not silently rewrite
|
|
4689
|
+
* an existing appearance `/BBox`. FS_RECTF layout is
|
|
4366
4690
|
* `{ left, top, right, bottom }` (top > bottom in PDF coords).
|
|
4367
4691
|
*/
|
|
4368
4692
|
function setAnnotRect(fn, mem, annotPtr, rect) {
|
|
@@ -4372,12 +4696,40 @@ function setAnnotRect(fn, mem, annotPtr, rect) {
|
|
|
4372
4696
|
mem.poke(buf, "f32", rect.top, 4);
|
|
4373
4697
|
mem.poke(buf, "f32", rect.right, 8);
|
|
4374
4698
|
mem.poke(buf, "f32", rect.bottom, 12);
|
|
4375
|
-
if (!fn.
|
|
4699
|
+
if (!fn.EPDFAnnot_SetRect(annotPtr, buf)) throw new EngineError(EngineErrorCode.Unknown, "EPDFAnnot_SetRect returned false");
|
|
4376
4700
|
} finally {
|
|
4377
4701
|
mem.free(buf);
|
|
4378
4702
|
}
|
|
4379
4703
|
}
|
|
4380
4704
|
/**
|
|
4705
|
+
* Write a UTF-16 string entry into an annotation dictionary. The write
|
|
4706
|
+
* side of `readAnnotString`.
|
|
4707
|
+
*/
|
|
4708
|
+
function writeAnnotString(fn, mem, annotPtr, key, value) {
|
|
4709
|
+
const ptr = mem.writeU16String(value);
|
|
4710
|
+
try {
|
|
4711
|
+
fn.FPDFAnnot_SetStringValue(annotPtr, key, ptr);
|
|
4712
|
+
} finally {
|
|
4713
|
+
mem.free(ptr);
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
/**
|
|
4717
|
+
* Three-state string write: `null` REMOVES the dictionary entry.
|
|
4718
|
+
*
|
|
4719
|
+
* Clearing goes through `EPDFAnnot_RemoveKey` because
|
|
4720
|
+
* `FPDFAnnot_SetStringValue` with a NULL value writes an EMPTY string —
|
|
4721
|
+
* it never removes. True removal is what keeps the read side honest:
|
|
4722
|
+
* `readAnnotString` returns `null` iff the key is absent, so a cleared
|
|
4723
|
+
* field reads back as `null`, not `''`.
|
|
4724
|
+
*/
|
|
4725
|
+
function writeAnnotStringOrClear(fn, mem, annotPtr, key, value) {
|
|
4726
|
+
if (value === null) {
|
|
4727
|
+
fn.EPDFAnnot_RemoveKey(annotPtr, key);
|
|
4728
|
+
return;
|
|
4729
|
+
}
|
|
4730
|
+
writeAnnotString(fn, mem, annotPtr, key, value);
|
|
4731
|
+
}
|
|
4732
|
+
/**
|
|
4381
4733
|
* Set `/BS /S` (border style code) and `/BS /W` (width) in one call via
|
|
4382
4734
|
* `EPDFAnnot_SetBorderStyle`. `styleCode` is a raw
|
|
4383
4735
|
* `FPDF_ANNOT_BORDER_STYLE` enum value — the string<->code mapping lives
|
|
@@ -4576,8 +4928,8 @@ function setCalloutLine(fn, mem, annotPtr, points) {
|
|
|
4576
4928
|
//#region src/features/annotations/internal/write/writeAnnotationBase.ts
|
|
4577
4929
|
/**
|
|
4578
4930
|
* 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
|
|
4931
|
+
* (contents/subject/nm). The kind-specific writer calls this BEFORE its
|
|
4932
|
+
* own field writes; order doesn't actually matter at the PDF level, but
|
|
4581
4933
|
* keeping the base first makes the writers symmetric with the readers.
|
|
4582
4934
|
*
|
|
4583
4935
|
* `/T` (author display) is stamped by the mutator from
|
|
@@ -4588,13 +4940,14 @@ function setCalloutLine(fn, mem, annotPtr, points) {
|
|
|
4588
4940
|
* annotation; the writer does not.
|
|
4589
4941
|
*/
|
|
4590
4942
|
function applyAnnotationBaseDraft(fn, mem, annotPtr, draft) {
|
|
4591
|
-
if (draft.contents !== void 0)
|
|
4592
|
-
if (draft.
|
|
4943
|
+
if (draft.contents !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "Contents", draft.contents);
|
|
4944
|
+
if (draft.subject !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "Subj", draft.subject);
|
|
4945
|
+
if (draft.nm !== void 0 && draft.nm.length > 0) writeAnnotString(fn, mem, annotPtr, "NM", draft.nm);
|
|
4593
4946
|
if (draft.flags !== void 0) setAnnotFlags(fn, annotPtr, draft.flags);
|
|
4594
4947
|
}
|
|
4595
4948
|
/**
|
|
4596
4949
|
* Write the annotation-wide base fields shared by every Patch
|
|
4597
|
-
* (contents). /T is bound at creation and not patchable — see
|
|
4950
|
+
* (contents/subject). /T is bound at creation and not patchable — see
|
|
4598
4951
|
* `AnnotationPatchBase`. /NM is monotonic per annotation; the mutator
|
|
4599
4952
|
* may stamp /NM opportunistically on a weak annotation via
|
|
4600
4953
|
* `writeAnnotationNm`.
|
|
@@ -4605,7 +4958,8 @@ function applyAnnotationBaseDraft(fn, mem, annotPtr, draft) {
|
|
|
4605
4958
|
* "..." -> set the entry to that value
|
|
4606
4959
|
*/
|
|
4607
4960
|
function applyAnnotationBasePatch(fn, mem, annotPtr, patch) {
|
|
4608
|
-
if (patch.contents !== void 0)
|
|
4961
|
+
if (patch.contents !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "Contents", patch.contents);
|
|
4962
|
+
if (patch.subject !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "Subj", patch.subject);
|
|
4609
4963
|
if (patch.flags !== void 0) setAnnotFlags(fn, annotPtr, patch.flags);
|
|
4610
4964
|
}
|
|
4611
4965
|
/**
|
|
@@ -4616,7 +4970,7 @@ function applyAnnotationBasePatch(fn, mem, annotPtr, patch) {
|
|
|
4616
4970
|
*/
|
|
4617
4971
|
function writeAnnotationAuthor(fn, mem, annotPtr, displayName) {
|
|
4618
4972
|
if (displayName.length === 0) return;
|
|
4619
|
-
|
|
4973
|
+
writeAnnotString(fn, mem, annotPtr, "T", displayName);
|
|
4620
4974
|
}
|
|
4621
4975
|
/**
|
|
4622
4976
|
* Stamp /NM on an annotation that didn't have one. Used by the mutator
|
|
@@ -4625,7 +4979,7 @@ function writeAnnotationAuthor(fn, mem, annotPtr, displayName) {
|
|
|
4625
4979
|
*/
|
|
4626
4980
|
function writeAnnotationNm(fn, mem, annotPtr, nm) {
|
|
4627
4981
|
if (nm.length === 0) return;
|
|
4628
|
-
|
|
4982
|
+
writeAnnotString(fn, mem, annotPtr, "NM", nm);
|
|
4629
4983
|
}
|
|
4630
4984
|
/**
|
|
4631
4985
|
* Stamp /M (modification date) on an annotation. /M is a standard
|
|
@@ -4642,22 +4996,7 @@ function writeAnnotationNm(fn, mem, annotPtr, nm) {
|
|
|
4642
4996
|
* Format: `D:YYYYMMDDHHmmSSOHH'mm'` (see formatPdfDate).
|
|
4643
4997
|
*/
|
|
4644
4998
|
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);
|
|
4999
|
+
writeAnnotString(fn, mem, annotPtr, "M", formatPdfDate(now));
|
|
4661
5000
|
}
|
|
4662
5001
|
//#endregion
|
|
4663
5002
|
//#region src/features/annotations/internal/identity/captureOrStampStableId.ts
|
|
@@ -4823,32 +5162,207 @@ function computeMutationImpact(inputs) {
|
|
|
4823
5162
|
* `AnnotationUpdateResult.updated`.
|
|
4824
5163
|
*/
|
|
4825
5164
|
function readAnnotationFromPtr(fn, mem, annotPtr, pageObjectNumber, index, revision, ctx) {
|
|
4826
|
-
const base = readAnnotationBase(fn, mem, annotPtr, pageObjectNumber, index, revision);
|
|
5165
|
+
const base = readAnnotationBase(fn, mem, ctx.docPtr, annotPtr, pageObjectNumber, index, revision);
|
|
4827
5166
|
const subtypeCode = fn.FPDFAnnot_GetSubtype(annotPtr);
|
|
4828
5167
|
const { reader } = pickReader(subtypeCode);
|
|
4829
5168
|
return reader(fn, mem, annotPtr, base, subtypeCode, ctx);
|
|
4830
5169
|
}
|
|
4831
5170
|
//#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;
|
|
5171
|
+
//#region src/features/annotations/internal/write/writeEmbedMetadata.ts
|
|
4841
5172
|
/**
|
|
4842
|
-
*
|
|
4843
|
-
*
|
|
4844
|
-
*
|
|
4845
|
-
*
|
|
4846
|
-
*
|
|
4847
|
-
*
|
|
5173
|
+
* Write the EmbedPDF-namespaced /EMBD_Metadata annotation dictionary.
|
|
5174
|
+
*
|
|
5175
|
+
* This module owns ONLY the vendor-extension dictionary. Standard PDF
|
|
5176
|
+
* base fields (/T, /M, /NM, /Contents) are handled by
|
|
5177
|
+
* `writers/annotations/base.ts` — /EMBD_Metadata is a separate
|
|
5178
|
+
* dictionary nested under the annotation that carries cloud-side
|
|
5179
|
+
* identity bookkeeping which has no PDF-spec equivalent.
|
|
5180
|
+
*
|
|
5181
|
+
* Schema (v1):
|
|
5182
|
+
* /EMBD_Metadata <<
|
|
5183
|
+
* /SchemaVersion 1 % EmbedPDF metadata schema version
|
|
5184
|
+
* /UserID (acme/alice) % identity that created the annotation
|
|
5185
|
+
* /GroupID (engineering) % group the annotation belongs to
|
|
5186
|
+
* /CreatedBy (acme/alice) % same as UserID on create
|
|
5187
|
+
* /UpdatedBy (acme/alice) % last editor; refreshed on every update
|
|
5188
|
+
* >>
|
|
5189
|
+
*
|
|
5190
|
+
* Read side: see `readers/annotations/embed-metadata.ts` (commit 12).
|
|
5191
|
+
*
|
|
5192
|
+
* PDFium bindings used: the `EPDFAnnot_*EmbedMetadata*` extension API
|
|
5193
|
+
* shipped in the EmbedPDF PDFium fork. Values are written as UTF-16LE
|
|
5194
|
+
* strings via the same `mem.writeU16String` pattern the base writer
|
|
5195
|
+
* uses for /T.
|
|
4848
5196
|
*/
|
|
4849
|
-
|
|
4850
|
-
|
|
5197
|
+
const KEY_SCHEMA_VERSION = "SchemaVersion";
|
|
5198
|
+
const KEY_USER_ID = "UserID";
|
|
5199
|
+
const KEY_GROUP_ID = "GroupID";
|
|
5200
|
+
const KEY_CREATED_BY = "CreatedBy";
|
|
5201
|
+
const KEY_UPDATED_BY = "UpdatedBy";
|
|
5202
|
+
/**
|
|
5203
|
+
* Stamp /EMBD_Metadata on a freshly created annotation. No-op if the
|
|
5204
|
+
* actor has no identity fields — the worker still emits the standard
|
|
5205
|
+
* /M timestamp via the base writer; only the vendor dictionary is
|
|
5206
|
+
* skipped.
|
|
5207
|
+
*
|
|
5208
|
+
* actor.userId → /UserID, /CreatedBy, /UpdatedBy
|
|
5209
|
+
* actor.groupId → /GroupID
|
|
5210
|
+
*
|
|
5211
|
+
* /SchemaVersion is written whenever any field is written.
|
|
5212
|
+
*/
|
|
5213
|
+
function applyEmbedMetadataOnCreate(fn, mem, annotPtr, actor) {
|
|
5214
|
+
if (!actor || !actor.userId && !actor.groupId) return;
|
|
5215
|
+
fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_SCHEMA_VERSION, 1);
|
|
5216
|
+
if (actor.userId) {
|
|
5217
|
+
setMetadataString(fn, mem, annotPtr, KEY_USER_ID, actor.userId);
|
|
5218
|
+
setMetadataString(fn, mem, annotPtr, KEY_CREATED_BY, actor.userId);
|
|
5219
|
+
setMetadataString(fn, mem, annotPtr, KEY_UPDATED_BY, actor.userId);
|
|
5220
|
+
}
|
|
5221
|
+
if (actor.groupId) setMetadataString(fn, mem, annotPtr, KEY_GROUP_ID, actor.groupId);
|
|
5222
|
+
}
|
|
5223
|
+
/**
|
|
5224
|
+
* Apply /EMBD_Metadata refreshes on update. Two fields can change on
|
|
5225
|
+
* update; everything else (UserID, CreatedBy) is immutable.
|
|
5226
|
+
*
|
|
5227
|
+
* actor.userId → /UpdatedBy (refreshed every update where the
|
|
5228
|
+
* caller has an identity)
|
|
5229
|
+
* actor.groupId → /GroupID (reassigned only when the patch
|
|
5230
|
+
* explicitly contains a new groupId; route runs
|
|
5231
|
+
* `checkSetGroup` first)
|
|
5232
|
+
*
|
|
5233
|
+
* If the target annotation has no EMBD_Metadata yet (created before
|
|
5234
|
+
* the cloud started stamping, or by a non-cloud writer), we seed
|
|
5235
|
+
* /SchemaVersion so subsequent readers can find the marker.
|
|
5236
|
+
*
|
|
5237
|
+
* No-op when the actor has neither userId nor groupId.
|
|
5238
|
+
*/
|
|
5239
|
+
function applyEmbedMetadataOnUpdate(fn, mem, annotPtr, actor) {
|
|
5240
|
+
if (!actor || !actor.userId && !actor.groupId) return;
|
|
5241
|
+
if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_SCHEMA_VERSION, 1);
|
|
5242
|
+
if (actor.userId) setMetadataString(fn, mem, annotPtr, KEY_UPDATED_BY, actor.userId);
|
|
5243
|
+
if (actor.groupId) setMetadataString(fn, mem, annotPtr, KEY_GROUP_ID, actor.groupId);
|
|
5244
|
+
}
|
|
5245
|
+
function setMetadataString(fn, mem, annotPtr, key, value) {
|
|
5246
|
+
const ptr = mem.writeU16String(value);
|
|
5247
|
+
try {
|
|
5248
|
+
fn.EPDFAnnot_SetEmbedMetadataString(annotPtr, key, ptr);
|
|
5249
|
+
} finally {
|
|
5250
|
+
mem.free(ptr);
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
//#endregion
|
|
5254
|
+
//#region src/features/annotations/internal/write/writeAnnotationTransformMetadata.ts
|
|
5255
|
+
/**
|
|
5256
|
+
* Write the EmbedPDF transform keys under /EMBD_Metadata. This is the seam
|
|
5257
|
+
* PDFium's native AP generator reads to bake a ROTATED appearance:
|
|
5258
|
+
*
|
|
5259
|
+
* /EMBD_Metadata <<
|
|
5260
|
+
* /Rotation 45 % degrees, PDF convention (CCW)
|
|
5261
|
+
* /UnrotatedRect [x0 y0 x1 y1] % BOX kinds only — the logical box
|
|
5262
|
+
* >>
|
|
5263
|
+
*
|
|
5264
|
+
* The family split (see the v3 plan):
|
|
5265
|
+
* - BOX kinds (square/circle/free-text): `/Rotation` + `/UnrotatedRect`.
|
|
5266
|
+
* With BOTH present the AP generator emits an `/AP /Matrix` that rotates a
|
|
5267
|
+
* box-sized appearance about the box centre, and `/Rect` is the enclosing
|
|
5268
|
+
* AABB. This is the portable, externally-correct rotation.
|
|
5269
|
+
* - VERTEX kinds (line/polyline/polygon/ink): `/Rotation` ONLY (advisory).
|
|
5270
|
+
* The points are already rotated (they are the visual), so a lone
|
|
5271
|
+
* `/Rotation` is INERT for the AP generator (it ignores `/Rotation` with no
|
|
5272
|
+
* `/UnrotatedRect`) — it just records the applied angle so EmbedPDF can
|
|
5273
|
+
* reconstruct an oriented selection box + offer reset.
|
|
5274
|
+
*
|
|
5275
|
+
* Both fields follow the engine-wide tri-state law ("a patch touches what it
|
|
5276
|
+
* states, preserves what it omits"):
|
|
5277
|
+
* - `undefined` → the key is UNTOUCHED (a rect-only move keeps its rotation).
|
|
5278
|
+
* - `null` or `0` → CLEAR just that key via `EPDFAnnot_ClearEmbedMetadataKey`
|
|
5279
|
+
* (0 ≡ no rotation is the canonical identity, not a sentinel; never clear
|
|
5280
|
+
* the whole dict — identity fields UserID/GroupID/CreatedBy/UpdatedBy must
|
|
5281
|
+
* survive).
|
|
5282
|
+
* - a value → SET the key. Setting a nonzero rotation with no
|
|
5283
|
+
* `/UnrotatedRect` (neither in the patch nor already on the annotation)
|
|
5284
|
+
* is an unsatisfiable state for the AP generator and throws `InvalidArg`.
|
|
5285
|
+
*
|
|
5286
|
+
* MUST run BEFORE `EPDFAnnot_GenerateAppearance` so the bake sees the rotation.
|
|
5287
|
+
* `/SchemaVersion` is seeded (stays 1) if this is the first key in the dict.
|
|
5288
|
+
*/
|
|
5289
|
+
const KEY_ROTATION = "Rotation";
|
|
5290
|
+
const KEY_UNROTATED_RECT = "UnrotatedRect";
|
|
5291
|
+
/** Seed `/SchemaVersion` when we are about to create the dict by writing the
|
|
5292
|
+
* first transform key, so the marker readers look for is always present. */
|
|
5293
|
+
function ensureSchemaVersion(fn, annotPtr) {
|
|
5294
|
+
if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, "SchemaVersion", 1);
|
|
5295
|
+
}
|
|
5296
|
+
function setRotation(fn, annotPtr, rotation) {
|
|
5297
|
+
ensureSchemaVersion(fn, annotPtr);
|
|
5298
|
+
fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_ROTATION, rotation);
|
|
5299
|
+
}
|
|
5300
|
+
function setUnrotatedRect(fn, mem, annotPtr, rect) {
|
|
5301
|
+
ensureSchemaVersion(fn, annotPtr);
|
|
5302
|
+
const buf = mem.alloc(16);
|
|
5303
|
+
try {
|
|
5304
|
+
mem.poke(buf, "f32", rect.left, 0);
|
|
5305
|
+
mem.poke(buf, "f32", rect.top, 4);
|
|
5306
|
+
mem.poke(buf, "f32", rect.right, 8);
|
|
5307
|
+
mem.poke(buf, "f32", rect.bottom, 12);
|
|
5308
|
+
fn.EPDFAnnot_SetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT, buf);
|
|
5309
|
+
} finally {
|
|
5310
|
+
mem.free(buf);
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
/**
|
|
5314
|
+
* Write transform metadata for a BOX kind (square/circle/free-text/stamp),
|
|
5315
|
+
* tri-state per field: `undefined` never touches the document (safe to call on
|
|
5316
|
+
* every patch), `null`/`0` clears, a value sets. A nonzero rotation with no
|
|
5317
|
+
* unrotated box anywhere (patch or annotation) is unsatisfiable — the AP
|
|
5318
|
+
* generator needs the pair — and throws instead of writing a broken state.
|
|
5319
|
+
*/
|
|
5320
|
+
function writeBoxTransformMetadata(fn, mem, annotPtr, t) {
|
|
5321
|
+
if (t.rotation !== void 0) if (t.rotation !== null && t.rotation !== 0) {
|
|
5322
|
+
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)");
|
|
5323
|
+
setRotation(fn, annotPtr, t.rotation);
|
|
5324
|
+
} else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
|
|
5325
|
+
if (t.unrotatedRect !== void 0) if (t.unrotatedRect !== null) setUnrotatedRect(fn, mem, annotPtr, t.unrotatedRect);
|
|
5326
|
+
else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
|
|
5327
|
+
}
|
|
5328
|
+
/**
|
|
5329
|
+
* Write the advisory `/Rotation` scalar for a VERTEX kind
|
|
5330
|
+
* (line/polyline/polygon/ink), tri-state: `undefined` never touches the
|
|
5331
|
+
* document, `null`/`0` clears, a value sets. Whenever it does write, any
|
|
5332
|
+
* `/UnrotatedRect` is defensively cleared — that key is an impossible state on
|
|
5333
|
+
* a vertex kind and must never accidentally drive the AP generator.
|
|
5334
|
+
*/
|
|
5335
|
+
function writeVertexTransformMetadata(fn, annotPtr, t) {
|
|
5336
|
+
if (t.rotation === void 0) return;
|
|
5337
|
+
if (t.rotation !== null && t.rotation !== 0) setRotation(fn, annotPtr, t.rotation);
|
|
5338
|
+
else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
|
|
5339
|
+
fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
|
|
5340
|
+
}
|
|
5341
|
+
//#endregion
|
|
5342
|
+
//#region src/features/annotations/internal/write/writeCaretAnnotation.ts
|
|
5343
|
+
/** Default `/C` colour when a caret draft omits it (engine-wide default mark). */
|
|
5344
|
+
const DEFAULT_CARET_COLOR = {
|
|
5345
|
+
r: 255,
|
|
5346
|
+
g: 0,
|
|
5347
|
+
b: 0
|
|
5348
|
+
};
|
|
5349
|
+
/** Default opacity, set explicitly so reads always round-trip the same value. */
|
|
5350
|
+
const DEFAULT_OPACITY$4 = 1;
|
|
5351
|
+
/**
|
|
5352
|
+
* Apply a caret draft to a freshly-created annotation. Caret carries no
|
|
5353
|
+
* geometry of its own beyond `/Rect`. Order:
|
|
5354
|
+
* 1. base author-metadata (contents/nm/flags)
|
|
5355
|
+
* 2. `/Rect` (required — supplied by the caller; never derived)
|
|
5356
|
+
* 3. `/C` color + `/CA` opacity
|
|
5357
|
+
* 4. `/RD` rectangle differences (optional)
|
|
5358
|
+
*/
|
|
5359
|
+
function applyCaretDraft(fn, mem, annotPtr, draft) {
|
|
5360
|
+
applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
|
|
4851
5361
|
setAnnotRect(fn, mem, annotPtr, draft.rect);
|
|
5362
|
+
writeBoxTransformMetadata(fn, mem, annotPtr, {
|
|
5363
|
+
rotation: draft.rotation,
|
|
5364
|
+
unrotatedRect: draft.unrotatedRect
|
|
5365
|
+
});
|
|
4852
5366
|
setAnnotColor(fn, annotPtr, draft.color ?? DEFAULT_CARET_COLOR);
|
|
4853
5367
|
setAnnotOpacity(fn, annotPtr, draft.opacity ?? DEFAULT_OPACITY$4);
|
|
4854
5368
|
if (draft.intent !== void 0) setIntent(fn, annotPtr, caretIntentToName(draft.intent));
|
|
@@ -4861,6 +5375,10 @@ function applyCaretDraft(fn, mem, annotPtr, draft) {
|
|
|
4861
5375
|
function applyCaretPatch(fn, mem, annotPtr, patch) {
|
|
4862
5376
|
applyAnnotationBasePatch(fn, mem, annotPtr, patch);
|
|
4863
5377
|
if (patch.rect !== void 0) setAnnotRect(fn, mem, annotPtr, patch.rect);
|
|
5378
|
+
if (patch.rotation !== void 0 || patch.unrotatedRect !== void 0) writeBoxTransformMetadata(fn, mem, annotPtr, {
|
|
5379
|
+
rotation: patch.rotation,
|
|
5380
|
+
unrotatedRect: patch.unrotatedRect
|
|
5381
|
+
});
|
|
4864
5382
|
if (patch.color !== void 0) setAnnotColor(fn, annotPtr, patch.color);
|
|
4865
5383
|
if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
|
|
4866
5384
|
if (patch.intent !== void 0) setIntent(fn, annotPtr, caretIntentToName(patch.intent));
|
|
@@ -4950,264 +5468,93 @@ function attachFile(fn, mem, annotPtr, file, resource, docPtr) {
|
|
|
4950
5468
|
* path; anything else is treated as a registered-font `key` and resolved to
|
|
4951
5469
|
* this thread's FontId. A key with no resolver wired (e.g. a host without a
|
|
4952
5470
|
* font registry) is a programming error — fail loud rather than silently
|
|
4953
|
-
* downgrade to Helvetica and embed the wrong glyphs.
|
|
4954
|
-
*
|
|
4955
|
-
* Shared by the free-text and redact writers: `/DA` styles a free-text body
|
|
4956
|
-
* and a redaction `/OverlayText` label through the exact same triple.
|
|
4957
|
-
*/
|
|
4958
|
-
function applyDefaultAppearance(fn, annotPtr, fontFamily, fontSize, color, ctx) {
|
|
4959
|
-
if (isStandardFont(fontFamily)) {
|
|
4960
|
-
setDefaultAppearance(fn, annotPtr, standardFontToCode(fontFamily), fontSize, color);
|
|
4961
|
-
return;
|
|
4962
|
-
}
|
|
4963
|
-
if (!ctx?.resolveRegisteredFontId) throw new EngineError(EngineErrorCode.InvalidArg, `fontFamily '${fontFamily}' is not a standard font and no font registry is available on this host`);
|
|
4964
|
-
setDefaultAppearanceRegisteredFont(fn, annotPtr, ctx.resolveRegisteredFontId(fontFamily), fontSize, color);
|
|
4965
|
-
}
|
|
4966
|
-
const DEFAULT_COLOR = {
|
|
4967
|
-
r: 255,
|
|
4968
|
-
g: 0,
|
|
4969
|
-
b: 0
|
|
4970
|
-
};
|
|
4971
|
-
/**
|
|
4972
|
-
* Apply the geometry styling (`/C`, `/CA`, `/BS`, dash) from a draft to a
|
|
4973
|
-
* freshly-created annotation. This is the no-fill layer shared by ink and
|
|
4974
|
-
* the filled families; `interiorColor` (`/IC`) is layered on by
|
|
4975
|
-
* {@link applyFilledStyleDraft}. Geometry (`/Rect`, `/Vertices`, `/L`,
|
|
4976
|
-
* `/InkList`), cloudy borders, rect-differences, and line endings are
|
|
4977
|
-
* layered on by the family-specific writer around this call.
|
|
4978
|
-
*/
|
|
4979
|
-
function applyGeometryStyleDraft(fn, mem, annotPtr, draft) {
|
|
4980
|
-
setAnnotColor(fn, annotPtr, draft.color ?? DEFAULT_COLOR);
|
|
4981
|
-
setAnnotOpacity(fn, annotPtr, draft.opacity ?? 1);
|
|
4982
|
-
applyBorderDraft(fn, mem, annotPtr, draft);
|
|
4983
|
-
}
|
|
4984
|
-
/**
|
|
4985
|
-
* Write the `/BS` border (style + width, then dash) from a draft. This is
|
|
4986
|
-
* the border-only slice of {@link applyGeometryStyleDraft}, shared with the
|
|
4987
|
-
* free-text box border (which manages `/C` and `/DA` itself). A missing
|
|
4988
|
-
* `borderStyle`/`strokeWidth` falls back to a 1pt solid border.
|
|
4989
|
-
*/
|
|
4990
|
-
function applyBorderDraft(fn, mem, annotPtr, draft) {
|
|
4991
|
-
setBorderStyle(fn, annotPtr, borderStyleToCode(draft.borderStyle ?? "solid"), draft.strokeWidth ?? 1);
|
|
4992
|
-
if (draft.dashArray !== void 0 && draft.dashArray.length > 0) setBorderDashPattern(fn, mem, annotPtr, draft.dashArray);
|
|
4993
|
-
}
|
|
4994
|
-
/**
|
|
4995
|
-
* Apply the geometry styling from a patch to an existing annotation. Only
|
|
4996
|
-
* present fields are touched. Border style + width share the single
|
|
4997
|
-
* `EPDFAnnot_SetBorderStyle` call, so when only one is patched we read the
|
|
4998
|
-
* current pair first and preserve the other.
|
|
4999
|
-
*/
|
|
5000
|
-
function applyGeometryStylePatch(fn, mem, annotPtr, patch) {
|
|
5001
|
-
if (patch.color !== void 0) setAnnotColor(fn, annotPtr, patch.color);
|
|
5002
|
-
if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
|
|
5003
|
-
applyBorderPatch(fn, mem, annotPtr, patch);
|
|
5004
|
-
}
|
|
5005
|
-
/**
|
|
5006
|
-
* Write the `/BS` border from a patch, touching only present fields. Border
|
|
5007
|
-
* style + width share the single `EPDFAnnot_SetBorderStyle` call, so when
|
|
5008
|
-
* only one is patched we read the current pair first and preserve the other.
|
|
5009
|
-
* The border-only slice of {@link applyGeometryStylePatch}, shared with the
|
|
5010
|
-
* free-text box border.
|
|
5011
|
-
*/
|
|
5012
|
-
function applyBorderPatch(fn, mem, annotPtr, patch) {
|
|
5013
|
-
if (patch.borderStyle !== void 0 || patch.strokeWidth !== void 0) {
|
|
5014
|
-
const current = readBorderStyle(fn, mem, annotPtr);
|
|
5015
|
-
const style = patch.borderStyle ?? borderStyleFromCode(current.styleCode);
|
|
5016
|
-
const width = patch.strokeWidth ?? current.width;
|
|
5017
|
-
setBorderStyle(fn, annotPtr, borderStyleToCode(style), width);
|
|
5018
|
-
}
|
|
5019
|
-
if (patch.dashArray !== void 0 && patch.dashArray.length > 0) setBorderDashPattern(fn, mem, annotPtr, patch.dashArray);
|
|
5020
|
-
}
|
|
5021
|
-
/**
|
|
5022
|
-
* Apply the full stroke/fill styling (geometry styling + `/IC`) from a
|
|
5023
|
-
* draft. A `null`/omitted fill clears `/IC` so the shape renders unfilled.
|
|
5024
|
-
* Used by the filled families (circle/square/polygon/polyline/line).
|
|
5025
|
-
*/
|
|
5026
|
-
function applyFilledStyleDraft(fn, mem, annotPtr, draft) {
|
|
5027
|
-
const interior = draft.interiorColor ?? null;
|
|
5028
|
-
if (interior === null) clearAnnotColor(fn, annotPtr, FPDFANNOT_COLORTYPE.InteriorColor);
|
|
5029
|
-
else setAnnotColor(fn, annotPtr, interior, FPDFANNOT_COLORTYPE.InteriorColor);
|
|
5030
|
-
applyGeometryStyleDraft(fn, mem, annotPtr, draft);
|
|
5031
|
-
}
|
|
5032
|
-
/**
|
|
5033
|
-
* Apply the full stroke/fill styling from a patch. Only present fields are
|
|
5034
|
-
* touched; `interiorColor: null` clears `/IC`.
|
|
5035
|
-
*/
|
|
5036
|
-
function applyFilledStylePatch(fn, mem, annotPtr, patch) {
|
|
5037
|
-
if (patch.interiorColor !== void 0) if (patch.interiorColor === null) clearAnnotColor(fn, annotPtr, FPDFANNOT_COLORTYPE.InteriorColor);
|
|
5038
|
-
else setAnnotColor(fn, annotPtr, patch.interiorColor, FPDFANNOT_COLORTYPE.InteriorColor);
|
|
5039
|
-
applyGeometryStylePatch(fn, mem, annotPtr, patch);
|
|
5040
|
-
}
|
|
5041
|
-
//#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`.
|
|
5471
|
+
* downgrade to Helvetica and embed the wrong glyphs.
|
|
5156
5472
|
*
|
|
5157
|
-
*
|
|
5158
|
-
*
|
|
5473
|
+
* Shared by the free-text and redact writers: `/DA` styles a free-text body
|
|
5474
|
+
* and a redaction `/OverlayText` label through the exact same triple.
|
|
5159
5475
|
*/
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
if (!
|
|
5476
|
+
function applyDefaultAppearance(fn, annotPtr, fontFamily, fontSize, color, ctx) {
|
|
5477
|
+
if (isStandardFont(fontFamily)) {
|
|
5478
|
+
setDefaultAppearance(fn, annotPtr, standardFontToCode(fontFamily), fontSize, color);
|
|
5479
|
+
return;
|
|
5480
|
+
}
|
|
5481
|
+
if (!ctx?.resolveRegisteredFontId) throw new EngineError(EngineErrorCode.InvalidArg, `fontFamily '${fontFamily}' is not a standard font and no font registry is available on this host`);
|
|
5482
|
+
setDefaultAppearanceRegisteredFont(fn, annotPtr, ctx.resolveRegisteredFontId(fontFamily), fontSize, color);
|
|
5166
5483
|
}
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5484
|
+
const DEFAULT_COLOR = {
|
|
5485
|
+
r: 255,
|
|
5486
|
+
g: 0,
|
|
5487
|
+
b: 0
|
|
5488
|
+
};
|
|
5489
|
+
/**
|
|
5490
|
+
* Apply the geometry styling (`/C`, `/CA`, `/BS`, dash) from a draft to a
|
|
5491
|
+
* freshly-created annotation. This is the no-fill layer shared by ink and
|
|
5492
|
+
* the filled families; `interiorColor` (`/IC`) is layered on by
|
|
5493
|
+
* {@link applyFilledStyleDraft}. Geometry (`/Rect`, `/Vertices`, `/L`,
|
|
5494
|
+
* `/InkList`), cloudy borders, rect-differences, and line endings are
|
|
5495
|
+
* layered on by the family-specific writer around this call.
|
|
5496
|
+
*/
|
|
5497
|
+
function applyGeometryStyleDraft(fn, mem, annotPtr, draft) {
|
|
5498
|
+
setAnnotColor(fn, annotPtr, draft.color ?? DEFAULT_COLOR);
|
|
5499
|
+
setAnnotOpacity(fn, annotPtr, draft.opacity ?? 1);
|
|
5500
|
+
applyBorderDraft(fn, mem, annotPtr, draft);
|
|
5170
5501
|
}
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5502
|
+
/**
|
|
5503
|
+
* Write the `/BS` border (style + width, then dash) from a draft. This is
|
|
5504
|
+
* the border-only slice of {@link applyGeometryStyleDraft}, shared with the
|
|
5505
|
+
* free-text box border (which manages `/C` and `/DA` itself). A missing
|
|
5506
|
+
* `borderStyle`/`strokeWidth` falls back to a 1pt solid border.
|
|
5507
|
+
*/
|
|
5508
|
+
function applyBorderDraft(fn, mem, annotPtr, draft) {
|
|
5509
|
+
setBorderStyle(fn, annotPtr, borderStyleToCode(draft.borderStyle ?? "solid"), draft.strokeWidth ?? 1);
|
|
5510
|
+
if (draft.dashArray !== void 0 && draft.dashArray.length > 0) setBorderDashPattern(fn, mem, annotPtr, draft.dashArray);
|
|
5511
|
+
}
|
|
5512
|
+
/**
|
|
5513
|
+
* Apply the geometry styling from a patch to an existing annotation. Only
|
|
5514
|
+
* present fields are touched. Border style + width share the single
|
|
5515
|
+
* `EPDFAnnot_SetBorderStyle` call, so when only one is patched we read the
|
|
5516
|
+
* current pair first and preserve the other.
|
|
5517
|
+
*/
|
|
5518
|
+
function applyGeometryStylePatch(fn, mem, annotPtr, patch) {
|
|
5519
|
+
if (patch.color !== void 0) setAnnotColor(fn, annotPtr, patch.color);
|
|
5520
|
+
if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
|
|
5521
|
+
applyBorderPatch(fn, mem, annotPtr, patch);
|
|
5522
|
+
}
|
|
5523
|
+
/**
|
|
5524
|
+
* Write the `/BS` border from a patch, touching only present fields. Border
|
|
5525
|
+
* style + width share the single `EPDFAnnot_SetBorderStyle` call, so when
|
|
5526
|
+
* only one is patched we read the current pair first and preserve the other.
|
|
5527
|
+
* The border-only slice of {@link applyGeometryStylePatch}, shared with the
|
|
5528
|
+
* free-text box border.
|
|
5529
|
+
*/
|
|
5530
|
+
function applyBorderPatch(fn, mem, annotPtr, patch) {
|
|
5531
|
+
if (patch.borderStyle !== void 0 || patch.strokeWidth !== void 0) {
|
|
5532
|
+
const current = readBorderStyle(fn, mem, annotPtr);
|
|
5533
|
+
const style = patch.borderStyle ?? borderStyleFromCode(current.styleCode);
|
|
5534
|
+
const width = patch.strokeWidth ?? current.width;
|
|
5535
|
+
setBorderStyle(fn, annotPtr, borderStyleToCode(style), width);
|
|
5182
5536
|
}
|
|
5537
|
+
if (patch.dashArray !== void 0 && patch.dashArray.length > 0) setBorderDashPattern(fn, mem, annotPtr, patch.dashArray);
|
|
5183
5538
|
}
|
|
5184
5539
|
/**
|
|
5185
|
-
*
|
|
5186
|
-
*
|
|
5187
|
-
*
|
|
5188
|
-
* unrotated box anywhere (patch or annotation) is unsatisfiable — the AP
|
|
5189
|
-
* generator needs the pair — and throws instead of writing a broken state.
|
|
5540
|
+
* Apply the full stroke/fill styling (geometry styling + `/IC`) from a
|
|
5541
|
+
* draft. A `null`/omitted fill clears `/IC` so the shape renders unfilled.
|
|
5542
|
+
* Used by the filled families (circle/square/polygon/polyline/line).
|
|
5190
5543
|
*/
|
|
5191
|
-
function
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
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);
|
|
5544
|
+
function applyFilledStyleDraft(fn, mem, annotPtr, draft) {
|
|
5545
|
+
const interior = draft.interiorColor ?? null;
|
|
5546
|
+
if (interior === null) clearAnnotColor(fn, annotPtr, FPDFANNOT_COLORTYPE.InteriorColor);
|
|
5547
|
+
else setAnnotColor(fn, annotPtr, interior, FPDFANNOT_COLORTYPE.InteriorColor);
|
|
5548
|
+
applyGeometryStyleDraft(fn, mem, annotPtr, draft);
|
|
5198
5549
|
}
|
|
5199
5550
|
/**
|
|
5200
|
-
*
|
|
5201
|
-
*
|
|
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.
|
|
5551
|
+
* Apply the full stroke/fill styling from a patch. Only present fields are
|
|
5552
|
+
* touched; `interiorColor: null` clears `/IC`.
|
|
5205
5553
|
*/
|
|
5206
|
-
function
|
|
5207
|
-
if (
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
|
|
5554
|
+
function applyFilledStylePatch(fn, mem, annotPtr, patch) {
|
|
5555
|
+
if (patch.interiorColor !== void 0) if (patch.interiorColor === null) clearAnnotColor(fn, annotPtr, FPDFANNOT_COLORTYPE.InteriorColor);
|
|
5556
|
+
else setAnnotColor(fn, annotPtr, patch.interiorColor, FPDFANNOT_COLORTYPE.InteriorColor);
|
|
5557
|
+
applyGeometryStylePatch(fn, mem, annotPtr, patch);
|
|
5211
5558
|
}
|
|
5212
5559
|
//#endregion
|
|
5213
5560
|
//#region src/features/annotations/internal/write/writeFreeTextAnnotation.ts
|
|
@@ -5979,13 +6326,18 @@ const DEFAULT_OPACITY = 1;
|
|
|
5979
6326
|
* Apply a text (sticky-note) draft. The visual is entirely generator-owned:
|
|
5980
6327
|
* the mutator's closing `regenerateAppearance` bakes the 20×20 note icon
|
|
5981
6328
|
* from `/C` + `/Name` (GenerateTextAP), so this writer only records state.
|
|
6329
|
+
* `/State` + `/StateModel` are dictionary-only (ISO 32000 §12.5.6.3) and
|
|
6330
|
+
* never reach the generator.
|
|
5982
6331
|
*/
|
|
5983
6332
|
function applyTextDraft(fn, mem, annotPtr, draft) {
|
|
6333
|
+
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
6334
|
applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
|
|
5985
6335
|
setAnnotRect(fn, mem, annotPtr, draft.rect);
|
|
5986
6336
|
setAnnotColor(fn, annotPtr, draft.color ?? DEFAULT_NOTE_COLOR);
|
|
5987
6337
|
setAnnotOpacity(fn, annotPtr, draft.opacity ?? DEFAULT_OPACITY);
|
|
5988
6338
|
setNoteIcon(fn, annotPtr, draft.icon ?? "note");
|
|
6339
|
+
if (draft.stateModel !== void 0) writeAnnotString(fn, mem, annotPtr, "StateModel", stateModelToPdf(draft.stateModel));
|
|
6340
|
+
if (draft.state !== void 0) writeAnnotString(fn, mem, annotPtr, "State", stateToPdf(draft.state));
|
|
5989
6341
|
}
|
|
5990
6342
|
function applyTextPatch(fn, mem, annotPtr, patch) {
|
|
5991
6343
|
applyAnnotationBasePatch(fn, mem, annotPtr, patch);
|
|
@@ -5993,6 +6345,8 @@ function applyTextPatch(fn, mem, annotPtr, patch) {
|
|
|
5993
6345
|
if (patch.color !== void 0) setAnnotColor(fn, annotPtr, patch.color);
|
|
5994
6346
|
if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
|
|
5995
6347
|
if (patch.icon !== void 0) setNoteIcon(fn, annotPtr, patch.icon);
|
|
6348
|
+
if (patch.stateModel !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "StateModel", patch.stateModel === null ? null : stateModelToPdf(patch.stateModel));
|
|
6349
|
+
if (patch.state !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "State", patch.state === null ? null : stateToPdf(patch.state));
|
|
5996
6350
|
}
|
|
5997
6351
|
function isTextSubtype(subtype) {
|
|
5998
6352
|
return subtype === "text";
|
|
@@ -6970,17 +7324,35 @@ var RedactionApplier = class {
|
|
|
6970
7324
|
}
|
|
6971
7325
|
applyAllOnPage(pagePtr) {
|
|
6972
7326
|
const { fn, mem } = this.runtime;
|
|
7327
|
+
const hasRedactions = this.pageHasRedactAnnotations(pagePtr);
|
|
6973
7328
|
return withScratch(mem, 4, (countPtr) => {
|
|
6974
|
-
if (!fn.EPDFPage_ApplyRedactions(pagePtr, countPtr))
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
7329
|
+
if (!fn.EPDFPage_ApplyRedactions(pagePtr, countPtr)) {
|
|
7330
|
+
if (hasRedactions) throw new EngineError(EngineErrorCode.Unknown, "native page redaction apply failed");
|
|
7331
|
+
return {
|
|
7332
|
+
status: "unchanged",
|
|
7333
|
+
removed: 0
|
|
7334
|
+
};
|
|
7335
|
+
}
|
|
6978
7336
|
return {
|
|
6979
7337
|
status: "applied",
|
|
6980
7338
|
removed: readI32(mem, countPtr) >>> 0
|
|
6981
7339
|
};
|
|
6982
7340
|
});
|
|
6983
7341
|
}
|
|
7342
|
+
pageHasRedactAnnotations(pagePtr) {
|
|
7343
|
+
const { fn } = this.runtime;
|
|
7344
|
+
const count = fn.FPDFPage_GetAnnotCount(pagePtr);
|
|
7345
|
+
for (let index = 0; index < count; index++) {
|
|
7346
|
+
const annotPtr = fn.FPDFPage_GetAnnot(pagePtr, index);
|
|
7347
|
+
if (!annotPtr) continue;
|
|
7348
|
+
try {
|
|
7349
|
+
if (subtypeFromCode(fn.FPDFAnnot_GetSubtype(annotPtr)) === "redact") return true;
|
|
7350
|
+
} finally {
|
|
7351
|
+
fn.FPDFPage_CloseAnnot(annotPtr);
|
|
7352
|
+
}
|
|
7353
|
+
}
|
|
7354
|
+
return false;
|
|
7355
|
+
}
|
|
6984
7356
|
applyRefsOnPage(pagePtr, refs) {
|
|
6985
7357
|
const { fn, mem } = this.runtime;
|
|
6986
7358
|
let removed = 0;
|
|
@@ -7006,23 +7378,26 @@ var RedactionApplier = class {
|
|
|
7006
7378
|
//#region src/features/text/PageTextReader.ts
|
|
7007
7379
|
/**
|
|
7008
7380
|
* Per-page slow-path text reader. Acquires a `pagePtr` from the
|
|
7009
|
-
* `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`),
|
|
7010
|
-
*
|
|
7011
|
-
*
|
|
7012
|
-
*
|
|
7013
|
-
*
|
|
7014
|
-
*
|
|
7015
|
-
*
|
|
7016
|
-
*
|
|
7017
|
-
*
|
|
7018
|
-
*
|
|
7019
|
-
*
|
|
7020
|
-
*
|
|
7021
|
-
*
|
|
7022
|
-
*
|
|
7023
|
-
*
|
|
7024
|
-
*
|
|
7025
|
-
*
|
|
7381
|
+
* `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`), extracts
|
|
7382
|
+
* the page text, and releases everything in reverse order.
|
|
7383
|
+
*
|
|
7384
|
+
* Index spaces (see engine-core `text/charmap.ts`): `charCount` is the
|
|
7385
|
+
* CHARACTER space (`FPDFText_CountChars` — the space geometry runs tile);
|
|
7386
|
+
* `text` is the TEXT projection. The two diverge when a character
|
|
7387
|
+
* contributes zero text units (non-printing) or two (supplementary plane);
|
|
7388
|
+
* `charMap` anchors encode exactly those deviations.
|
|
7389
|
+
*
|
|
7390
|
+
* Both come from the fork's batched extension calls, which share one
|
|
7391
|
+
* encoding authority (`FX_UTF16Encode`) so text and map can never
|
|
7392
|
+
* disagree:
|
|
7393
|
+
*
|
|
7394
|
+
* - `EPDFText_GetTextFull` — true UTF-16LE extraction; supplementary
|
|
7395
|
+
* characters arrive as surrogate pairs instead of being silently
|
|
7396
|
+
* dropped by `FPDFText_GetText`'s UCS-2 encoder. Called once with an
|
|
7397
|
+
* upper-bound buffer (2 units per character + NUL), so no measure
|
|
7398
|
+
* round-trip.
|
|
7399
|
+
* - `EPDFText_GetCharToTextMap` — the anchors, measure-then-fill. Zero
|
|
7400
|
+
* anchors is the common (identity) page and skips the fill call.
|
|
7026
7401
|
*/
|
|
7027
7402
|
var PageTextReader = class {
|
|
7028
7403
|
constructor(runtime, session) {
|
|
@@ -7031,7 +7406,7 @@ var PageTextReader = class {
|
|
|
7031
7406
|
}
|
|
7032
7407
|
read(pageObjectNumber, signal) {
|
|
7033
7408
|
throwIfAborted(signal);
|
|
7034
|
-
const { fn
|
|
7409
|
+
const { fn } = this.runtime;
|
|
7035
7410
|
const pool = this.session.pagePool();
|
|
7036
7411
|
const pagePtr = pool.acquire(pageObjectNumber);
|
|
7037
7412
|
try {
|
|
@@ -7040,10 +7415,18 @@ var PageTextReader = class {
|
|
|
7040
7415
|
if (!textPagePtr) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `FPDFText_LoadPage returned null for page object ${pageObjectNumber}`);
|
|
7041
7416
|
try {
|
|
7042
7417
|
throwIfAborted(signal);
|
|
7043
|
-
const charCount = fn.FPDFText_CountChars(textPagePtr);
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7418
|
+
const charCount = Math.max(fn.FPDFText_CountChars(textPagePtr), 0);
|
|
7419
|
+
const text = readTextFull(this.runtime, textPagePtr, charCount);
|
|
7420
|
+
const charMap = readCharMap(this.runtime, textPagePtr, pageObjectNumber);
|
|
7421
|
+
const violation = charMapViolation(charCount, text.length, charMap);
|
|
7422
|
+
if (violation !== null) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `char map invariant violated for page object ${pageObjectNumber}: ${violation}`);
|
|
7423
|
+
return charMap.length > 0 ? {
|
|
7424
|
+
text,
|
|
7425
|
+
charCount,
|
|
7426
|
+
charMap
|
|
7427
|
+
} : {
|
|
7428
|
+
text,
|
|
7429
|
+
charCount
|
|
7047
7430
|
};
|
|
7048
7431
|
} finally {
|
|
7049
7432
|
fn.FPDFText_ClosePage(textPagePtr);
|
|
@@ -7053,21 +7436,65 @@ var PageTextReader = class {
|
|
|
7053
7436
|
}
|
|
7054
7437
|
}
|
|
7055
7438
|
};
|
|
7056
|
-
|
|
7439
|
+
/**
|
|
7440
|
+
* Full-fidelity page text in one call: the buffer is sized at the maximum
|
|
7441
|
+
* possible expansion (every character a surrogate pair, plus the NUL), so
|
|
7442
|
+
* the returned requirement always fits and the measure round-trip is
|
|
7443
|
+
* skipped. The return value counts UTF-16 units INCLUDING the terminator.
|
|
7444
|
+
*/
|
|
7445
|
+
function readTextFull(runtime, textPagePtr, charCount) {
|
|
7057
7446
|
if (charCount <= 0) return "";
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7447
|
+
const { fn, mem } = runtime;
|
|
7448
|
+
const bufUnits = charCount * 2 + 1;
|
|
7449
|
+
return withScratch(mem, bufUnits * 2, (buf) => {
|
|
7450
|
+
const written = fn.EPDFText_GetTextFull(textPagePtr, buf, bufUnits);
|
|
7451
|
+
if (written <= 1) return "";
|
|
7452
|
+
return decodeUtf16Le(mem.readBytes(buf, (written - 1) * 2));
|
|
7453
|
+
});
|
|
7454
|
+
}
|
|
7455
|
+
function decodeUtf16Le(bytes) {
|
|
7456
|
+
const units = new Uint16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 1);
|
|
7457
|
+
let out = "";
|
|
7458
|
+
const CHUNK = 8192;
|
|
7459
|
+
for (let i = 0; i < units.length; i += CHUNK) out += String.fromCharCode(...units.subarray(i, Math.min(i + CHUNK, units.length)));
|
|
7460
|
+
return out;
|
|
7461
|
+
}
|
|
7462
|
+
/** Anchor struct layout: two little-endian i32s (see EPDF_CHAR_MAP_ANCHOR). */
|
|
7463
|
+
const ANCHOR_BYTES = 8;
|
|
7464
|
+
function readCharMap(runtime, textPagePtr, pageObjectNumber) {
|
|
7465
|
+
const { fn, mem } = runtime;
|
|
7466
|
+
const count = fn.EPDFText_GetCharToTextMap(textPagePtr, NULL_PTR, 0);
|
|
7467
|
+
if (count < 0) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `EPDFText_GetCharToTextMap failed for page object ${pageObjectNumber}`);
|
|
7468
|
+
if (count === 0) return [];
|
|
7469
|
+
return withScratch(mem, count * ANCHOR_BYTES, (buf) => {
|
|
7470
|
+
fn.EPDFText_GetCharToTextMap(textPagePtr, buf, count);
|
|
7471
|
+
const bytes = mem.readBytes(buf, count * ANCHOR_BYTES);
|
|
7472
|
+
const words = new Int32Array(bytes.buffer, bytes.byteOffset, count * 2);
|
|
7473
|
+
const anchors = new Array(count);
|
|
7474
|
+
for (let i = 0; i < count; i++) anchors[i] = [words[i * 2], words[i * 2 + 1]];
|
|
7475
|
+
return anchors;
|
|
7061
7476
|
});
|
|
7062
7477
|
}
|
|
7063
7478
|
//#endregion
|
|
7064
7479
|
//#region src/features/geometry/PageGeometryReader.ts
|
|
7065
7480
|
/**
|
|
7481
|
+
* Mid-object orientation drift that forces a run split (~0.5°). Real content
|
|
7482
|
+
* keeps one char matrix per text object; this guards the exotic cases so a
|
|
7483
|
+
* run's `rotation` is always representative of every glyph in it.
|
|
7484
|
+
*/
|
|
7485
|
+
const ROTATION_SPLIT_TOLERANCE = .0087;
|
|
7486
|
+
/**
|
|
7066
7487
|
* Geometry-only text layout reader.
|
|
7067
7488
|
*
|
|
7068
7489
|
* Emits geometry in PDF user space (y-up edges) — the canonical engine
|
|
7069
7490
|
* geometry. The viewer converts to content/view space via the page geometry
|
|
7070
7491
|
* matrix; this reader applies NO Y-flip or device transform.
|
|
7492
|
+
*
|
|
7493
|
+
* One `EPDFText_GetCharGeometry` call per glyph supplies boxes, oriented
|
|
7494
|
+
* cells, and flags together; `buildRunsFromRawGlyphs` then groups glyphs
|
|
7495
|
+
* into the upright/rotated run union. Upright runs are byte-identical to the
|
|
7496
|
+
* pre-orientation reader (quads the native layer offers are deliberately
|
|
7497
|
+
* dropped — the dominant case never pays for orientation).
|
|
7071
7498
|
*/
|
|
7072
7499
|
var PageGeometryReader = class {
|
|
7073
7500
|
constructor(runtime, session) {
|
|
@@ -7076,7 +7503,7 @@ var PageGeometryReader = class {
|
|
|
7076
7503
|
}
|
|
7077
7504
|
read(pageObjectNumber, signal) {
|
|
7078
7505
|
throwIfAborted(signal);
|
|
7079
|
-
const { fn } = this.runtime;
|
|
7506
|
+
const { fn, mem } = this.runtime;
|
|
7080
7507
|
const pool = this.session.pagePool();
|
|
7081
7508
|
const pagePtr = pool.acquire(pageObjectNumber);
|
|
7082
7509
|
try {
|
|
@@ -7086,12 +7513,27 @@ var PageGeometryReader = class {
|
|
|
7086
7513
|
try {
|
|
7087
7514
|
throwIfAborted(signal);
|
|
7088
7515
|
const glyphCount = Math.max(fn.FPDFText_CountChars(textPagePtr), 0);
|
|
7089
|
-
const
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7516
|
+
const records = [];
|
|
7517
|
+
const geometryPtr = mem.alloc(EPDF_CHAR_GEOMETRY_LAYOUT.bytes);
|
|
7518
|
+
try {
|
|
7519
|
+
let prevObjectKey = null;
|
|
7520
|
+
for (let i = 0; i < glyphCount; i++) {
|
|
7521
|
+
throwIfAborted(signal);
|
|
7522
|
+
const objectKey = fn.FPDFText_GetTextObject(textPagePtr, i);
|
|
7523
|
+
const record = {
|
|
7524
|
+
objectKey,
|
|
7525
|
+
...this.readGlyphRaw(textPagePtr, i, geometryPtr)
|
|
7526
|
+
};
|
|
7527
|
+
if (objectKey !== prevObjectKey) {
|
|
7528
|
+
record.fontSize = fn.FPDFText_GetFontSize(textPagePtr, i);
|
|
7529
|
+
prevObjectKey = objectKey;
|
|
7530
|
+
}
|
|
7531
|
+
records.push(record);
|
|
7532
|
+
}
|
|
7533
|
+
} finally {
|
|
7534
|
+
mem.free(geometryPtr);
|
|
7093
7535
|
}
|
|
7094
|
-
return { runs:
|
|
7536
|
+
return { runs: buildRunsFromRawGlyphs(records) };
|
|
7095
7537
|
} finally {
|
|
7096
7538
|
fn.FPDFText_ClosePage(textPagePtr);
|
|
7097
7539
|
}
|
|
@@ -7099,96 +7541,206 @@ var PageGeometryReader = class {
|
|
|
7099
7541
|
pool.release(pageObjectNumber);
|
|
7100
7542
|
}
|
|
7101
7543
|
}
|
|
7102
|
-
|
|
7103
|
-
const { fn } = this.runtime;
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7544
|
+
readGlyphRaw(textPagePtr, charIndex, geometryPtr) {
|
|
7545
|
+
const { fn, mem } = this.runtime;
|
|
7546
|
+
if (!fn.EPDFText_GetCharGeometry(textPagePtr, charIndex, geometryPtr)) return emptyRawGlyph();
|
|
7547
|
+
const { offsets, flags: bits } = EPDF_CHAR_GEOMETRY_LAYOUT;
|
|
7548
|
+
const native = readI32(mem, geometryPtr, offsets.flags) >>> 0;
|
|
7549
|
+
if (native & bits.empty) return emptyRawGlyph();
|
|
7550
|
+
const upright = Boolean(native & bits.upright);
|
|
7551
|
+
const raw = {
|
|
7552
|
+
flags: native & bits.space ? 1 : 0,
|
|
7553
|
+
looseBox: normalizePdfRect(readRectF(mem, geometryPtr, offsets.looseBox)),
|
|
7554
|
+
upright,
|
|
7555
|
+
rotation: 0,
|
|
7556
|
+
ascentFlip: false
|
|
7557
|
+
};
|
|
7558
|
+
if (native & bits.hasTightBox) raw.tightBox = normalizePdfRect(readRectF(mem, geometryPtr, offsets.tightBox));
|
|
7559
|
+
if (native & bits.hasLooseQuad) {
|
|
7560
|
+
raw.looseQuad = readQuad(mem, geometryPtr, offsets.looseQuad);
|
|
7561
|
+
if (native & bits.hasTightQuad) raw.tightQuad = readQuad(mem, geometryPtr, offsets.tightQuad);
|
|
7562
|
+
if (!upright) {
|
|
7563
|
+
const a = readF32(mem, geometryPtr, offsets.matrix);
|
|
7564
|
+
const b = readF32(mem, geometryPtr, offsets.matrix + 4);
|
|
7565
|
+
const c = readF32(mem, geometryPtr, offsets.matrix + 8);
|
|
7566
|
+
const d = readF32(mem, geometryPtr, offsets.matrix + 12);
|
|
7567
|
+
raw.rotation = Math.atan2(b, a);
|
|
7568
|
+
raw.ascentFlip = a * d - b * c < 0;
|
|
7121
7569
|
}
|
|
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
7570
|
}
|
|
7130
|
-
return
|
|
7571
|
+
return raw;
|
|
7131
7572
|
}
|
|
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
|
-
|
|
7573
|
+
};
|
|
7574
|
+
/**
|
|
7575
|
+
* Group per-glyph records into the run union. Pure — exported for tests.
|
|
7576
|
+
*
|
|
7577
|
+
* Runs split on text-object change (the legacy rule) and on orientation
|
|
7578
|
+
* change between classifiable glyphs (θ drift / mixed orientation). A run's
|
|
7579
|
+
* variant is decided by its FIRST classifiable glyph:
|
|
7580
|
+
* - empty glyphs never classify (they adopt the run's variant);
|
|
7581
|
+
* - real glyphs WITHOUT an oriented cell (singular matrix, synthesized
|
|
7582
|
+
* /ActualText pieces) classify as upright — box-only data degrades to
|
|
7583
|
+
* exactly the legacy behavior;
|
|
7584
|
+
* - real glyphs with a non-upright matrix classify as rotated.
|
|
7585
|
+
* Runs that never see a classifiable glyph emit as upright (degenerate,
|
|
7586
|
+
* legacy behavior).
|
|
7587
|
+
*/
|
|
7588
|
+
function buildRunsFromRawGlyphs(records) {
|
|
7589
|
+
const runs = [];
|
|
7590
|
+
let buffer = [];
|
|
7591
|
+
let charStart = 0;
|
|
7592
|
+
let fontSize;
|
|
7593
|
+
let objectKey = null;
|
|
7594
|
+
let cls;
|
|
7595
|
+
const flush = (nextStart) => {
|
|
7596
|
+
if (buffer.length > 0) {
|
|
7597
|
+
runs.push(materializeRun(buffer, charStart, fontSize, cls));
|
|
7598
|
+
buffer = [];
|
|
7599
|
+
cls = void 0;
|
|
7600
|
+
}
|
|
7601
|
+
charStart = nextStart;
|
|
7602
|
+
};
|
|
7603
|
+
for (let i = 0; i < records.length; i++) {
|
|
7604
|
+
const record = records[i];
|
|
7605
|
+
if (record.objectKey !== objectKey) {
|
|
7606
|
+
flush(i);
|
|
7607
|
+
objectKey = record.objectKey;
|
|
7608
|
+
fontSize = record.fontSize;
|
|
7609
|
+
}
|
|
7610
|
+
const recordClass = classOf(record);
|
|
7611
|
+
if (recordClass) {
|
|
7612
|
+
if (cls && !sameClass(cls, recordClass)) flush(i);
|
|
7613
|
+
if (!cls) cls = recordClass;
|
|
7173
7614
|
}
|
|
7615
|
+
buffer.push(record);
|
|
7174
7616
|
}
|
|
7617
|
+
flush(records.length);
|
|
7618
|
+
return runs;
|
|
7619
|
+
}
|
|
7620
|
+
function classOf(record) {
|
|
7621
|
+
if (record.flags & 2) return void 0;
|
|
7622
|
+
if (record.looseQuad && !record.upright) return {
|
|
7623
|
+
upright: false,
|
|
7624
|
+
rotation: record.rotation,
|
|
7625
|
+
ascentFlip: record.ascentFlip
|
|
7626
|
+
};
|
|
7627
|
+
return { upright: true };
|
|
7628
|
+
}
|
|
7629
|
+
function sameClass(a, b) {
|
|
7630
|
+
if (a.upright || b.upright) return a.upright === b.upright;
|
|
7631
|
+
if (a.ascentFlip !== b.ascentFlip) return false;
|
|
7632
|
+
const delta = a.rotation - b.rotation;
|
|
7633
|
+
return Math.abs(Math.atan2(Math.sin(delta), Math.cos(delta))) <= ROTATION_SPLIT_TOLERANCE;
|
|
7634
|
+
}
|
|
7635
|
+
function materializeRun(buffer, charStart, fontSize, cls) {
|
|
7636
|
+
if (cls && !cls.upright) {
|
|
7637
|
+
const glyphs = buffer.map((g) => g.looseQuad ? {
|
|
7638
|
+
looseQuad: g.looseQuad,
|
|
7639
|
+
flags: g.flags,
|
|
7640
|
+
...g.tightQuad ? { tightQuad: g.tightQuad } : {}
|
|
7641
|
+
} : {
|
|
7642
|
+
looseQuad: zeroQuad(),
|
|
7643
|
+
flags: g.flags
|
|
7644
|
+
});
|
|
7645
|
+
return {
|
|
7646
|
+
rect: runBounds(buffer, (g) => g.looseQuad ? pdfQuadBounds(g.looseQuad) : ZERO_RECT),
|
|
7647
|
+
charStart,
|
|
7648
|
+
glyphs,
|
|
7649
|
+
rotation: cls.rotation,
|
|
7650
|
+
ascentFlip: cls.ascentFlip,
|
|
7651
|
+
...fontSize !== void 0 ? { fontSize } : {}
|
|
7652
|
+
};
|
|
7653
|
+
}
|
|
7654
|
+
const glyphs = buffer.map((g) => ({
|
|
7655
|
+
looseBox: g.looseBox,
|
|
7656
|
+
flags: g.flags,
|
|
7657
|
+
...g.tightBox ? { tightBox: g.tightBox } : {}
|
|
7658
|
+
}));
|
|
7659
|
+
return {
|
|
7660
|
+
rect: runBounds(buffer, (g) => g.looseBox),
|
|
7661
|
+
charStart,
|
|
7662
|
+
glyphs,
|
|
7663
|
+
...fontSize !== void 0 ? { fontSize } : {}
|
|
7664
|
+
};
|
|
7665
|
+
}
|
|
7666
|
+
/** Legacy run-rect algorithm: seed from the first glyph, expand on non-empty. */
|
|
7667
|
+
function runBounds(buffer, boundsOf) {
|
|
7668
|
+
const seed = boundsOf(buffer[0]);
|
|
7669
|
+
const rect = {
|
|
7670
|
+
left: seed.left,
|
|
7671
|
+
bottom: seed.bottom,
|
|
7672
|
+
right: seed.right,
|
|
7673
|
+
top: seed.top
|
|
7674
|
+
};
|
|
7675
|
+
for (const g of buffer) {
|
|
7676
|
+
if (g.flags & 2) continue;
|
|
7677
|
+
const b = boundsOf(g);
|
|
7678
|
+
rect.left = Math.min(rect.left, b.left);
|
|
7679
|
+
rect.bottom = Math.min(rect.bottom, b.bottom);
|
|
7680
|
+
rect.right = Math.max(rect.right, b.right);
|
|
7681
|
+
rect.top = Math.max(rect.top, b.top);
|
|
7682
|
+
}
|
|
7683
|
+
return rect;
|
|
7684
|
+
}
|
|
7685
|
+
const ZERO_RECT = {
|
|
7686
|
+
left: 0,
|
|
7687
|
+
bottom: 0,
|
|
7688
|
+
right: 0,
|
|
7689
|
+
top: 0
|
|
7175
7690
|
};
|
|
7176
|
-
function
|
|
7691
|
+
function zeroQuad() {
|
|
7692
|
+
return {
|
|
7693
|
+
p1: {
|
|
7694
|
+
x: 0,
|
|
7695
|
+
y: 0
|
|
7696
|
+
},
|
|
7697
|
+
p2: {
|
|
7698
|
+
x: 0,
|
|
7699
|
+
y: 0
|
|
7700
|
+
},
|
|
7701
|
+
p3: {
|
|
7702
|
+
x: 0,
|
|
7703
|
+
y: 0
|
|
7704
|
+
},
|
|
7705
|
+
p4: {
|
|
7706
|
+
x: 0,
|
|
7707
|
+
y: 0
|
|
7708
|
+
}
|
|
7709
|
+
};
|
|
7710
|
+
}
|
|
7711
|
+
function emptyRawGlyph() {
|
|
7177
7712
|
return {
|
|
7713
|
+
flags: 2,
|
|
7178
7714
|
looseBox: {
|
|
7179
7715
|
left: 0,
|
|
7180
7716
|
bottom: 0,
|
|
7181
7717
|
right: 0,
|
|
7182
7718
|
top: 0
|
|
7183
7719
|
},
|
|
7184
|
-
|
|
7720
|
+
upright: true,
|
|
7721
|
+
rotation: 0,
|
|
7722
|
+
ascentFlip: false
|
|
7185
7723
|
};
|
|
7186
7724
|
}
|
|
7187
|
-
function
|
|
7725
|
+
function readQuad(mem, ptr, byteOffset) {
|
|
7726
|
+
const f = (offset) => readF32(mem, ptr, byteOffset + offset);
|
|
7188
7727
|
return {
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7728
|
+
p1: {
|
|
7729
|
+
x: f(0),
|
|
7730
|
+
y: f(4)
|
|
7731
|
+
},
|
|
7732
|
+
p2: {
|
|
7733
|
+
x: f(8),
|
|
7734
|
+
y: f(12)
|
|
7735
|
+
},
|
|
7736
|
+
p3: {
|
|
7737
|
+
x: f(16),
|
|
7738
|
+
y: f(20)
|
|
7739
|
+
},
|
|
7740
|
+
p4: {
|
|
7741
|
+
x: f(24),
|
|
7742
|
+
y: f(28)
|
|
7743
|
+
}
|
|
7192
7744
|
};
|
|
7193
7745
|
}
|
|
7194
7746
|
//#endregion
|
|
@@ -7828,13 +8380,13 @@ var FormReader = class {
|
|
|
7828
8380
|
snapshot(signal) {
|
|
7829
8381
|
throwIfAborted(signal);
|
|
7830
8382
|
const model = acquireFormModel(this.runtime, this.session);
|
|
7831
|
-
return readFormSnapshot(this.runtime, model);
|
|
8383
|
+
return readFormSnapshot(this.runtime, model, this.session.requireDocPtr());
|
|
7832
8384
|
}
|
|
7833
8385
|
field(ref, signal) {
|
|
7834
8386
|
throwIfAborted(signal);
|
|
7835
8387
|
const model = acquireFormModel(this.runtime, this.session);
|
|
7836
8388
|
const { fieldIndex } = resolveFieldRef(this.runtime, model, ref);
|
|
7837
|
-
return readFieldAt(this.runtime, model, fieldIndex);
|
|
8389
|
+
return readFieldAt(this.runtime, model, fieldIndex, this.session.requireDocPtr());
|
|
7838
8390
|
}
|
|
7839
8391
|
/**
|
|
7840
8392
|
* Serialize form data. Exports read the reconciled view (recovered
|
|
@@ -7990,7 +8542,7 @@ var FormMutator = class {
|
|
|
7990
8542
|
const model = acquireFormModel(this.runtime, this.session);
|
|
7991
8543
|
const resolved = resolveFieldRef(this.runtime, model, ref);
|
|
7992
8544
|
this.assertWritable(resolved.fieldObjectNumber);
|
|
7993
|
-
const before = readFieldAt(this.runtime, model, resolved.fieldIndex);
|
|
8545
|
+
const before = readFieldAt(this.runtime, model, resolved.fieldIndex, this.session.requireDocPtr());
|
|
7994
8546
|
if (!FAMILY_BY_VALUE_TYPE[value.type].includes(before.family)) throw new EngineError(EngineErrorCode.InvalidArg, `value type '${value.type}' does not apply to a '${before.family}' field`);
|
|
7995
8547
|
const changed = this.applyWrite(resolved.fieldObjectNumber, value);
|
|
7996
8548
|
return this.readBack(resolved.fieldObjectNumber, changed);
|
|
@@ -8026,7 +8578,7 @@ var FormMutator = class {
|
|
|
8026
8578
|
const fresh = acquireFormModel(this.runtime, this.session);
|
|
8027
8579
|
return {
|
|
8028
8580
|
...counters,
|
|
8029
|
-
snapshot: readFormSnapshot(this.runtime, fresh),
|
|
8581
|
+
snapshot: readFormSnapshot(this.runtime, fresh, this.session.requireDocPtr()),
|
|
8030
8582
|
meta: EMPTY_META$1
|
|
8031
8583
|
};
|
|
8032
8584
|
}
|
|
@@ -8099,7 +8651,7 @@ var FormMutator = class {
|
|
|
8099
8651
|
const model = acquireFormModel(this.runtime, this.session);
|
|
8100
8652
|
const resolved = resolveFieldRef(this.runtime, model, ref);
|
|
8101
8653
|
this.assertWritable(resolved.fieldObjectNumber);
|
|
8102
|
-
const before = readFieldAt(this.runtime, model, resolved.fieldIndex);
|
|
8654
|
+
const before = readFieldAt(this.runtime, model, resolved.fieldIndex, this.session.requireDocPtr());
|
|
8103
8655
|
if (before.family !== patch.family) throw new EngineError(EngineErrorCode.InvalidArg, `patch family '${patch.family}' does not match field family '${before.family}'`);
|
|
8104
8656
|
const fieldObjectNumber = resolved.fieldObjectNumber;
|
|
8105
8657
|
if (patch.name !== void 0) this.applyWideSetter(fn.EPDFForm_SetFieldName, fieldObjectNumber, patch.name, `cannot rename to "${patch.name}" (sibling conflict or invalid)`);
|
|
@@ -8130,7 +8682,7 @@ var FormMutator = class {
|
|
|
8130
8682
|
const model = acquireFormModel(this.runtime, this.session);
|
|
8131
8683
|
const resolved = resolveFieldRef(this.runtime, model, ref);
|
|
8132
8684
|
this.assertWritable(resolved.fieldObjectNumber);
|
|
8133
|
-
const before = readFieldAt(this.runtime, model, resolved.fieldIndex);
|
|
8685
|
+
const before = readFieldAt(this.runtime, model, resolved.fieldIndex, this.session.requireDocPtr());
|
|
8134
8686
|
if (!withScratchN(mem, [256 * 4, 4], ([buf, countPtr]) => {
|
|
8135
8687
|
mem.poke(countPtr, "i32", 0);
|
|
8136
8688
|
return fn.EPDFForm_DeleteField(this.session.requireDocPtr(), resolved.fieldObjectNumber, buf, 256, countPtr);
|
|
@@ -8150,7 +8702,7 @@ var FormMutator = class {
|
|
|
8150
8702
|
const model = acquireFormModel(this.runtime, this.session);
|
|
8151
8703
|
const resolved = resolveFieldRef(this.runtime, model, ref);
|
|
8152
8704
|
this.assertWritable(resolved.fieldObjectNumber);
|
|
8153
|
-
const before = readFieldAt(this.runtime, model, resolved.fieldIndex);
|
|
8705
|
+
const before = readFieldAt(this.runtime, model, resolved.fieldIndex, this.session.requireDocPtr());
|
|
8154
8706
|
const toggle = before.family === "checkbox" || before.family === "radio";
|
|
8155
8707
|
const state = toggle ? onState ?? (before.family === "checkbox" ? "Yes" : "") : "";
|
|
8156
8708
|
if (toggle && (!state || state === "Off")) throw new EngineError(EngineErrorCode.InvalidArg, "attaching to a radio group needs a non-\"Off\" onState");
|
|
@@ -8196,7 +8748,7 @@ var FormMutator = class {
|
|
|
8196
8748
|
const fresh = acquireFormModel(this.runtime, this.session);
|
|
8197
8749
|
const fieldIndex = this.runtime.fn.EPDFForm_GetFieldIndexByObjNum(fresh, fieldObjectNumber);
|
|
8198
8750
|
if (fieldIndex < 0) throw new EngineError(EngineErrorCode.Unknown, "form field vanished after write");
|
|
8199
|
-
return readFieldAt(this.runtime, fresh, fieldIndex);
|
|
8751
|
+
return readFieldAt(this.runtime, fresh, fieldIndex, this.session.requireDocPtr());
|
|
8200
8752
|
}
|
|
8201
8753
|
assertWritable(fieldObjectNumber) {
|
|
8202
8754
|
if (fieldObjectNumber === 0) throw new EngineError(EngineErrorCode.InvalidArg, "form field is stored as a direct object and cannot be written");
|
|
@@ -8242,7 +8794,7 @@ var FormMutator = class {
|
|
|
8242
8794
|
const fresh = acquireFormModel(this.runtime, this.session);
|
|
8243
8795
|
const fieldIndex = this.runtime.fn.EPDFForm_GetFieldIndexByObjNum(fresh, fieldObjectNumber);
|
|
8244
8796
|
if (fieldIndex < 0) throw new EngineError(EngineErrorCode.Unknown, "form field vanished after write");
|
|
8245
|
-
const field = readFieldAt(this.runtime, fresh, fieldIndex);
|
|
8797
|
+
const field = readFieldAt(this.runtime, fresh, fieldIndex, this.session.requireDocPtr());
|
|
8246
8798
|
const changedSet = new Set(changedObjNums);
|
|
8247
8799
|
return {
|
|
8248
8800
|
field,
|
|
@@ -8388,7 +8940,7 @@ var FormsEffectsApplier = class {
|
|
|
8388
8940
|
if (refs.length === 0) throw new EngineError(EngineErrorCode.InvalidArg, "reset effect requires at least one field");
|
|
8389
8941
|
const model = acquireFormModel(this.runtime, this.session);
|
|
8390
8942
|
const resolved = refs.map((ref) => this.resolve(model, ref));
|
|
8391
|
-
const fields = resolved.map(({ fieldIndex }) => readFieldAt(this.runtime, model, fieldIndex, actionBudget));
|
|
8943
|
+
const fields = resolved.map(({ fieldIndex }) => readFieldAt(this.runtime, model, fieldIndex, this.session.requireDocPtr(), actionBudget));
|
|
8392
8944
|
validateEffect(effect, fields);
|
|
8393
8945
|
return {
|
|
8394
8946
|
effect,
|
|
@@ -8413,7 +8965,7 @@ var FormsEffectsApplier = class {
|
|
|
8413
8965
|
const model = acquireFormModel(this.runtime, this.session);
|
|
8414
8966
|
const index = this.runtime.fn.EPDFForm_GetFieldIndexByObjNum(model, fieldObjectNumber);
|
|
8415
8967
|
if (index < 0) throw new EngineError(EngineErrorCode.InvalidReference, "form field disappeared");
|
|
8416
|
-
return readFieldAt(this.runtime, model, index, actionBudget);
|
|
8968
|
+
return readFieldAt(this.runtime, model, index, this.session.requireDocPtr(), actionBudget);
|
|
8417
8969
|
}
|
|
8418
8970
|
readFieldsBestEffort(fieldObjectNumbers, actionBudget) {
|
|
8419
8971
|
const fields = [];
|
|
@@ -8561,7 +9113,7 @@ function emptyResult(index, status) {
|
|
|
8561
9113
|
//#endregion
|
|
8562
9114
|
//#region src/features/search/internal/pageCorpusCache.ts
|
|
8563
9115
|
/**
|
|
8564
|
-
* Per-session, per-page search corpus: the page's
|
|
9116
|
+
* Per-session, per-page search corpus: the page's text snapshot plus its
|
|
8565
9117
|
* DEFAULT fold (the one literal queries with default options search).
|
|
8566
9118
|
* This is the local engine's in-memory equivalent of the server's corpus
|
|
8567
9119
|
* artifacts — same fold version, same shape, built lazily on first search
|
|
@@ -8586,19 +9138,20 @@ function acquirePageCorpus(runtime, session, pageObjectNumber, signal) {
|
|
|
8586
9138
|
cache.set(session, pages);
|
|
8587
9139
|
}
|
|
8588
9140
|
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, {
|
|
9141
|
+
if (hit && hit.seq === seq) return hit;
|
|
9142
|
+
const snapshot = new PageTextReader(runtime, session).read(pageObjectNumber, signal);
|
|
9143
|
+
const entry = {
|
|
8594
9144
|
seq,
|
|
8595
|
-
folded
|
|
8596
|
-
|
|
9145
|
+
folded: foldText(snapshot.text),
|
|
9146
|
+
snapshot
|
|
9147
|
+
};
|
|
9148
|
+
pages.delete(pageObjectNumber);
|
|
9149
|
+
pages.set(pageObjectNumber, entry);
|
|
8597
9150
|
if (pages.size > MAX_CACHED_PAGES) {
|
|
8598
9151
|
const oldest = pages.keys().next().value;
|
|
8599
9152
|
if (oldest !== void 0) pages.delete(oldest);
|
|
8600
9153
|
}
|
|
8601
|
-
return
|
|
9154
|
+
return entry;
|
|
8602
9155
|
}
|
|
8603
9156
|
//#endregion
|
|
8604
9157
|
//#region src/features/search/internal/searchCursor.ts
|
|
@@ -8707,19 +9260,23 @@ var SearchReader = class {
|
|
|
8707
9260
|
throwIfAborted(signal);
|
|
8708
9261
|
const pon = order[scanned].pageObjectNumber;
|
|
8709
9262
|
const corpus = acquirePageCorpus(this.runtime, this.session, pon, signal);
|
|
9263
|
+
const text = corpus.snapshot.text;
|
|
8710
9264
|
let ranges;
|
|
8711
|
-
if (query.regex) ranges = matchRegex(
|
|
8712
|
-
else if (query.matchCase || query.matchDiacritics) ranges = matchLiteral(foldText(
|
|
8713
|
-
else ranges = matchLiteral(corpus, query);
|
|
9265
|
+
if (query.regex) ranges = matchRegex(text, query);
|
|
9266
|
+
else if (query.matchCase || query.matchDiacritics) ranges = matchLiteral(foldText(text, foldOptionsFor(query)), query);
|
|
9267
|
+
else ranges = matchLiteral(corpus.folded, query);
|
|
8714
9268
|
if (ranges.length > 0) {
|
|
8715
|
-
const
|
|
8716
|
-
for (const range of ranges)
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
9269
|
+
const layout = buildPageTextLayout(new PageGeometryReader(this.runtime, this.session).read(pon, signal));
|
|
9270
|
+
for (const range of ranges) {
|
|
9271
|
+
const chars = charRangeForTextOffsets(corpus.snapshot, range.start, range.start + range.length);
|
|
9272
|
+
matches.push({
|
|
9273
|
+
pageObjectNumber: pon,
|
|
9274
|
+
charStart: chars.start,
|
|
9275
|
+
charCount: chars.end - chars.start,
|
|
9276
|
+
segments: textSegmentsForRange(layout, chars.start, chars.end - chars.start),
|
|
9277
|
+
...mode === "full" ? { snippet: buildSnippet(text, range) } : {}
|
|
9278
|
+
});
|
|
9279
|
+
}
|
|
8723
9280
|
}
|
|
8724
9281
|
scanned++;
|
|
8725
9282
|
pagesThisSlice++;
|
|
@@ -8775,22 +9332,26 @@ var DocumentActionsReader = class {
|
|
|
8775
9332
|
} finally {
|
|
8776
9333
|
fn.FPDFDoc_CloseJavaScriptAction(namedPtr);
|
|
8777
9334
|
}
|
|
8778
|
-
|
|
9335
|
+
budget.reservePayloadBytes(name.length);
|
|
9336
|
+
const action = requireAction(readActionModel(fn, mem, docPtr, fn.EPDFDoc_GetNamedJavaScriptActionModel(docPtr, index), budget), "name-tree script", index);
|
|
8779
9337
|
nameTreeScripts.push({
|
|
8780
9338
|
name,
|
|
8781
9339
|
action
|
|
8782
9340
|
});
|
|
8783
9341
|
}
|
|
8784
|
-
const openAction = readActionModel(fn, mem, fn.EPDFDoc_GetOpenActionModel(docPtr), budget);
|
|
9342
|
+
const openAction = readActionModel(fn, mem, docPtr, fn.EPDFDoc_GetOpenActionModel(docPtr), budget);
|
|
9343
|
+
const openDestPtr = openAction ? NULL_PTR : fn.EPDFDoc_GetOpenActionDest(docPtr);
|
|
9344
|
+
const openDestination = openDestPtr !== NULL_PTR ? readDestination(fn, mem, docPtr, openDestPtr) : null;
|
|
8785
9345
|
const additional = {};
|
|
8786
9346
|
for (const [key, event] of DOCUMENT_ACTION_EVENTS) {
|
|
8787
9347
|
throwIfAborted(signal);
|
|
8788
|
-
const action = readActionModel(fn, mem, fn.EPDFDoc_GetAdditionalActionModel(docPtr, event), budget);
|
|
9348
|
+
const action = readActionModel(fn, mem, docPtr, fn.EPDFDoc_GetAdditionalActionModel(docPtr, event), budget);
|
|
8789
9349
|
if (action) additional[key] = action;
|
|
8790
9350
|
}
|
|
8791
9351
|
return {
|
|
8792
9352
|
nameTreeScripts,
|
|
8793
9353
|
openAction,
|
|
9354
|
+
openDestination,
|
|
8794
9355
|
...additional
|
|
8795
9356
|
};
|
|
8796
9357
|
}
|
|
@@ -9000,9 +9561,10 @@ function validatePatchValue(key, value) {
|
|
|
9000
9561
|
* worker wire shape from @embedpdf/engine-core.
|
|
9001
9562
|
*/
|
|
9002
9563
|
var WorkerHost = class {
|
|
9003
|
-
constructor(runtime, post) {
|
|
9564
|
+
constructor(runtime, post, options = {}) {
|
|
9004
9565
|
this.runtime = runtime;
|
|
9005
9566
|
this.post = post;
|
|
9567
|
+
this.options = options;
|
|
9006
9568
|
_defineProperty(this, "sessions", /* @__PURE__ */ new Map());
|
|
9007
9569
|
_defineProperty(this, "baseDocuments", void 0);
|
|
9008
9570
|
_defineProperty(this, "aborts", /* @__PURE__ */ new Map());
|
|
@@ -9028,6 +9590,10 @@ var WorkerHost = class {
|
|
|
9028
9590
|
this.aborts.get(msg.jobId)?.abort();
|
|
9029
9591
|
return;
|
|
9030
9592
|
}
|
|
9593
|
+
if (msg.kind === "pages.renderEncoded" || msg.kind === "document.renderPageFileEncoded" || msg.kind === "annotations.renderAppearancesEncoded") {
|
|
9594
|
+
this.receiveEncoded(msg);
|
|
9595
|
+
return;
|
|
9596
|
+
}
|
|
9031
9597
|
const ctrl = new AbortController();
|
|
9032
9598
|
this.aborts.set(msg.jobId, ctrl);
|
|
9033
9599
|
let resultPack;
|
|
@@ -9131,6 +9697,9 @@ var WorkerHost = class {
|
|
|
9131
9697
|
case "pages.insert":
|
|
9132
9698
|
resultPack = this.handlePagesInsert(msg, ctrl.signal);
|
|
9133
9699
|
break;
|
|
9700
|
+
case "pages.insertBlank":
|
|
9701
|
+
resultPack = this.handlePagesInsertBlank(msg, ctrl.signal);
|
|
9702
|
+
break;
|
|
9134
9703
|
case "attachments.list":
|
|
9135
9704
|
resultPack = this.handleAttachmentsList(msg, ctrl.signal);
|
|
9136
9705
|
break;
|
|
@@ -9423,6 +9992,17 @@ var WorkerHost = class {
|
|
|
9423
9992
|
result
|
|
9424
9993
|
}, req.artifactPath);
|
|
9425
9994
|
}
|
|
9995
|
+
handlePagesInsertBlank(req, signal) {
|
|
9996
|
+
const session = this.requireSession(req);
|
|
9997
|
+
const result = new PagesInserter(this.runtime, session).insertBlank({
|
|
9998
|
+
size: req.size,
|
|
9999
|
+
count: req.count
|
|
10000
|
+
}, req.destIndex, signal);
|
|
10001
|
+
return this.finishMutation(session, {
|
|
10002
|
+
tag: "pages.insertBlank",
|
|
10003
|
+
result
|
|
10004
|
+
}, req.artifactPath);
|
|
10005
|
+
}
|
|
9426
10006
|
handleAttachmentsList(req, signal) {
|
|
9427
10007
|
const session = this.requireSession(req);
|
|
9428
10008
|
return wirePack({
|
|
@@ -9515,6 +10095,105 @@ var WorkerHost = class {
|
|
|
9515
10095
|
raster
|
|
9516
10096
|
}, [raster.data]);
|
|
9517
10097
|
}
|
|
10098
|
+
async receiveEncoded(msg) {
|
|
10099
|
+
if (!this.options.imageEncoder) {
|
|
10100
|
+
this.post(wirePack({
|
|
10101
|
+
kind: "reject",
|
|
10102
|
+
jobId: msg.jobId,
|
|
10103
|
+
error: serializeError(new EngineError(EngineErrorCode.NotImplemented, "this engine has no image encoder (the *.renderEncoded kinds are cloud-server surface)"))
|
|
10104
|
+
}, EMPTY_TRANSFER));
|
|
10105
|
+
return;
|
|
10106
|
+
}
|
|
10107
|
+
const ctrl = new AbortController();
|
|
10108
|
+
this.aborts.set(msg.jobId, ctrl);
|
|
10109
|
+
try {
|
|
10110
|
+
let resultPack;
|
|
10111
|
+
switch (msg.kind) {
|
|
10112
|
+
case "pages.renderEncoded":
|
|
10113
|
+
resultPack = await this.handlePagesRenderEncoded(msg, ctrl.signal);
|
|
10114
|
+
break;
|
|
10115
|
+
case "document.renderPageFileEncoded":
|
|
10116
|
+
resultPack = await this.handleDocumentRenderPageFileEncoded(msg, ctrl.signal);
|
|
10117
|
+
break;
|
|
10118
|
+
case "annotations.renderAppearancesEncoded":
|
|
10119
|
+
resultPack = await this.handleAnnotationsRenderAppearancesEncoded(msg, ctrl.signal);
|
|
10120
|
+
break;
|
|
10121
|
+
}
|
|
10122
|
+
this.post(wirePack({
|
|
10123
|
+
kind: "resolve",
|
|
10124
|
+
jobId: msg.jobId,
|
|
10125
|
+
result: resultPack.payload
|
|
10126
|
+
}, resultPack.transfer));
|
|
10127
|
+
} catch (err) {
|
|
10128
|
+
this.post(wirePack({
|
|
10129
|
+
kind: "reject",
|
|
10130
|
+
jobId: msg.jobId,
|
|
10131
|
+
error: serializeError(err)
|
|
10132
|
+
}, EMPTY_TRANSFER));
|
|
10133
|
+
} finally {
|
|
10134
|
+
this.aborts.delete(msg.jobId);
|
|
10135
|
+
}
|
|
10136
|
+
}
|
|
10137
|
+
async encodeRaster(raster, encode, signal) {
|
|
10138
|
+
const encoder = this.options.imageEncoder;
|
|
10139
|
+
if (!encoder) throw new EngineError(EngineErrorCode.NotImplemented, "this engine has no image encoder (the *.renderEncoded kinds are cloud-server surface)");
|
|
10140
|
+
const { bytes, contentType } = await encoder.encode(raster, encode);
|
|
10141
|
+
if (signal.aborted) throw new EngineError(EngineErrorCode.Aborted, "aborted during image encode");
|
|
10142
|
+
return {
|
|
10143
|
+
contentType,
|
|
10144
|
+
width: raster.width,
|
|
10145
|
+
height: raster.height,
|
|
10146
|
+
bytes
|
|
10147
|
+
};
|
|
10148
|
+
}
|
|
10149
|
+
async handlePagesRenderEncoded(req, signal) {
|
|
10150
|
+
const inner = this.handlePagesRender({
|
|
10151
|
+
...req,
|
|
10152
|
+
kind: "pages.render"
|
|
10153
|
+
}, signal);
|
|
10154
|
+
if (inner.payload.tag !== "pages.render") throw new EngineError(EngineErrorCode.WireFormat, `unexpected ${inner.payload.tag}`);
|
|
10155
|
+
const image = await this.encodeRaster(inner.payload.raster, req.encode, signal);
|
|
10156
|
+
return wirePack({
|
|
10157
|
+
tag: "pages.renderEncoded",
|
|
10158
|
+
image
|
|
10159
|
+
}, [image.bytes.buffer]);
|
|
10160
|
+
}
|
|
10161
|
+
async handleDocumentRenderPageFileEncoded(req, signal) {
|
|
10162
|
+
const inner = this.handleDocumentRenderPageFile({
|
|
10163
|
+
...req,
|
|
10164
|
+
kind: "document.renderPageFile"
|
|
10165
|
+
}, signal);
|
|
10166
|
+
if (inner.payload.tag !== "document.renderPageFile") throw new EngineError(EngineErrorCode.WireFormat, `unexpected ${inner.payload.tag}`);
|
|
10167
|
+
const image = await this.encodeRaster(inner.payload.raster, req.encode, signal);
|
|
10168
|
+
return wirePack({
|
|
10169
|
+
tag: "document.renderPageFileEncoded",
|
|
10170
|
+
pageObjectNumber: inner.payload.pageObjectNumber,
|
|
10171
|
+
pageCount: inner.payload.pageCount,
|
|
10172
|
+
image
|
|
10173
|
+
}, [image.bytes.buffer]);
|
|
10174
|
+
}
|
|
10175
|
+
async handleAnnotationsRenderAppearancesEncoded(req, signal) {
|
|
10176
|
+
const inner = this.handleAnnotationsRenderAppearances({
|
|
10177
|
+
...req,
|
|
10178
|
+
kind: "annotations.renderAppearances"
|
|
10179
|
+
}, signal);
|
|
10180
|
+
if (inner.payload.tag !== "annotations.renderAppearances") throw new EngineError(EngineErrorCode.WireFormat, `unexpected ${inner.payload.tag}`);
|
|
10181
|
+
const { pageState, appearances } = inner.payload.result;
|
|
10182
|
+
const encoded = [];
|
|
10183
|
+
for (const a of appearances) encoded.push({
|
|
10184
|
+
ref: a.ref,
|
|
10185
|
+
mode: a.mode,
|
|
10186
|
+
rect: a.rect,
|
|
10187
|
+
image: await this.encodeRaster(a.raster, req.encode, signal)
|
|
10188
|
+
});
|
|
10189
|
+
return wirePack({
|
|
10190
|
+
tag: "annotations.renderAppearancesEncoded",
|
|
10191
|
+
result: {
|
|
10192
|
+
pageState,
|
|
10193
|
+
appearances: encoded
|
|
10194
|
+
}
|
|
10195
|
+
}, encoded.map((e) => e.image.bytes.buffer));
|
|
10196
|
+
}
|
|
9518
10197
|
handleDocumentSaveBuffer(req) {
|
|
9519
10198
|
const session = this.requireSession(req);
|
|
9520
10199
|
const saved = new DocumentSaver(this.runtime, session).saveStandaloneToBuffer(req.mode);
|
|
@@ -9846,6 +10525,6 @@ function passwordRequiredProbe() {
|
|
|
9846
10525
|
};
|
|
9847
10526
|
}
|
|
9848
10527
|
//#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 };
|
|
10528
|
+
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
10529
|
|
|
9851
10530
|
//# sourceMappingURL=index.js.map
|