@embedpdf/engine-services 3.0.0-next.1 → 3.0.0-next.11

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