@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.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, revisionTokensEqual, searchRectsForRange, serializeError, shiftsExistingAnnotationIndices, sniffBinaryMetadata, subtypeFromCode, validateSearchQuery, wirePack } from "@embedpdf/engine-core/runtime";
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
- const { serverId } = event.origin;
229
- if (serverId !== null) this.serverId = this.serverId === null ? serverId : Math.max(this.serverId, serverId);
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, 0),
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
- * (the link-target projection) use so the two surfaces cannot drift. */
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
- /** Is this `EPDFAction_GetRootNode`/`GetNextAt` result a real node id?
1228
- * (`EPDF_ACTION_NODE_INVALID` may surface as -1 or u32 max via cwrap.) */
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
- return {
1290
- type: ACTION_TYPE_BY_CODE[fn.EPDFAction_GetNodeType(modelPtr, nodeId)] ?? "unknown",
1291
- subtype,
1292
- ...script !== void 0 ? { script } : {},
1293
- next
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: rootId === null ? null : readNode(rootId),
1658
+ root,
1301
1659
  incomplete,
1302
1660
  warningFlags,
1303
- warnings: warningsFromFlags(warningFlags, incomplete)
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, two views: the target is projected off the SAME fork action
2898
- * model the scripting plane reads (`base.actions`) — root classification
2899
- * via the full `EPDF_ACTION_TYPE_*` vocabulary, payloads via the on-demand
2900
- * node getters. The classic `FPDFAction_*` surface (5 types, JavaScript-
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` wins over `/Dest` — the spec forbids carrying both, and when a
2905
- * malformed file has both, Acrobat gives the action precedence. (Our own
2906
- * writes can't produce that state: `EPDFAnnot_SetAction` strips `/Dest`.)
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
- function readLinkTarget(fn, mem, annotPtr, ctx) {
2916
- const modelPtr = fn.EPDFAnnot_GetActionModel(annotPtr, 0);
2917
- if (modelPtr !== NULL_PTR) try {
2918
- const root = fn.EPDFAction_GetRootNode(modelPtr);
2919
- if (isValidActionNodeId(root)) return readRootTarget(fn, mem, modelPtr, root, ctx);
2920
- } finally {
2921
- fn.EPDFAction_CloseModel(modelPtr);
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): the kinds
4134
- * whose v3 writers put rotation in the AP `/Matrix` + `/EMBD_Metadata`
4135
- * `/UnrotatedRect`. Only these are eligible for rotation-stripped appearance
4136
- * rendering — vertex kinds (line/polyline/polygon/ink) pre-rotate their
4137
- * geometry, so their rasters must stay on the classic path.
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` via `FPDFAnnot_SetRect`. FS_RECTF layout is
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.FPDFAnnot_SetRect(annotPtr, buf)) throw new EngineError(EngineErrorCode.Unknown, "FPDFAnnot_SetRect returned false");
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 own
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) writeStringOrClear(fn, mem, annotPtr, "Contents", draft.contents);
4592
- if (draft.nm !== void 0 && draft.nm.length > 0) writeString(fn, mem, annotPtr, "NM", draft.nm);
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) writeStringOrClear(fn, mem, annotPtr, "Contents", patch.contents);
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
- writeString(fn, mem, annotPtr, "T", displayName);
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
- writeString(fn, mem, annotPtr, "NM", nm);
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
- writeString(fn, mem, annotPtr, "M", formatPdfDate(now));
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/writeCaretAnnotation.ts
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
- * Apply a caret draft to a freshly-created annotation. Caret carries no
4843
- * geometry of its own beyond `/Rect`. Order:
4844
- * 1. base author-metadata (contents/nm/flags)
4845
- * 2. `/Rect` (required — supplied by the caller; never derived)
4846
- * 3. `/C` color + `/CA` opacity
4847
- * 4. `/RD` rectangle differences (optional)
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
- function applyCaretDraft(fn, mem, annotPtr, draft) {
4850
- applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
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
- * MUST run BEFORE `EPDFAnnot_GenerateAppearance` so the bake sees the rotation.
5158
- * `/SchemaVersion` is seeded (stays 1) if this is the first key in the dict.
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
- const KEY_ROTATION = "Rotation";
5161
- const KEY_UNROTATED_RECT = "UnrotatedRect";
5162
- /** Seed `/SchemaVersion` when we are about to create the dict by writing the
5163
- * first transform key, so the marker readers look for is always present. */
5164
- function ensureSchemaVersion(fn, annotPtr) {
5165
- if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, "SchemaVersion", 1);
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
- function setRotation(fn, annotPtr, rotation) {
5168
- ensureSchemaVersion(fn, annotPtr);
5169
- fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_ROTATION, rotation);
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
- function setUnrotatedRect(fn, mem, annotPtr, rect) {
5172
- ensureSchemaVersion(fn, annotPtr);
5173
- const buf = mem.alloc(16);
5174
- try {
5175
- mem.poke(buf, "f32", rect.left, 0);
5176
- mem.poke(buf, "f32", rect.top, 4);
5177
- mem.poke(buf, "f32", rect.right, 8);
5178
- mem.poke(buf, "f32", rect.bottom, 12);
5179
- fn.EPDFAnnot_SetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT, buf);
5180
- } finally {
5181
- mem.free(buf);
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
- * Write transform metadata for a BOX kind (square/circle/free-text/stamp),
5186
- * tri-state per field: `undefined` never touches the document (safe to call on
5187
- * every patch), `null`/`0` clears, a value sets. A nonzero rotation with no
5188
- * unrotated box anywhere (patch or annotation) is unsatisfiable — the AP
5189
- * generator needs the pair — and throws instead of writing a broken state.
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 writeBoxTransformMetadata(fn, mem, annotPtr, t) {
5192
- if (t.rotation !== void 0) if (t.rotation !== null && t.rotation !== 0) {
5193
- if (!(t.unrotatedRect != null || t.unrotatedRect === void 0 && readAnnotationUnrotatedRect(fn, mem, annotPtr) != null)) throw new EngineError(EngineErrorCode.InvalidArg, "rotation requires unrotatedRect: supply the pair together (or rely on an existing /EMBD_Metadata/UnrotatedRect)");
5194
- setRotation(fn, annotPtr, t.rotation);
5195
- } else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
5196
- if (t.unrotatedRect !== void 0) if (t.unrotatedRect !== null) setUnrotatedRect(fn, mem, annotPtr, t.unrotatedRect);
5197
- else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
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
- * Write the advisory `/Rotation` scalar for a VERTEX kind
5201
- * (line/polyline/polygon/ink), tri-state: `undefined` never touches the
5202
- * document, `null`/`0` clears, a value sets. Whenever it does write, any
5203
- * `/UnrotatedRect` is defensively cleared — that key is an impossible state on
5204
- * a vertex kind and must never accidentally drive the AP generator.
5551
+ * Apply the full stroke/fill styling from a patch. Only present fields are
5552
+ * touched; `interiorColor: null` clears `/IC`.
5205
5553
  */
5206
- function writeVertexTransformMetadata(fn, annotPtr, t) {
5207
- if (t.rotation === void 0) return;
5208
- if (t.rotation !== null && t.rotation !== 0) setRotation(fn, annotPtr, t.rotation);
5209
- else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
5210
- fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
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)) return {
6975
- status: "unchanged",
6976
- removed: 0
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
- * extracts the full UTF-16 buffer with `FPDFText_GetText`, decodes,
7011
- * and releases everything in reverse order.
7012
- *
7013
- * Memory model:
7014
- * - `count` is in UTF-16 code units (FPDFText's "char").
7015
- * - The buffer needs `(count + 1) * 2` bytes (PDFium writes a
7016
- * trailing NUL terminator).
7017
- * - `mem.readU16String` walks the buffer up to the NUL and decodes
7018
- * to a JS string, faithfully preserving surrogate pairs.
7019
- *
7020
- * Errors:
7021
- * - `EPDFDoc_LoadPageByObjectNumber` returning 0 is `NotFound`
7022
- * (raised by `PagePtrPool.acquire`).
7023
- * - `FPDFText_LoadPage` returning 0 is `RuntimeUnavailable` — should
7024
- * not happen on a healthy page; treat it as a runtime bug rather
7025
- * than user error.
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, mem } = this.runtime;
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
- return {
7045
- text: readPageText(fn, mem, textPagePtr, charCount),
7046
- charCount: Math.max(charCount, 0)
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
- function readPageText(fn, mem, textPagePtr, charCount) {
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
- return withScratch(mem, (charCount + 1) * 2, (buf) => {
7059
- if (fn.FPDFText_GetText(textPagePtr, 0, charCount, buf) <= 0) return "";
7060
- return mem.readU16String(buf);
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 glyphs = [];
7090
- for (let i = 0; i < glyphCount; i++) {
7091
- throwIfAborted(signal);
7092
- glyphs.push(this.readGlyphInfo(textPagePtr, i));
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: this.buildRunsFromGlyphs(glyphs, textPagePtr) };
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
- buildRunsFromGlyphs(glyphs, textPagePtr) {
7103
- const { fn } = this.runtime;
7104
- const runs = [];
7105
- let current = null;
7106
- let curObjPtr = null;
7107
- let bounds = null;
7108
- for (let i = 0; i < glyphs.length; i++) {
7109
- const glyph = glyphs[i];
7110
- const objPtr = fn.FPDFText_GetTextObject(textPagePtr, i);
7111
- if (objPtr !== curObjPtr) {
7112
- curObjPtr = objPtr;
7113
- current = {
7114
- rect: { ...glyph.looseBox },
7115
- charStart: i,
7116
- glyphs: [],
7117
- fontSize: fn.FPDFText_GetFontSize(textPagePtr, i)
7118
- };
7119
- bounds = { ...glyph.looseBox };
7120
- runs.push(current);
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 runs;
7571
+ return raw;
7131
7572
  }
7132
- readGlyphInfo(textPagePtr, charIndex) {
7133
- const { fn, mem } = this.runtime;
7134
- const rectPtr = mem.alloc(16);
7135
- const tLeftPtr = mem.alloc(8);
7136
- const tRightPtr = mem.alloc(8);
7137
- const tBottomPtr = mem.alloc(8);
7138
- const tTopPtr = mem.alloc(8);
7139
- const ptrs = [
7140
- rectPtr,
7141
- tLeftPtr,
7142
- tRightPtr,
7143
- tBottomPtr,
7144
- tTopPtr
7145
- ];
7146
- try {
7147
- if (!fn.FPDFText_GetLooseCharBox(textPagePtr, charIndex, rectPtr)) return emptyGlyph();
7148
- const left = Number(mem.peek(rectPtr, "f32", 0));
7149
- const top = Number(mem.peek(rectPtr, "f32", 4));
7150
- const right = Number(mem.peek(rectPtr, "f32", 8));
7151
- const bottom = Number(mem.peek(rectPtr, "f32", 12));
7152
- if (left === right || top === bottom) return emptyGlyph();
7153
- const glyph = { looseBox: normalizePdfRect({
7154
- left,
7155
- bottom,
7156
- right,
7157
- top
7158
- }) };
7159
- if (fn.FPDFText_GetCharBox(textPagePtr, charIndex, tLeftPtr, tRightPtr, tBottomPtr, tTopPtr)) {
7160
- const tLeft = Number(mem.peek(tLeftPtr, "f64"));
7161
- const tRight = Number(mem.peek(tRightPtr, "f64"));
7162
- glyph.tightBox = normalizePdfRect({
7163
- left: tLeft,
7164
- bottom: Number(mem.peek(tBottomPtr, "f64")),
7165
- right: tRight,
7166
- top: Number(mem.peek(tTopPtr, "f64"))
7167
- });
7168
- }
7169
- glyph.isSpace = fn.FPDFText_GetUnicode(textPagePtr, charIndex) === 32;
7170
- return glyph;
7171
- } finally {
7172
- for (const ptr of ptrs) mem.free(ptr);
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 emptyGlyph() {
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
- isEmpty: true
7720
+ upright: true,
7721
+ rotation: 0,
7722
+ ascentFlip: false
7185
7723
  };
7186
7724
  }
7187
- function toSlimGlyph(glyph) {
7725
+ function readQuad(mem, ptr, byteOffset) {
7726
+ const f = (offset) => readF32(mem, ptr, byteOffset + offset);
7188
7727
  return {
7189
- looseBox: glyph.looseBox,
7190
- flags: glyph.isEmpty ? 2 : glyph.isSpace ? 1 : 0,
7191
- ...glyph.tightBox ? { tightBox: glyph.tightBox } : {}
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 extracted text plus its
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.folded;
8590
- const text = new PageTextReader(runtime, session).read(pageObjectNumber, signal).text;
8591
- const folded = foldText(text);
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 folded;
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(corpus.original, query);
8712
- else if (query.matchCase || query.matchDiacritics) ranges = matchLiteral(foldText(corpus.original, foldOptionsFor(query)), query);
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 geometry = new PageGeometryReader(this.runtime, this.session).read(pon, signal);
8716
- for (const range of ranges) matches.push({
8717
- pageObjectNumber: pon,
8718
- charStart: range.start,
8719
- charCount: range.length,
8720
- rects: searchRectsForRange(geometry, range.start, range.length),
8721
- ...mode === "full" ? { snippet: buildSnippet(corpus.original, range) } : {}
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
- const action = requireAction(readActionModel(fn, mem, fn.EPDFDoc_GetNamedJavaScriptActionModel(docPtr, index), budget), "name-tree script", index);
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