@embedpdf/engine-core 3.0.0-next.15 → 3.0.0-next.16

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.
Files changed (91) hide show
  1. package/dist/{AnnotationList-CkijRHet.d.cts → AnnotationList-DcyhF2kU.d.cts} +1836 -1308
  2. package/dist/{AnnotationList-CkijRHet.d.ts → AnnotationList-DcyhF2kU.d.ts} +1836 -1308
  3. package/dist/{Change-CjImsrN0.cjs → Change-Ct-dB8D5.cjs} +115 -2
  4. package/dist/Change-Ct-dB8D5.cjs.map +1 -0
  5. package/dist/{Change-CKoxfTNY.js → Change-tMR34hTU.js} +92 -3
  6. package/dist/Change-tMR34hTU.js.map +1 -0
  7. package/dist/{DocumentRenderService-BfFdFSwI.cjs → DocumentRenderService-9IjSQEIU.cjs} +2 -1
  8. package/dist/{DocumentRenderService-BfFdFSwI.cjs.map → DocumentRenderService-9IjSQEIU.cjs.map} +1 -1
  9. package/dist/{DocumentRenderService-DV3J9lMz.js → DocumentRenderService-DFrZ9wfW.js} +2 -1
  10. package/dist/{DocumentRenderService-DV3J9lMz.js.map → DocumentRenderService-DFrZ9wfW.js.map} +1 -1
  11. package/dist/{DocumentSecurityService-e6vp2de8.d.cts → DocumentSecurityService-D2zA4NKi.d.cts} +2 -2
  12. package/dist/{DocumentSecurityService-CU1M6Q7m.d.ts → DocumentSecurityService-YJV9df8P.d.ts} +2 -2
  13. package/dist/{LocalEngine-DFoBm4B3.d.cts → LocalEngine-DBt2-GWF.d.ts} +102 -18
  14. package/dist/{LocalEngine-DPriFujS.d.ts → LocalEngine-IA9zh3Jh.d.cts} +102 -18
  15. package/dist/{authority-DLMNR7ff.cjs → authority-BFp6XM04.cjs} +373 -52
  16. package/dist/authority-BFp6XM04.cjs.map +1 -0
  17. package/dist/{authority-B3BklsB4.js → authority-PuxpGFhJ.js} +320 -53
  18. package/dist/authority-PuxpGFhJ.js.map +1 -0
  19. package/dist/{builders-CNj0POeY.js → builders-DV62HA3I.js} +6 -2
  20. package/dist/builders-DV62HA3I.js.map +1 -0
  21. package/dist/{builders-Cgdp_YCn.cjs → builders-DgTAj_tP.cjs} +6 -2
  22. package/dist/builders-DgTAj_tP.cjs.map +1 -0
  23. package/dist/{calibrate-DJBXnhLE.cjs → calibrate-DJbi1wuK.cjs} +118 -25
  24. package/dist/calibrate-DJbi1wuK.cjs.map +1 -0
  25. package/dist/{calibrate-7T3fnP8-.js → calibrate-nQFgCuki.js} +83 -26
  26. package/dist/calibrate-nQFgCuki.js.map +1 -0
  27. package/dist/charmap-Cprti1ta.js.map +1 -1
  28. package/dist/charmap-aYlefQP9.cjs.map +1 -1
  29. package/dist/conformance.cjs +1744 -75
  30. package/dist/conformance.cjs.map +1 -1
  31. package/dist/conformance.d.cts +105 -10
  32. package/dist/conformance.d.ts +105 -10
  33. package/dist/conformance.js +1738 -76
  34. package/dist/conformance.js.map +1 -1
  35. package/dist/index.cjs +25 -6
  36. package/dist/index.d.cts +2 -2
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.js +7 -7
  39. package/dist/public.cjs +6 -4
  40. package/dist/public.d.cts +5 -5
  41. package/dist/public.d.ts +5 -5
  42. package/dist/public.js +5 -5
  43. package/dist/{resolver-CzxmD9Ck.js → resolver-BBggAxQc.js} +6 -3
  44. package/dist/resolver-BBggAxQc.js.map +1 -0
  45. package/dist/{resolver-D2OCtE8F.cjs → resolver-BWSWSF6R.cjs} +6 -3
  46. package/dist/resolver-BWSWSF6R.cjs.map +1 -0
  47. package/dist/{runtime-Cl0v56mm.d.ts → runtime-BaDKjxiu.d.ts} +67 -140
  48. package/dist/{runtime-p1WDzDo4.d.cts → runtime-CWEuiwbk.d.cts} +67 -140
  49. package/dist/runtime.cjs +267 -7
  50. package/dist/runtime.cjs.map +1 -1
  51. package/dist/runtime.d.cts +5 -5
  52. package/dist/runtime.d.ts +5 -5
  53. package/dist/runtime.js +246 -8
  54. package/dist/runtime.js.map +1 -1
  55. package/dist/{schema-DzlUNk38.js → schema-6S2VmlAj.js} +8 -2
  56. package/dist/schema-6S2VmlAj.js.map +1 -0
  57. package/dist/{schema-C-gU4vj7.cjs → schema-miH6tWQC.cjs} +13 -1
  58. package/dist/schema-miH6tWQC.cjs.map +1 -0
  59. package/dist/{schemas-D6xOGjqg.js → schemas-BE2QGOit.js} +256 -59
  60. package/dist/schemas-BE2QGOit.js.map +1 -0
  61. package/dist/{schemas-Berf1swF.cjs → schemas-DljJR9ut.cjs} +397 -80
  62. package/dist/schemas-DljJR9ut.cjs.map +1 -0
  63. package/dist/validation.cjs +1 -1
  64. package/dist/validation.d.cts +1 -1
  65. package/dist/validation.d.ts +1 -1
  66. package/dist/validation.js +1 -1
  67. package/dist/{viewport-DlL5Bxbg.cjs → viewport-CHG9qQUb.cjs} +2 -2
  68. package/dist/{viewport-DlL5Bxbg.cjs.map → viewport-CHG9qQUb.cjs.map} +1 -1
  69. package/dist/{viewport-BJW13EGZ.js → viewport-DQR5LyX3.js} +2 -2
  70. package/dist/{viewport-BJW13EGZ.js.map → viewport-DQR5LyX3.js.map} +1 -1
  71. package/dist/wire.cjs +225 -57
  72. package/dist/wire.cjs.map +1 -1
  73. package/dist/wire.d.cts +3548 -1151
  74. package/dist/wire.d.ts +3548 -1151
  75. package/dist/wire.js +198 -53
  76. package/dist/wire.js.map +1 -1
  77. package/package.json +1 -1
  78. package/dist/Change-CKoxfTNY.js.map +0 -1
  79. package/dist/Change-CjImsrN0.cjs.map +0 -1
  80. package/dist/authority-B3BklsB4.js.map +0 -1
  81. package/dist/authority-DLMNR7ff.cjs.map +0 -1
  82. package/dist/builders-CNj0POeY.js.map +0 -1
  83. package/dist/builders-Cgdp_YCn.cjs.map +0 -1
  84. package/dist/calibrate-7T3fnP8-.js.map +0 -1
  85. package/dist/calibrate-DJBXnhLE.cjs.map +0 -1
  86. package/dist/resolver-CzxmD9Ck.js.map +0 -1
  87. package/dist/resolver-D2OCtE8F.cjs.map +0 -1
  88. package/dist/schema-C-gU4vj7.cjs.map +0 -1
  89. package/dist/schema-DzlUNk38.js.map +0 -1
  90. package/dist/schemas-Berf1swF.cjs.map +0 -1
  91. package/dist/schemas-D6xOGjqg.js.map +0 -1
@@ -1,11 +1,11 @@
1
+ import { C as outlineItemAfter, E as OUTLINE_READ_LIMITS, S as deletedAttachmentOf, d as formFieldAfter, f as formFieldOfDraft, g as widgetAfterPatch, l as fieldAfterReset, m as widgetsAfterDisplay, s as formEffectOps, t as isSkippedItem, u as fieldAfterValue, w as outlineItemOfDraft } from "./Change-tMR34hTU.js";
2
+ import { At as pageTransform, Ct as isRotatedGeometryRun, c as foldText, f as applyAnnotationPatch, g as pagePointTurned, n as measureFromRatio, nt as AnnotationTransfer, p as drawnPointsOf, rt as assertAnnotationBundle, t as measureFromKnownLength, tt as toAttachmentRef, u as annotationOfDraft, v as pagePointsBounds, vt as toOutlineItemRef, wt as pageBoxOf, x as pageTurnOfUpright, y as pageQuadBounds, yt as AbortError } from "./calibrate-nQFgCuki.js";
1
3
  import { O as DRAWN_RECT_KINDS, S as measurementReadout, X as resourceIdOf, wt as pdfRectTurnedBounds } from "./resolveAnnotationDraft-CUpf0SMh.js";
2
4
  import { f as toPageRef, h as EngineError, i as encodeFieldRefKey, m as annotationKey, o as toFieldRef, u as encodePageKey, y as EngineErrorCode } from "./format-Dgg428Kb.js";
3
- import { Dt as pageTransform, bt as isRotatedGeometryRun, c as foldText, et as toAttachmentRef, f as applyAnnotationPatch, g as pagePointTurned, gt as AbortError, n as measureFromRatio, nt as assertAnnotationBundle, p as drawnPointsOf, t as measureFromKnownLength, tt as AnnotationTransfer, u as annotationOfDraft, v as pagePointsBounds, x as pageTurnOfUpright, xt as pageBoxOf, y as pageQuadBounds } from "./calibrate-7T3fnP8-.js";
4
5
  import { n as pageRefsIn } from "./pageRefs-DkbuxSCV.js";
5
- import { S as deletedAttachmentOf, d as formFieldAfter, f as formFieldOfDraft, g as widgetAfterPatch, l as fieldAfterReset, m as widgetsAfterDisplay, s as formEffectOps, t as isSkippedItem, u as fieldAfterValue } from "./Change-CKoxfTNY.js";
6
6
  import "./PageInsertBlankInput-DhVYVJ_S.js";
7
- import { C as declarationOf, _ as AnnotationSchema, in as DocumentActionsSnapshotSchema, l as FormSnapshotSchema } from "./schema-DzlUNk38.js";
8
- import { C as AttachmentCreateResultSchema, Ht as PageGeometrySnapshotSchema, Rt as PageDeleteResultSchema, T as AttachmentListSchema, Vt as PageFlattenResultSchema, Yt as PageNameResultSchema, an as PageTextSnapshotSchema, cn as RedactionApplyResultSchema, ct as FormRepairResultSchema, dt as FormSetValueResultSchema, gt as FormWidgetLinkResultSchema, h as AnnotationListSchema, o as AnnotationCreateResultSchema, pn as SearchSliceSchema, qt as PageListSnapshotSchema, rn as PageRotateResultSchema, tn as PageReorderResultSchema, u as AnnotationFlattenResultSchema, ut as FormResetResultSchema, v as AnnotationReorderResultSchema, xt as FormWidgetsReorderResultSchema, y as AnnotationUpdateResultSchema } from "./schemas-D6xOGjqg.js";
7
+ import { C as declarationOf, _ as AnnotationSchema, an as DocumentActionsSnapshotSchema, l as FormSnapshotSchema } from "./schema-6S2VmlAj.js";
8
+ import { C as AttachmentCreateResultSchema, Ct as FormWidgetsReorderResultSchema, Jt as PageDeleteResultSchema, Qt as PageGeometrySnapshotSchema, Sn as SearchSliceSchema, T as AttachmentListSchema, Zt as PageFlattenResultSchema, _n as RedactionApplyResultSchema, fn as PageRotateResultSchema, ft as FormResetResultSchema, h as AnnotationListSchema, in as PageNameResultSchema, mn as PageTextSnapshotSchema, nn as PageListSnapshotSchema, o as AnnotationCreateResultSchema, pt as FormSetValueResultSchema, u as AnnotationFlattenResultSchema, un as PageReorderResultSchema, ut as FormRepairResultSchema, v as AnnotationReorderResultSchema, vt as FormWidgetLinkResultSchema, y as AnnotationUpdateResultSchema } from "./schemas-BE2QGOit.js";
9
9
  import { n as isLocalPage, t as isLocalDocument } from "./LocalDocumentHandle-DAPdaOt_.js";
10
10
  //#region src/conformance/fixtureScope.ts
11
11
  /**
@@ -3791,7 +3791,7 @@ const FORGED = {
3791
3791
  modifiedAt: "2001-01-01T00:00:00.000Z",
3792
3792
  importedBy: "mallory"
3793
3793
  };
3794
- const SQUARE$1 = {
3794
+ const SQUARE$2 = {
3795
3795
  subtype: "square",
3796
3796
  box: {
3797
3797
  x: 40,
@@ -3848,7 +3848,7 @@ function runAnnotationAttributionConformance(runner, opts) {
3848
3848
  }, async (page) => {
3849
3849
  const before = Date.now();
3850
3850
  const { annotation: created } = await page.annotations.create({
3851
- ...SQUARE$1,
3851
+ ...SQUARE$2,
3852
3852
  ...FORGED
3853
3853
  });
3854
3854
  const after = Date.now();
@@ -3881,7 +3881,7 @@ function runAnnotationAttributionConformance(runner, opts) {
3881
3881
  try {
3882
3882
  const page = await firstPage$1(alice);
3883
3883
  ({annotation: original} = await page.annotations.create({
3884
- ...SQUARE$1,
3884
+ ...SQUARE$2,
3885
3885
  nm: "attribution-conformance-update"
3886
3886
  }));
3887
3887
  } catch (error) {
@@ -3947,12 +3947,12 @@ function runAnnotationAttributionConformance(runner, opts) {
3947
3947
  identity: ALICE$2
3948
3948
  }, async (page) => {
3949
3949
  const own = await page.annotations.create({
3950
- ...SQUARE$1,
3950
+ ...SQUARE$2,
3951
3951
  groupId: "legal"
3952
3952
  });
3953
3953
  expect(own.annotation.groupId).toBe("legal");
3954
3954
  await expectRefused(() => page.annotations.create({
3955
- ...SQUARE$1,
3955
+ ...SQUARE$2,
3956
3956
  groupId: "finance"
3957
3957
  }), "set-group");
3958
3958
  });
@@ -3962,7 +3962,7 @@ function runAnnotationAttributionConformance(runner, opts) {
3962
3962
  };
3963
3963
  await asSession(granted, async (page) => {
3964
3964
  const { annotation: created } = await page.annotations.create({
3965
- ...SQUARE$1,
3965
+ ...SQUARE$2,
3966
3966
  groupId: "finance"
3967
3967
  });
3968
3968
  expect(attributionOf(await readBack(page, created.ref))).toMatchObject({
@@ -3976,7 +3976,7 @@ function runAnnotationAttributionConformance(runner, opts) {
3976
3976
  scope: SCOPE,
3977
3977
  identity: ALICE$2
3978
3978
  }, async (page) => {
3979
- const { annotation: created } = await page.annotations.create(SQUARE$1);
3979
+ const { annotation: created } = await page.annotations.create(SQUARE$2);
3980
3980
  await expectRefused(() => page.annotations.update(created.ref, { groupId: "finance" }), "set-group");
3981
3981
  expect((await readBack(page, created.ref)).groupId).toBe("legal");
3982
3982
  });
@@ -3985,7 +3985,7 @@ function runAnnotationAttributionConformance(runner, opts) {
3985
3985
  identity: ALICE$2
3986
3986
  };
3987
3987
  await asSession(granted, async (page) => {
3988
- const { annotation: created } = await page.annotations.create(SQUARE$1);
3988
+ const { annotation: created } = await page.annotations.create(SQUARE$2);
3989
3989
  await page.annotations.update(created.ref, { groupId: "finance" });
3990
3990
  expect((await readBack(page, created.ref)).groupId).toBe("finance");
3991
3991
  });
@@ -4002,7 +4002,7 @@ function runAnnotationAttributionConformance(runner, opts) {
4002
4002
  width: 30,
4003
4003
  height: 30
4004
4004
  };
4005
- const { annotation: square } = await page.annotations.create(SQUARE$1);
4005
+ const { annotation: square } = await page.annotations.create(SQUARE$2);
4006
4006
  const { annotation: note } = await page.annotations.create({
4007
4007
  subtype: "text",
4008
4008
  rect: iconRect(rect.x, rect.y)
@@ -4066,7 +4066,7 @@ function runAnnotationAttributionConformance(runner, opts) {
4066
4066
  scope: READ,
4067
4067
  identity: ALICE$2
4068
4068
  }, async (page, doc) => {
4069
- const { annotation: created } = await page.annotations.create(SQUARE$1);
4069
+ const { annotation: created } = await page.annotations.create(SQUARE$2);
4070
4070
  const bundle = await doc.annotations.export({ refs: [created.ref] });
4071
4071
  await expectRefused(() => doc.annotations.import(bundle), "doc.annotate.import");
4072
4072
  const stamped = await doc.annotations.import(bundle, { attribution: "stamp" });
@@ -4087,7 +4087,7 @@ function runAnnotationAttributionConformance(runner, opts) {
4087
4087
  ],
4088
4088
  identity: BOB$2
4089
4089
  }, async (page, doc) => {
4090
- const { annotation: created } = await page.annotations.create(SQUARE$1);
4090
+ const { annotation: created } = await page.annotations.create(SQUARE$2);
4091
4091
  const bundle = await doc.annotations.export({ refs: [created.ref] });
4092
4092
  const stamped = await doc.annotations.import(bundle, { attribution: "stamp" });
4093
4093
  expect(attributionOf(stamped.annotations[0])).toMatchObject({ userId: "bob" });
@@ -8708,24 +8708,24 @@ function runPageRenderConformance(runner, opts) {
8708
8708
  format: "png",
8709
8709
  ...options
8710
8710
  });
8711
- const raster = await decode$2(image);
8711
+ const raster = await decode$3(image);
8712
8712
  expect([image.width, image.height]).toEqual(size);
8713
8713
  expect([raster.width, raster.height]).toEqual(size);
8714
8714
  }
8715
8715
  });
8716
8716
  test("a page image starts at the crop box", async () => {
8717
- const raster = await decode$2(await page.render.image({
8717
+ const raster = await decode$3(await page.render.image({
8718
8718
  format: "png",
8719
8719
  viewport: {
8720
8720
  kind: "scale",
8721
8721
  scale: .5
8722
8722
  }
8723
8723
  }));
8724
- expect(isDark$1(pixel$1(raster, 75, 125))).toBe(true);
8725
- expect(isDark$1(pixel$1(raster, 25, 25))).toBe(false);
8724
+ expect(isDark$2(pixel$2(raster, 75, 125))).toBe(true);
8725
+ expect(isDark$2(pixel$2(raster, 25, 25))).toBe(false);
8726
8726
  });
8727
8727
  test("a target rect is in page space, from the crop box's top-left", async () => {
8728
- const square = await decode$2(await page.render.image({
8728
+ const square = await decode$3(await page.render.image({
8729
8729
  format: "png",
8730
8730
  target: {
8731
8731
  kind: "rect",
@@ -8737,8 +8737,8 @@ function runPageRenderConformance(runner, opts) {
8737
8737
  }
8738
8738
  }));
8739
8739
  expect([square.width, square.height]).toEqual([10, 10]);
8740
- expect(everyPixel(square, isDark$1)).toBe(true);
8741
- const beside = await decode$2(await page.render.image({
8740
+ expect(everyPixel(square, isDark$2)).toBe(true);
8741
+ const beside = await decode$3(await page.render.image({
8742
8742
  format: "png",
8743
8743
  target: {
8744
8744
  kind: "rect",
@@ -8754,7 +8754,7 @@ function runPageRenderConformance(runner, opts) {
8754
8754
  scale: .1
8755
8755
  }
8756
8756
  }));
8757
- expect(everyPixel(beside, (rgba) => !isDark$1(rgba))).toBe(true);
8757
+ expect(everyPixel(beside, (rgba) => !isDark$2(rgba))).toBe(true);
8758
8758
  });
8759
8759
  test("an appearance on a cropped page renders its own area", async () => {
8760
8760
  const { annotation } = await page.annotations.create({
@@ -8770,8 +8770,8 @@ function runPageRenderConformance(runner, opts) {
8770
8770
  const { x, y, width, height } = found.rect;
8771
8771
  expect(Math.abs(x - SQUARE_ON_PAGE.x) <= 2 && Math.abs(y - SQUARE_ON_PAGE.y) <= 2).toBe(true);
8772
8772
  expect(Math.abs(width - 100) <= 4 && Math.abs(height - 100) <= 4).toBe(true);
8773
- const raster = await decode$2(found.image);
8774
- const [r, g, b, a] = pixel$1(raster, raster.width >> 1, raster.height >> 1);
8773
+ const raster = await decode$3(found.image);
8774
+ const [r, g, b, a] = pixel$2(raster, raster.width >> 1, raster.height >> 1);
8775
8775
  expect(r > 200 && g < 60 && b < 60 && a > 200).toBe(true);
8776
8776
  } finally {
8777
8777
  await page.annotations.delete(annotation.ref);
@@ -8830,17 +8830,17 @@ function runPageRenderConformance(runner, opts) {
8830
8830
  const expected = pageTransform(layout, options);
8831
8831
  expect([expected.width, expected.height]).toEqual([image.width, image.height]);
8832
8832
  expect(transform.matrix.every((n, i) => Math.abs(n - expected.matrix[i]) < 1e-9)).toBe(true);
8833
- const raster = await decode$2(image);
8833
+ const raster = await decode$3(image);
8834
8834
  const inside = transform.pageToPixels({
8835
8835
  x: 150,
8836
8836
  y: 250
8837
8837
  });
8838
- expect(isDark$1(pixel$1(raster, Math.floor(inside.x), Math.floor(inside.y)))).toBe(true);
8838
+ expect(isDark$2(pixel$2(raster, Math.floor(inside.x), Math.floor(inside.y)))).toBe(true);
8839
8839
  const outside = transform.pageToPixels({
8840
8840
  x: 70,
8841
8841
  y: 250
8842
8842
  });
8843
- expect(isDark$1(pixel$1(raster, Math.floor(outside.x), Math.floor(outside.y)))).toBe(false);
8843
+ expect(isDark$2(pixel$2(raster, Math.floor(outside.x), Math.floor(outside.y)))).toBe(false);
8844
8844
  const back = transform.pixelsToPage(inside);
8845
8845
  expect(Math.abs(back.x - 150) < 1e-9 && Math.abs(back.y - 250) < 1e-9).toBe(true);
8846
8846
  }
@@ -8898,10 +8898,10 @@ function runPageRenderConformance(runner, opts) {
8898
8898
  });
8899
8899
  }
8900
8900
  /** The image's pixels, read the way an app would on either engine: through `blob()`. */
8901
- async function decode$2(image) {
8901
+ async function decode$3(image) {
8902
8902
  return decodePng(new Uint8Array(await (await image.blob()).arrayBuffer()));
8903
8903
  }
8904
- function pixel$1(raster, x, y) {
8904
+ function pixel$2(raster, x, y) {
8905
8905
  const at = (y * raster.width + x) * 4;
8906
8906
  const { rgba } = raster;
8907
8907
  return [
@@ -8911,14 +8911,507 @@ function pixel$1(raster, x, y) {
8911
8911
  rgba[at + 3]
8912
8912
  ];
8913
8913
  }
8914
- function isDark$1([r, g, b]) {
8914
+ function isDark$2([r, g, b]) {
8915
8915
  return r < 60 && g < 60 && b < 60;
8916
8916
  }
8917
8917
  function everyPixel(raster, check) {
8918
- for (let y = 0; y < raster.height; y++) for (let x = 0; x < raster.width; x++) if (!check(pixel$1(raster, x, y))) return false;
8918
+ for (let y = 0; y < raster.height; y++) for (let x = 0; x < raster.width; x++) if (!check(pixel$2(raster, x, y))) return false;
8919
8919
  return true;
8920
8920
  }
8921
8921
  //#endregion
8922
+ //#region src/conformance/runPrintRenderConformance.ts
8923
+ const SQUARE$1 = "0 g 0 0 40 40 re f";
8924
+ const LAYERS = "/OC /OCp BDC 0 g 10 150 40 40 re f EMC\n/OC /OCv BDC 0 g 60 150 40 40 re f EMC";
8925
+ /** An annotation drawn as a black 40 × 40 square at `rect`, with the flags `f`. */
8926
+ const square$2 = (rect, f) => `<< /Type /Annot /Subtype /Square /Rect [${rect}] /F ${f} /C [0 0 0] /P 3 0 R /AP << /N 8 0 R >> >>`;
8927
+ /**
8928
+ * A 200 × 200 page with one black 40 × 40 square for each print rule, and an
8929
+ * A0 page (2384 × 3370) with nothing on it:
8930
+ *
8931
+ * - a layer that only prints, and one only shown on screen (`/Usage`
8932
+ * `/PrintState` and `/ViewState`, as Acrobat's watermarks write them);
8933
+ * - four squares: one that only prints (`noView` + `print`), one only on
8934
+ * screen (no `print`), one on both (`print`), one on neither (`hidden`);
8935
+ * - two form fields: a push button only on screen (Acrobat's "Visible but
8936
+ * doesn't print") and a text field that only prints ("Hidden but
8937
+ * printable").
8938
+ */
8939
+ const PRINT_RULES_PDF = pdfOf([
8940
+ "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [13 0 R 14 0 R] >> /OCProperties << /OCGs [6 0 R 7 0 R] /D << /ON [7 0 R] /OFF [6 0 R] /AS [<< /Event /Print /Category [/Print] /OCGs [6 0 R 7 0 R] >> << /Event /View /Category [/View] /OCGs [6 0 R 7 0 R] >>] >> >> >>",
8941
+ "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>",
8942
+ "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 5 0 R /Resources << /Properties << /OCp 6 0 R /OCv 7 0 R >> >> /Annots [9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R] >>",
8943
+ "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 2384 3370] /Resources << >> >>",
8944
+ `<< /Length 77 >>\nstream\n${LAYERS}\nendstream`,
8945
+ "<< /Type /OCG /Name (Prints only) /Usage << /Print << /PrintState /ON >> /View << /ViewState /OFF >> >> >>",
8946
+ "<< /Type /OCG /Name (Screen only) /Usage << /Print << /PrintState /OFF >> /View << /ViewState /ON >> >> >>",
8947
+ `<< /Type /XObject /Subtype /Form /BBox [0 0 40 40] /Length 18 >>\nstream\n${SQUARE$1}\nendstream`,
8948
+ square$2("10 90 50 130", 36),
8949
+ square$2("60 90 100 130", 0),
8950
+ square$2("110 90 150 130", 4),
8951
+ square$2("150 90 190 130", 6),
8952
+ "<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (print) /Rect [10 30 50 70] /F 0 /P 3 0 R /AP << /N 8 0 R >> >>",
8953
+ "<< /Type /Annot /Subtype /Widget /FT /Tx /T (stamp) /V (x) /Rect [60 30 100 70] /F 36 /P 3 0 R /AP << /N 8 0 R >> >>"
8954
+ ]);
8955
+ /** The middle of each square in page space, and where it shows. */
8956
+ const SPOTS = {
8957
+ layerPrintOnly: {
8958
+ at: {
8959
+ x: 30,
8960
+ y: 30
8961
+ },
8962
+ display: false,
8963
+ print: true
8964
+ },
8965
+ layerScreenOnly: {
8966
+ at: {
8967
+ x: 80,
8968
+ y: 30
8969
+ },
8970
+ display: true,
8971
+ print: false
8972
+ },
8973
+ annotationPrintOnly: {
8974
+ at: {
8975
+ x: 30,
8976
+ y: 90
8977
+ },
8978
+ display: false,
8979
+ print: true
8980
+ },
8981
+ annotationScreenOnly: {
8982
+ at: {
8983
+ x: 80,
8984
+ y: 90
8985
+ },
8986
+ display: true,
8987
+ print: false
8988
+ },
8989
+ annotationBoth: {
8990
+ at: {
8991
+ x: 130,
8992
+ y: 90
8993
+ },
8994
+ display: true,
8995
+ print: true
8996
+ },
8997
+ annotationHidden: {
8998
+ at: {
8999
+ x: 170,
9000
+ y: 90
9001
+ },
9002
+ display: false,
9003
+ print: false
9004
+ },
9005
+ fieldScreenOnly: {
9006
+ at: {
9007
+ x: 30,
9008
+ y: 150
9009
+ },
9010
+ display: true,
9011
+ print: false
9012
+ },
9013
+ fieldPrintOnly: {
9014
+ at: {
9015
+ x: 80,
9016
+ y: 150
9017
+ },
9018
+ display: false,
9019
+ print: true
9020
+ }
9021
+ };
9022
+ const ANNOTATION_SPOTS = [
9023
+ "annotationPrintOnly",
9024
+ "annotationScreenOnly",
9025
+ "annotationBoth",
9026
+ "annotationHidden"
9027
+ ];
9028
+ const FIELD_SPOTS = ["fieldScreenOnly", "fieldPrintOnly"];
9029
+ /** The rights each case opens with: everything, no print, print but not high. */
9030
+ const PRINT_SCOPES = {
9031
+ everything: ["*"],
9032
+ screen: [
9033
+ "doc.open",
9034
+ "doc.render",
9035
+ "doc.annotate.read",
9036
+ "doc.forms.read"
9037
+ ],
9038
+ print: [
9039
+ "doc.open",
9040
+ "doc.render",
9041
+ "doc.annotate.read",
9042
+ "doc.forms.read",
9043
+ "doc.print"
9044
+ ]
9045
+ };
9046
+ /**
9047
+ * Print pictures on both engines (`usage: 'print'` with a `dpi` viewport).
9048
+ * A print picture follows
9049
+ * the print rules where a screen picture follows the screen's: layers by
9050
+ * their print state, annotations and form fields by their `print` flag,
9051
+ * `noView` ones included, `hidden` ones never. It is the whole page at its
9052
+ * DPI, never more pixels than the page fitted to A4 or Letter needs, and its
9053
+ * transform is `pageTransform`'s. It needs `doc.print`, and 300 DPI
9054
+ * `doc.print.high`; options that don't fit a print picture are refused.
9055
+ */
9056
+ function runPrintRenderConformance(runner, opts) {
9057
+ const { describe, test, beforeAll, afterAll, expect } = runner;
9058
+ describe(`print render conformance: ${opts.label}`, () => {
9059
+ let engine;
9060
+ let doc;
9061
+ let small;
9062
+ let large;
9063
+ beforeAll(async () => {
9064
+ engine = await opts.makeEngine();
9065
+ doc = await opts.open(engine, PRINT_SCOPES.everything);
9066
+ const { pages } = await doc.pages.list();
9067
+ small = pages[0];
9068
+ large = pages[1];
9069
+ });
9070
+ afterAll(async () => {
9071
+ if (doc) await doc.close();
9072
+ if (engine) await engine.destroy();
9073
+ });
9074
+ const as = async (scope, body) => {
9075
+ const restricted = await opts.open(engine, scope);
9076
+ try {
9077
+ await body(restricted);
9078
+ } finally {
9079
+ await restricted.close();
9080
+ }
9081
+ };
9082
+ /** Which spots are dark in a picture of the small page made with `options`. */
9083
+ const darkSpots = (options) => darkSpotsOf(doc, small.ref, options);
9084
+ test("a print picture is the whole page at its DPI, with pageTransform as its transform", async () => {
9085
+ for (const dpi of [150, 300]) {
9086
+ const options = print(dpi);
9087
+ const image = await doc.page(small.ref).render.image({
9088
+ format: "png",
9089
+ ...options
9090
+ });
9091
+ const size = Math.round(200 * dpi / 72);
9092
+ expect([image.width, image.height]).toEqual([size, size]);
9093
+ const raster = await decode$2(image);
9094
+ expect([raster.width, raster.height]).toEqual([size, size]);
9095
+ const expected = pageTransform(small, options);
9096
+ expect([expected.width, expected.height]).toEqual([size, size]);
9097
+ expect(image.transform.matrix.every((n, i) => Math.abs(n - expected.matrix[i]) < 1e-9)).toBe(true);
9098
+ }
9099
+ });
9100
+ test("a page bigger than the paper is printed at the size it has fitted to A4 or Letter", async () => {
9101
+ const options = {
9102
+ ...print(150),
9103
+ includeAnnotations: false,
9104
+ includeFormFields: false
9105
+ };
9106
+ const image = await doc.page(large.ref).render.image({
9107
+ format: "png",
9108
+ ...options
9109
+ });
9110
+ expect([image.width, image.height]).toEqual([1241, 1754]);
9111
+ const expected = pageTransform(large, options);
9112
+ expect([expected.width, expected.height]).toEqual([1241, 1754]);
9113
+ });
9114
+ test("a screen picture follows the screen rules, a print picture the print rules", async () => {
9115
+ const layers = {
9116
+ includeAnnotations: true,
9117
+ includeFormFields: true
9118
+ };
9119
+ const all = [...ANNOTATION_SPOTS, ...FIELD_SPOTS];
9120
+ expect(await darkSpots(layers)).toEqual(shown("display", all));
9121
+ expect(await darkSpots({
9122
+ ...layers,
9123
+ ...print(150)
9124
+ })).toEqual(shown("print", all));
9125
+ });
9126
+ test("each print picture draws what its family draws", async () => {
9127
+ const cases = [
9128
+ {
9129
+ layers: {
9130
+ includeAnnotations: false,
9131
+ includeFormFields: false
9132
+ },
9133
+ spots: []
9134
+ },
9135
+ {
9136
+ layers: {
9137
+ includeAnnotations: true,
9138
+ includeFormFields: false
9139
+ },
9140
+ spots: ANNOTATION_SPOTS
9141
+ },
9142
+ {
9143
+ layers: {
9144
+ includeAnnotations: false,
9145
+ includeFormFields: true
9146
+ },
9147
+ spots: FIELD_SPOTS
9148
+ }
9149
+ ];
9150
+ for (const { layers, spots } of cases) expect(await darkSpots({
9151
+ ...layers,
9152
+ ...print(300)
9153
+ })).toEqual(shown("print", spots));
9154
+ });
9155
+ test("options that do not fit a print picture are refused", async () => {
9156
+ const page = doc.page(small.ref);
9157
+ const refused = [
9158
+ { usage: "print" },
9159
+ {
9160
+ usage: "print",
9161
+ viewport: {
9162
+ kind: "scale",
9163
+ scale: 2
9164
+ }
9165
+ },
9166
+ {
9167
+ usage: "print",
9168
+ viewport: {
9169
+ kind: "width",
9170
+ width: 400
9171
+ }
9172
+ },
9173
+ {
9174
+ usage: "print",
9175
+ viewport: {
9176
+ kind: "dpi",
9177
+ dpi: 200
9178
+ }
9179
+ },
9180
+ { viewport: {
9181
+ kind: "dpi",
9182
+ dpi: 150
9183
+ } },
9184
+ {
9185
+ usage: "display",
9186
+ viewport: {
9187
+ kind: "dpi",
9188
+ dpi: 300
9189
+ }
9190
+ },
9191
+ {
9192
+ ...print(150),
9193
+ target: {
9194
+ kind: "rect",
9195
+ rect: {
9196
+ x: 0,
9197
+ y: 0,
9198
+ width: 50,
9199
+ height: 50
9200
+ }
9201
+ }
9202
+ },
9203
+ {
9204
+ ...print(150),
9205
+ rotation: 90
9206
+ },
9207
+ {
9208
+ ...print(150),
9209
+ background: "transparent"
9210
+ }
9211
+ ];
9212
+ for (const options of refused) await expect(page.render.image({
9213
+ format: "png",
9214
+ ...options
9215
+ })).rejects.toMatchObject({ code: EngineErrorCode.InvalidArg });
9216
+ });
9217
+ test("printing needs doc.print, and 300 DPI doc.print.high", async () => {
9218
+ await as(PRINT_SCOPES.screen, async (screenOnly) => {
9219
+ const page = screenOnly.page(small.ref);
9220
+ const image = await page.render.image({ format: "png" });
9221
+ expect([image.width, image.height]).toEqual([200, 200]);
9222
+ await expect(page.render.image({
9223
+ format: "png",
9224
+ ...print(150)
9225
+ })).rejects.toMatchObject({ code: EngineErrorCode.Forbidden });
9226
+ });
9227
+ await as(PRINT_SCOPES.print, async (printOnly) => {
9228
+ const page = printOnly.page(small.ref);
9229
+ const image = await page.render.image({
9230
+ format: "png",
9231
+ ...print(150)
9232
+ });
9233
+ expect((await decode$2(image)).width).toBe(417);
9234
+ await expect(page.render.image({
9235
+ format: "png",
9236
+ ...print(300)
9237
+ })).rejects.toMatchObject({ code: EngineErrorCode.Forbidden });
9238
+ });
9239
+ });
9240
+ });
9241
+ }
9242
+ function runPrintCopyConformance(runner, opts) {
9243
+ const { describe, test, beforeAll, afterAll, expect } = runner;
9244
+ describe(`print copy conformance: ${opts.label}`, () => {
9245
+ let engine;
9246
+ let doc;
9247
+ let small;
9248
+ beforeAll(async () => {
9249
+ engine = await opts.makeEngine();
9250
+ doc = await opts.open(engine, PRINT_SCOPES.everything);
9251
+ small = (await doc.pages.list()).pages[0];
9252
+ });
9253
+ afterAll(async () => {
9254
+ if (doc) await doc.close();
9255
+ if (engine) await engine.destroy();
9256
+ });
9257
+ const as = async (scope, body) => {
9258
+ const restricted = await opts.open(engine, scope);
9259
+ try {
9260
+ await body(restricted);
9261
+ } finally {
9262
+ await restricted.close();
9263
+ }
9264
+ };
9265
+ /** The spots dark in a picture (made with `picture`) of the copy `options` makes. */
9266
+ const copySpots = async (source, options, picture) => {
9267
+ const bytes = await source.pages.extract([small.ref], options);
9268
+ const copy = await opts.openCopy(engine, bytes);
9269
+ try {
9270
+ const { pages } = await copy.pages.list();
9271
+ expect(pages).toHaveLength(1);
9272
+ const page = pages[0];
9273
+ const { annotations } = await copy.page(page.ref).annotations.list();
9274
+ return {
9275
+ dark: await darkSpotsOf(copy, page.ref, picture),
9276
+ annotations: annotations.length
9277
+ };
9278
+ } finally {
9279
+ await copy.close();
9280
+ }
9281
+ };
9282
+ test("a print copy, printed, shows what a print picture shows, with nothing interactive left", async () => {
9283
+ const before = (await doc.page(small.ref).annotations.list()).annotations.length;
9284
+ const all = [...ANNOTATION_SPOTS, ...FIELD_SPOTS];
9285
+ const printed = await copySpots(doc, { flatten: { usage: "print" } }, print(150));
9286
+ expect(printed.dark).toEqual(shown("print", all));
9287
+ expect(printed.annotations).toBe(0);
9288
+ expect((await doc.page(small.ref).annotations.list()).annotations.length).toBe(before);
9289
+ });
9290
+ test("a display copy shows what a screen picture shows", async () => {
9291
+ const all = [...ANNOTATION_SPOTS, ...FIELD_SPOTS];
9292
+ const shownCopy = await copySpots(doc, { flatten: {} }, {});
9293
+ expect(shownCopy.dark).toEqual(shown("display", all));
9294
+ expect(shownCopy.annotations).toBe(0);
9295
+ });
9296
+ test("includeAnnotations and includeFormFields choose what a copy paints", async () => {
9297
+ const cases = [
9298
+ {
9299
+ options: {
9300
+ includeAnnotations: false,
9301
+ includeFormFields: false
9302
+ },
9303
+ spots: []
9304
+ },
9305
+ {
9306
+ options: {
9307
+ includeAnnotations: true,
9308
+ includeFormFields: false
9309
+ },
9310
+ spots: ANNOTATION_SPOTS
9311
+ },
9312
+ {
9313
+ options: {
9314
+ includeAnnotations: false,
9315
+ includeFormFields: true
9316
+ },
9317
+ spots: FIELD_SPOTS
9318
+ }
9319
+ ];
9320
+ for (const { options, spots } of cases) {
9321
+ const { dark } = await copySpots(doc, {
9322
+ ...options,
9323
+ flatten: { usage: "print" }
9324
+ }, print(150));
9325
+ expect(dark).toEqual(shown("print", spots));
9326
+ }
9327
+ });
9328
+ test("the include options take flatten", async () => {
9329
+ for (const options of [{ includeAnnotations: false }, { includeFormFields: true }]) await expect(doc.pages.extract([small.ref], options)).rejects.toMatchObject({ code: EngineErrorCode.InvalidArg });
9330
+ });
9331
+ test("a flattened copy needs doc.download or doc.download.flattened, and paints what the caller may read", async () => {
9332
+ const reads = [
9333
+ "doc.open",
9334
+ "doc.render",
9335
+ "doc.annotate.read",
9336
+ "doc.forms.read"
9337
+ ];
9338
+ await as([...reads, "doc.download.flattened"], async (flattenedOnly) => {
9339
+ await expect(flattenedOnly.pages.extract([small.ref])).rejects.toMatchObject({ code: EngineErrorCode.Forbidden });
9340
+ const bytes = await flattenedOnly.pages.extract([small.ref], { flatten: { usage: "print" } });
9341
+ expect(Array.from(bytes.slice(0, 4))).toEqual([
9342
+ 37,
9343
+ 80,
9344
+ 68,
9345
+ 70
9346
+ ]);
9347
+ });
9348
+ await as(reads, async (noDownload) => {
9349
+ await expect(noDownload.pages.extract([small.ref], { flatten: { usage: "print" } })).rejects.toMatchObject({ code: EngineErrorCode.Forbidden });
9350
+ });
9351
+ await as([
9352
+ "doc.open",
9353
+ "doc.render",
9354
+ "doc.forms.read",
9355
+ "doc.download"
9356
+ ], async (noAnnotations) => {
9357
+ await expect(noAnnotations.pages.extract([small.ref], {
9358
+ flatten: { usage: "print" },
9359
+ includeAnnotations: true
9360
+ })).rejects.toMatchObject({ code: EngineErrorCode.Forbidden });
9361
+ const { dark } = await copySpots(noAnnotations, { flatten: { usage: "print" } }, print(150));
9362
+ expect(dark).toEqual(shown("print", FIELD_SPOTS));
9363
+ });
9364
+ });
9365
+ });
9366
+ }
9367
+ /** Which spots are dark in a picture of `ref` made with `options`. */
9368
+ async function darkSpotsOf(doc, ref, options) {
9369
+ const image = await doc.page(ref).render.image({
9370
+ format: "png",
9371
+ ...options
9372
+ });
9373
+ const raster = await decode$2(image);
9374
+ const dark = {};
9375
+ for (const spot of Object.keys(SPOTS)) {
9376
+ const { x, y } = image.transform.pageToPixels(SPOTS[spot].at);
9377
+ dark[spot] = isDark$1(pixel$1(raster, Math.floor(x), Math.floor(y)));
9378
+ }
9379
+ return dark;
9380
+ }
9381
+ /** The spots `usage` shows when a picture draws the layers and `spots`. */
9382
+ function shown(usage, spots = []) {
9383
+ const expected = {};
9384
+ for (const spot of Object.keys(SPOTS)) expected[spot] = (spot === "layerPrintOnly" || spot === "layerScreenOnly" || spots.includes(spot)) && SPOTS[spot][usage];
9385
+ return expected;
9386
+ }
9387
+ /** The options of a print picture at `dpi`. */
9388
+ function print(dpi) {
9389
+ return {
9390
+ usage: "print",
9391
+ viewport: {
9392
+ kind: "dpi",
9393
+ dpi
9394
+ }
9395
+ };
9396
+ }
9397
+ /** The image's pixels, read the way an app would on either engine: through `blob()`. */
9398
+ async function decode$2(image) {
9399
+ return decodePng(new Uint8Array(await (await image.blob()).arrayBuffer()));
9400
+ }
9401
+ function pixel$1(raster, x, y) {
9402
+ const at = (y * raster.width + x) * 4;
9403
+ const { rgba } = raster;
9404
+ return [
9405
+ rgba[at],
9406
+ rgba[at + 1],
9407
+ rgba[at + 2],
9408
+ rgba[at + 3]
9409
+ ];
9410
+ }
9411
+ function isDark$1([r, g, b]) {
9412
+ return r < 60 && g < 60 && b < 60;
9413
+ }
9414
+ //#endregion
8922
9415
  //#region src/conformance/pageSpaceFixtures.ts
8923
9416
  const rect = (left, bottom, right, top) => ({
8924
9417
  left,
@@ -11501,17 +11994,17 @@ async function openFixture$8(engine, opts) {
11501
11994
  * Attachment conformance suite. Fixture requirement: a document whose
11502
11995
  * `/EmbeddedFiles` name tree contains at least one embedded file.
11503
11996
  *
11504
- * Both attachment surfaces are optional on the contract (the
11505
- * `downloadLayer?` pattern), probed independently and skipped cleanly:
11506
- * - `doc.attachments?` — document-level EmbeddedFiles (list/download)
11507
- * - `page.annotations.downloadResource(ref, 'file')` — annotation-level file bytes
11997
+ * Both homes of a file: `doc.attachments` (the `/EmbeddedFiles` name tree)
11998
+ * and a file-attachment annotation's `file` resource
11999
+ * (`page.annotations.downloadResource(ref, 'file')`). Both downloads answer
12000
+ * the bytes; the name and type are what the listing says.
11508
12001
  *
11509
12002
  * Invariants:
11510
12003
  * 1. `list()` reflects the name tree: positional indices, non-empty
11511
12004
  * names, and repeated calls agree (a read).
11512
- * 2. `download(index)` round-trips the listed metadata and returns
11513
- * exactly `size` decoded bytes when the fixture declares one.
11514
- * 3. Unknown indices reject with an `EngineError`.
12005
+ * 2. `download(ref)` returns exactly `size` decoded bytes when the
12006
+ * fixture declares one.
12007
+ * 3. Unknown keys reject with an `EngineError`.
11515
12008
  * 4. A created file-attachment annotation round-trips its file:
11516
12009
  * metadata inline on the DTO (never bytes), bytes byte-identical
11517
12010
  * as its `file` resource.
@@ -11521,22 +12014,17 @@ function runAttachmentConformance(runner, opts) {
11521
12014
  const { describe, test, beforeAll, afterAll, expect } = runner;
11522
12015
  describe(`attachment conformance: ${opts.label}`, () => {
11523
12016
  let engine;
11524
- let docSupported = false;
11525
- let annotSupported = false;
11526
12017
  let firstPageObjectNumber;
11527
12018
  beforeAll(async () => {
11528
12019
  engine = await opts.makeEngine();
11529
12020
  const probe = await openFixture(engine, opts);
11530
- docSupported = probe.attachments !== void 0;
11531
12021
  firstPageObjectNumber = (await probe.pages.list()).pages[0].ref.objectNumber;
11532
- annotSupported = probe.page(toPageRef(firstPageObjectNumber)).annotations.downloadResource !== void 0;
11533
12022
  await probe.close();
11534
12023
  });
11535
12024
  afterAll(async () => {
11536
12025
  if (engine) await engine.destroy();
11537
12026
  });
11538
12027
  test("attachments.list() reflects the /EmbeddedFiles name tree and is a stable read", async () => {
11539
- if (!docSupported) return;
11540
12028
  const doc = await openFixture(engine, opts);
11541
12029
  try {
11542
12030
  const list = await doc.attachments.list();
@@ -11554,23 +12042,20 @@ function runAttachmentConformance(runner, opts) {
11554
12042
  await doc.close();
11555
12043
  }
11556
12044
  });
11557
- test("attachments.download() round-trips the listed metadata and decoded size", async () => {
11558
- if (!docSupported) return;
12045
+ test("attachments.download() returns the listed decoded size", async () => {
11559
12046
  const doc = await openFixture(engine, opts);
11560
12047
  try {
11561
12048
  const { attachments } = await doc.attachments.list();
11562
12049
  for (const item of attachments) {
11563
- const content = await doc.attachments.download(item.ref);
11564
- expect(content.name).toBe(item.name);
11565
- if (item.mimeType !== null) expect(content.mimeType).toBe(item.mimeType);
11566
- if (item.size !== null) expect(content.bytes.length).toBe(item.size);
12050
+ const bytes = await doc.attachments.download(item.ref);
12051
+ expect(bytes).toBeInstanceOf(Uint8Array);
12052
+ if (item.size !== null) expect(bytes.length).toBe(item.size);
11567
12053
  }
11568
12054
  } finally {
11569
12055
  await doc.close();
11570
12056
  }
11571
12057
  });
11572
12058
  test("attachments.download() rejects an unknown key", async () => {
11573
- if (!docSupported) return;
11574
12059
  const doc = await openFixture(engine, opts);
11575
12060
  try {
11576
12061
  await expect(doc.attachments.download(toAttachmentRef("conformance-no-such-key.bin"))).rejects.toBeInstanceOf(EngineError);
@@ -11579,7 +12064,6 @@ function runAttachmentConformance(runner, opts) {
11579
12064
  }
11580
12065
  });
11581
12066
  test("a File brings its name and type, and a file without a type has none", async () => {
11582
- if (!docSupported) return;
11583
12067
  const doc = await openFixture(engine, opts);
11584
12068
  try {
11585
12069
  const picked = new File([new Uint8Array([
@@ -11599,15 +12083,15 @@ function runAttachmentConformance(runner, opts) {
11599
12083
  name: "0-untyped.bin"
11600
12084
  });
11601
12085
  expect(untyped.mimeType).toBe(null);
11602
- expect((await doc.attachments.download(untyped.ref)).mimeType).toBe(null);
11603
- expect((await doc.attachments.download(fromFile.ref)).mimeType).toBe("text/csv");
12086
+ const listed = (await doc.attachments.list()).attachments;
12087
+ expect(listed.find((item) => item.ref.key === "0-untyped.bin")?.mimeType).toBe(null);
12088
+ expect(listed.find((item) => item.ref.key === "0-picked.csv")?.mimeType).toBe("text/csv");
11604
12089
  await expect(doc.attachments.create({ data: new Uint8Array([7]) })).rejects.toMatchObject({ code: EngineErrorCode.InvalidArg });
11605
12090
  } finally {
11606
12091
  await doc.close();
11607
12092
  }
11608
12093
  });
11609
12094
  test("create() and delete() round-trip the name tree; keys survive index shifts", async () => {
11610
- if (!docSupported) return;
11611
12095
  const doc = await openFixture(engine, opts);
11612
12096
  try {
11613
12097
  const { attachments: before } = await doc.attachments.list();
@@ -11637,9 +12121,9 @@ function runAttachmentConformance(runner, opts) {
11637
12121
  data,
11638
12122
  name: "0-conformance.bin"
11639
12123
  })).rejects.toBeInstanceOf(EngineError);
11640
- const content = await doc.attachments.download(created.ref);
11641
- expect(content.bytes.length).toBe(data.length);
11642
- expect(content.bytes.every((byte, i) => byte === data[i])).toBe(true);
12124
+ const bytes = await doc.attachments.download(created.ref);
12125
+ expect(bytes.length).toBe(data.length);
12126
+ expect(bytes.every((byte, i) => byte === data[i])).toBe(true);
11643
12127
  const removed = await doc.attachments.delete(created.ref);
11644
12128
  expect(Object.keys(removed)).toEqual(["meta"]);
11645
12129
  expect(deletedAttachmentOf(removed)).toEqual(created.ref);
@@ -11651,7 +12135,6 @@ function runAttachmentConformance(runner, opts) {
11651
12135
  }
11652
12136
  });
11653
12137
  test("a file-attachment annotation takes a File as it is, and never guesses a type", async () => {
11654
- if (!annotSupported) return;
11655
12138
  const doc = await openFixture(engine, opts);
11656
12139
  try {
11657
12140
  const annotations = doc.page(toPageRef(firstPageObjectNumber)).annotations;
@@ -11691,7 +12174,6 @@ function runAttachmentConformance(runner, opts) {
11691
12174
  }
11692
12175
  });
11693
12176
  test("a created file-attachment annotation round-trips its file as its file resource", async () => {
11694
- if (!annotSupported) return;
11695
12177
  const doc = await openFixture(engine, opts);
11696
12178
  try {
11697
12179
  const annotations = doc.page(toPageRef(firstPageObjectNumber)).annotations;
@@ -11743,7 +12225,6 @@ function runAttachmentConformance(runner, opts) {
11743
12225
  }
11744
12226
  });
11745
12227
  test("a created text annotation round-trips icon, color, and contents", async () => {
11746
- if (!annotSupported) return;
11747
12228
  const doc = await openFixture(engine, opts);
11748
12229
  try {
11749
12230
  const annotations = doc.page(toPageRef(firstPageObjectNumber)).annotations;
@@ -12419,6 +12900,46 @@ const box$2 = (x, y = 120) => ({
12419
12900
  width: 40,
12420
12901
  height: 30
12421
12902
  });
12903
+ /** A new outline item at `objectNumber`, last under `parent` (a number; the top level without one). */
12904
+ const outlineItem = (objectNumber, title, parent) => ({
12905
+ type: "outline.create",
12906
+ parent: parent === void 0 ? null : toOutlineItemRef(parent),
12907
+ data: {
12908
+ title,
12909
+ target: {
12910
+ kind: "goto",
12911
+ destination: {
12912
+ kind: "fit",
12913
+ page: PAGE$4
12914
+ }
12915
+ }
12916
+ },
12917
+ ...objectNumber === void 0 ? {} : { objectNumber }
12918
+ });
12919
+ /** The bytes of a file to embed. */
12920
+ const NOTES = new TextEncoder().encode("Notes for the change conformance.");
12921
+ const NOTES_REF = toAttachmentRef("notes.txt");
12922
+ /** `notes.txt` embedded in the document, with `bytes`. */
12923
+ const embedNotes = (bytes = NOTES) => ({
12924
+ type: "attachments.create",
12925
+ file: {
12926
+ name: "notes.txt",
12927
+ mimeType: "text/plain",
12928
+ description: "Meeting notes"
12929
+ },
12930
+ resources: { file: bytes }
12931
+ });
12932
+ /** A saved PDF as text, one character per byte. */
12933
+ const latin1 = (bytes) => new TextDecoder("latin1").decode(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
12934
+ /** The `/Count` of object `objectNumber` in a saved PDF, as its last version writes it. */
12935
+ const countIn = (pdf, objectNumber) => {
12936
+ const versions = [...pdf.matchAll(new RegExp(`(?:^|[\\r\\n])${objectNumber} 0 obj`, "g"))];
12937
+ const last = versions[versions.length - 1];
12938
+ if (!last) return null;
12939
+ const body = pdf.slice(last.index, pdf.indexOf("endobj", last.index));
12940
+ const count = /\/Count (-?\d+)/.exec(body);
12941
+ return count ? Number(count[1]) : null;
12942
+ };
12422
12943
  /**
12423
12944
  * A round trip for every op type. Keyed by the type, so a new op without one
12424
12945
  * doesn't compile: every op must have an exact reverse.
@@ -12673,7 +13194,88 @@ const ROUND_TRIPS = {
12673
13194
  "metadata.updateCustom": { ops: () => [{
12674
13195
  type: "metadata.updateCustom",
12675
13196
  patch: { project: "apply" }
12676
- }] }
13197
+ }] },
13198
+ "outline.create": { ops: ([item]) => [outlineItem(item, "Chapter")] },
13199
+ "outline.update": {
13200
+ setup: ([item]) => [outlineItem(item, "Chapter")],
13201
+ ops: ([item]) => [{
13202
+ type: "outline.update",
13203
+ ref: toOutlineItemRef(item),
13204
+ patch: {
13205
+ title: "Renamed",
13206
+ color: "#e11d48",
13207
+ bold: true,
13208
+ target: {
13209
+ kind: "uri",
13210
+ uri: "https://example.com/"
13211
+ }
13212
+ }
13213
+ }]
13214
+ },
13215
+ "outline.move": {
13216
+ setup: ([first, second, child]) => [
13217
+ outlineItem(first, "First"),
13218
+ outlineItem(second, "Second"),
13219
+ outlineItem(child, "Child", first)
13220
+ ],
13221
+ ops: ([, second, child]) => [{
13222
+ type: "outline.move",
13223
+ refs: [toOutlineItemRef(child)],
13224
+ parent: toOutlineItemRef(second),
13225
+ position: "end"
13226
+ }]
13227
+ },
13228
+ "outline.delete": {
13229
+ setup: ([parent, child]) => [
13230
+ outlineItem(parent, "Parent"),
13231
+ outlineItem(child, "Child", parent),
13232
+ {
13233
+ type: "outline.update",
13234
+ ref: toOutlineItemRef(parent),
13235
+ patch: { open: true }
13236
+ }
13237
+ ],
13238
+ ops: ([parent]) => [{
13239
+ type: "outline.delete",
13240
+ ref: toOutlineItemRef(parent)
13241
+ }]
13242
+ },
13243
+ "pages.rotate": { ops: () => [{
13244
+ type: "pages.rotate",
13245
+ pages: [PAGE$4, EMPTY_PAGE],
13246
+ rotation: 90
13247
+ }] },
13248
+ "pages.reorder": { ops: () => [{
13249
+ type: "pages.reorder",
13250
+ pages: [EMPTY_PAGE],
13251
+ position: "start"
13252
+ }] },
13253
+ "pages.delete": { ops: () => [{
13254
+ type: "pages.delete",
13255
+ pages: [PAGE$4]
13256
+ }] },
13257
+ "pages.insertBlank": { ops: ([page]) => [{
13258
+ type: "pages.insertBlank",
13259
+ spec: { size: {
13260
+ width: 300,
13261
+ height: 300
13262
+ } },
13263
+ position: { after: PAGE$4 },
13264
+ objectNumbers: [page]
13265
+ }] },
13266
+ "pages.insert": { ops: () => [{
13267
+ type: "pages.insert",
13268
+ source: SIGNATURE_ARTWORK_PDF,
13269
+ position: "start"
13270
+ }] },
13271
+ "attachments.create": { ops: () => [embedNotes()] },
13272
+ "attachments.delete": {
13273
+ setup: () => [embedNotes()],
13274
+ ops: () => [{
13275
+ type: "attachments.delete",
13276
+ ref: NOTES_REF
13277
+ }]
13278
+ }
12677
13279
  };
12678
13280
  /**
12679
13281
  * `doc.apply`, one user action as one change:
@@ -12716,15 +13318,18 @@ function runChangeConformance(runner, opts) {
12716
13318
  };
12717
13319
  /** Everything a change can write, as the document reads it. */
12718
13320
  const factsOf = async (doc) => {
12719
- const [onPage, onEmptyPage, form, metadata, custom] = await Promise.all([
12720
- doc.page(PAGE$4).annotations.list(),
12721
- doc.page(EMPTY_PAGE).annotations.list(),
13321
+ const pages = await doc.pages.list();
13322
+ const [annotations, form, metadata, custom, outline, attachments] = await Promise.all([
13323
+ Promise.all(pages.pages.map((page) => doc.page(page.ref).annotations.list())),
12722
13324
  doc.forms.list(),
12723
13325
  doc.metadata.get(),
12724
- doc.metadata.custom.get()
13326
+ doc.metadata.custom.get(),
13327
+ doc.outline.get(),
13328
+ doc.attachments.list()
12725
13329
  ]);
12726
13330
  return {
12727
- annotations: [onPage.annotations, onEmptyPage.annotations],
13331
+ pages,
13332
+ annotations: annotations.map((list) => list.annotations),
12728
13333
  fields: form.fields,
12729
13334
  widgets: form.widgets,
12730
13335
  calculationOrder: form.calculationOrder,
@@ -12732,7 +13337,9 @@ function runChangeConformance(runner, opts) {
12732
13337
  title: metadata.title,
12733
13338
  subject: metadata.subject
12734
13339
  },
12735
- custom
13340
+ custom,
13341
+ outline,
13342
+ attachments: attachments.attachments
12736
13343
  };
12737
13344
  };
12738
13345
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
@@ -13114,14 +13721,406 @@ function runChangeConformance(runner, opts) {
13114
13721
  expect(await colorOf(doc, FILE_SQUARE)).toBe("#e11d48");
13115
13722
  });
13116
13723
  });
13117
- test("undoing a change twice is harmless", async () => {
13724
+ const pageOrderOf = async (doc) => (await doc.pages.list()).pages.map((page) => page.ref);
13725
+ const blankPage = (objectNumber) => ({
13726
+ type: "pages.insertBlank",
13727
+ spec: { size: {
13728
+ width: 300,
13729
+ height: 300
13730
+ } },
13731
+ objectNumbers: [objectNumber]
13732
+ });
13733
+ test("the undo of a page insert is skipped when someone annotated the page since", async () => {
13118
13734
  await withDocument(async (doc) => {
13119
- const before = await factsOf(doc);
13120
- const mine = await doc.apply({ ops: [{
13121
- type: "forms.setValue",
13122
- field: NAME$3,
13123
- value: { value: "Bea" }
13124
- }] });
13735
+ const [page, square] = await take(doc, 2);
13736
+ const inserted = await doc.apply({ ops: [blankPage(page)] });
13737
+ const added = toPageRef(page);
13738
+ await doc.apply({ ops: [{
13739
+ type: "annotations.create",
13740
+ page: added,
13741
+ data: {
13742
+ subtype: "square",
13743
+ box: box$2(20)
13744
+ },
13745
+ objectNumber: square
13746
+ }] });
13747
+ const undo = await doc.apply({ undoOf: inserted.meta.opId });
13748
+ expect(isSkippedItem(itemOfType(undo, "pages.delete"))).toBe(true);
13749
+ expect(await pageOrderOf(doc)).toEqual([
13750
+ PAGE$4,
13751
+ EMPTY_PAGE,
13752
+ added
13753
+ ]);
13754
+ });
13755
+ });
13756
+ test("the undo of a page insert still applies after pages moved around it", async () => {
13757
+ await withDocument(async (doc) => {
13758
+ const [page] = await take(doc, 1);
13759
+ const inserted = await doc.apply({ ops: [blankPage(page)] });
13760
+ await doc.apply({ ops: [{
13761
+ type: "pages.reorder",
13762
+ pages: [EMPTY_PAGE],
13763
+ position: "end"
13764
+ }] });
13765
+ const undo = await doc.apply({ undoOf: inserted.meta.opId });
13766
+ expect(appliedOfType(undo, "pages.delete").pages).toEqual([toPageRef(page)]);
13767
+ expect(await pageOrderOf(doc)).toEqual([PAGE$4, EMPTY_PAGE]);
13768
+ });
13769
+ });
13770
+ test("the undo of a page move leaves a page someone moved since", async () => {
13771
+ await withDocument(async (doc) => {
13772
+ const moved = await doc.apply({ ops: [{
13773
+ type: "pages.reorder",
13774
+ pages: [EMPTY_PAGE],
13775
+ position: "start"
13776
+ }] });
13777
+ await doc.apply({ ops: [{
13778
+ type: "pages.reorder",
13779
+ pages: [EMPTY_PAGE],
13780
+ position: "end"
13781
+ }] });
13782
+ const undo = await doc.apply({ undoOf: moved.meta.opId });
13783
+ expect(isSkippedItem(itemOfType(undo, "pages.reorder"))).toBe(true);
13784
+ expect(await pageOrderOf(doc)).toEqual([PAGE$4, EMPTY_PAGE]);
13785
+ });
13786
+ });
13787
+ test("the undo of a page rotate leaves a page someone turned since", async () => {
13788
+ await withDocument(async (doc) => {
13789
+ const turned = await doc.apply({ ops: [{
13790
+ type: "pages.rotate",
13791
+ pages: [PAGE$4, EMPTY_PAGE],
13792
+ rotation: 90
13793
+ }] });
13794
+ await doc.apply({ ops: [{
13795
+ type: "pages.rotate",
13796
+ pages: [EMPTY_PAGE],
13797
+ rotation: 180
13798
+ }] });
13799
+ const undo = await doc.apply({ undoOf: turned.meta.opId });
13800
+ expect(appliedOfType(undo, "pages.rotate").pages).toEqual([PAGE$4]);
13801
+ const { pages } = await doc.pages.list();
13802
+ expect(pages.map((page) => page.rotation)).toEqual([0, 180]);
13803
+ });
13804
+ });
13805
+ test("a deleted page comes back at its place, with its name", async () => {
13806
+ await withDocument(async (doc) => {
13807
+ await doc.pages.setName({
13808
+ name: "Appendix",
13809
+ page: EMPTY_PAGE
13810
+ });
13811
+ await doc.apply({ ops: [{
13812
+ type: "pages.reorder",
13813
+ pages: [EMPTY_PAGE],
13814
+ position: "start"
13815
+ }] });
13816
+ const deleted = await doc.apply({ ops: [{
13817
+ type: "pages.delete",
13818
+ pages: [EMPTY_PAGE]
13819
+ }] });
13820
+ expect((await doc.pages.list()).namedPages).toEqual([]);
13821
+ const recorded = recordEvents(doc);
13822
+ const undo = await doc.apply({ undoOf: deleted.meta.opId });
13823
+ recorded.stop();
13824
+ expect(appliedOfType(undo, "pages.restore").pages).toEqual([EMPTY_PAGE]);
13825
+ expect(await pageOrderOf(doc)).toEqual([EMPTY_PAGE, PAGE$4]);
13826
+ expect((await doc.pages.list()).namedPages).toEqual([{
13827
+ name: "Appendix",
13828
+ target: {
13829
+ kind: "page",
13830
+ page: EMPTY_PAGE
13831
+ }
13832
+ }]);
13833
+ expect(recorded.events.map((event) => event.type)).toEqual(["pages.restored"]);
13834
+ });
13835
+ });
13836
+ test("an op meets the pages as the ops before it in the change left them", async () => {
13837
+ await withDocument(async (doc) => {
13838
+ const deleted = await doc.apply({ ops: [{
13839
+ type: "annotations.update",
13840
+ ref: FILE_SQUARE,
13841
+ patch: { color: "#e11d48" }
13842
+ }, {
13843
+ type: "pages.delete",
13844
+ pages: [PAGE$4]
13845
+ }] });
13846
+ expect(deleted.items.map((item) => item.type)).toEqual(["annotations.update", "pages.delete"]);
13847
+ expect(await pageOrderOf(doc)).toEqual([EMPTY_PAGE]);
13848
+ const [page, square] = await take(doc, 2);
13849
+ const added = toPageRef(page);
13850
+ await doc.apply({ ops: [blankPage(page), {
13851
+ type: "annotations.create",
13852
+ page: added,
13853
+ data: {
13854
+ subtype: "square",
13855
+ box: box$2(20)
13856
+ },
13857
+ objectNumber: square
13858
+ }] });
13859
+ const { annotations } = await doc.page(added).annotations.list();
13860
+ expect(annotations.map((annotation) => annotation.ref)).toEqual([at(square, added)]);
13861
+ });
13862
+ });
13863
+ test("the undo of a page insert applies after the page was turned and turned back", async () => {
13864
+ await withDocument(async (doc) => {
13865
+ const inserted = await doc.apply({ ops: [{
13866
+ type: "pages.insert",
13867
+ source: SIGNATURE_ARTWORK_PDF,
13868
+ position: "end"
13869
+ }] });
13870
+ const [added] = appliedOfType(inserted, "pages.insert").insertedPages;
13871
+ const turned = await doc.apply({ ops: [{
13872
+ type: "pages.rotate",
13873
+ pages: [added],
13874
+ rotation: 90
13875
+ }] });
13876
+ await doc.apply({ undoOf: turned.meta.opId });
13877
+ const undo = await doc.apply({ undoOf: inserted.meta.opId });
13878
+ expect(appliedOfType(undo, "pages.delete").pages).toEqual([added]);
13879
+ expect(await pageOrderOf(doc)).toEqual([PAGE$4, EMPTY_PAGE]);
13880
+ });
13881
+ });
13882
+ test("the undo of a page insert is skipped when someone turned the page since", async () => {
13883
+ await withDocument(async (doc) => {
13884
+ const inserted = await doc.apply({ ops: [{
13885
+ type: "pages.insert",
13886
+ source: SIGNATURE_ARTWORK_PDF,
13887
+ position: "end"
13888
+ }] });
13889
+ const [added] = appliedOfType(inserted, "pages.insert").insertedPages;
13890
+ await doc.apply({ ops: [{
13891
+ type: "pages.rotate",
13892
+ pages: [added],
13893
+ rotation: 90
13894
+ }] });
13895
+ const undo = await doc.apply({ undoOf: inserted.meta.opId });
13896
+ expect(isSkippedItem(itemOfType(undo, "pages.delete"))).toBe(true);
13897
+ expect(await pageOrderOf(doc)).toEqual([
13898
+ PAGE$4,
13899
+ EMPTY_PAGE,
13900
+ added
13901
+ ]);
13902
+ });
13903
+ });
13904
+ test("a page op naming no pages is refused", async () => {
13905
+ await withDocument(async (doc) => {
13906
+ for (const op of [
13907
+ {
13908
+ type: "pages.rotate",
13909
+ pages: [],
13910
+ rotation: 90
13911
+ },
13912
+ {
13913
+ type: "pages.delete",
13914
+ pages: []
13915
+ },
13916
+ {
13917
+ type: "pages.reorder",
13918
+ pages: [],
13919
+ position: "start"
13920
+ }
13921
+ ]) await expect(doc.apply({ ops: [op] })).rejects.toMatchObject({ code: EngineErrorCode.InvalidArg });
13922
+ expect(await pageOrderOf(doc)).toEqual([PAGE$4, EMPTY_PAGE]);
13923
+ });
13924
+ });
13925
+ test("the undo of opening an item counts the items above it again", async () => {
13926
+ await withDocument(async (doc) => {
13927
+ const [parent, child, grandchild] = await take(doc, 3);
13928
+ await doc.apply({ ops: [
13929
+ outlineItem(parent, "Parent"),
13930
+ outlineItem(child, "Child", parent),
13931
+ outlineItem(grandchild, "Grandchild", child),
13932
+ {
13933
+ type: "outline.update",
13934
+ ref: toOutlineItemRef(parent),
13935
+ patch: { open: true }
13936
+ }
13937
+ ] });
13938
+ const opened = await doc.apply({ ops: [{
13939
+ type: "outline.update",
13940
+ ref: toOutlineItemRef(child),
13941
+ patch: { open: true }
13942
+ }] });
13943
+ expect(countIn(latin1(await doc.download()), parent)).toBe(2);
13944
+ await doc.apply({ undoOf: opened.meta.opId });
13945
+ expect(countIn(latin1(await doc.download()), parent)).toBe(1);
13946
+ });
13947
+ });
13948
+ test("the undo of a delete or a move that emptied an open item opens it again", async () => {
13949
+ await withDocument(async (doc) => {
13950
+ const [parent, child] = await take(doc, 2);
13951
+ await doc.apply({ ops: [
13952
+ outlineItem(parent, "Parent"),
13953
+ outlineItem(child, "Child", parent),
13954
+ {
13955
+ type: "outline.update",
13956
+ ref: toOutlineItemRef(parent),
13957
+ patch: { open: true }
13958
+ }
13959
+ ] });
13960
+ const openOf = async (item) => (await doc.outline.get()).items.find((read) => read.ref.objectNumber === item)?.open;
13961
+ const deleted = await doc.apply({ ops: [{
13962
+ type: "outline.delete",
13963
+ ref: toOutlineItemRef(child)
13964
+ }] });
13965
+ expect(await openOf(parent)).toBe(false);
13966
+ const undoDelete = await doc.apply({ undoOf: deleted.meta.opId });
13967
+ expect(undoDelete.items.map((item) => item.type)).toEqual(["outline.restore", "outline.update"]);
13968
+ expect(await openOf(parent)).toBe(true);
13969
+ const moved = await doc.apply({ ops: [{
13970
+ type: "outline.move",
13971
+ refs: [toOutlineItemRef(child)],
13972
+ parent: null,
13973
+ position: "end"
13974
+ }] });
13975
+ expect(await openOf(parent)).toBe(false);
13976
+ await doc.apply({ undoOf: moved.meta.opId });
13977
+ expect(await openOf(parent)).toBe(true);
13978
+ });
13979
+ });
13980
+ test("the undo of an item made applies after the page it goes to was deleted", async () => {
13981
+ await withDocument(async (doc) => {
13982
+ const [item] = await take(doc, 1);
13983
+ const created = await doc.apply({ ops: [{
13984
+ type: "outline.create",
13985
+ parent: null,
13986
+ data: {
13987
+ title: "Appendix",
13988
+ target: {
13989
+ kind: "goto",
13990
+ destination: {
13991
+ kind: "fit",
13992
+ page: EMPTY_PAGE
13993
+ }
13994
+ }
13995
+ },
13996
+ objectNumber: item
13997
+ }] });
13998
+ await doc.apply({ ops: [{
13999
+ type: "pages.delete",
14000
+ pages: [EMPTY_PAGE]
14001
+ }] });
14002
+ const undo = await doc.apply({ undoOf: created.meta.opId });
14003
+ expect(appliedOfType(undo, "outline.delete")).toBeTruthy();
14004
+ expect((await doc.outline.get()).items).toEqual([]);
14005
+ });
14006
+ });
14007
+ test("the undo of a new target leaves it when the old one goes to a page that is gone", async () => {
14008
+ await withDocument(async (doc) => {
14009
+ const [item] = await take(doc, 1);
14010
+ const ref = toOutlineItemRef(item);
14011
+ await doc.apply({ ops: [outlineItem(item, "Chapter")] });
14012
+ const retargeted = await doc.apply({ ops: [{
14013
+ type: "outline.update",
14014
+ ref,
14015
+ patch: { target: {
14016
+ kind: "goto",
14017
+ destination: {
14018
+ kind: "fit",
14019
+ page: EMPTY_PAGE
14020
+ }
14021
+ } }
14022
+ }] });
14023
+ await doc.apply({ ops: [{
14024
+ type: "outline.update",
14025
+ ref,
14026
+ patch: { title: "Renamed" }
14027
+ }] });
14028
+ await doc.apply({ ops: [{
14029
+ type: "pages.delete",
14030
+ pages: [PAGE$4]
14031
+ }] });
14032
+ const undo = await doc.apply({ undoOf: retargeted.meta.opId });
14033
+ expect(isSkippedItem(itemOfType(undo, "outline.update"))).toBe(true);
14034
+ const [read] = (await doc.outline.get()).items;
14035
+ expect(read?.target).toEqual({
14036
+ kind: "goto",
14037
+ destination: {
14038
+ kind: "fit",
14039
+ page: EMPTY_PAGE
14040
+ }
14041
+ });
14042
+ });
14043
+ });
14044
+ test("no item is made under the deepest item a read shows", async () => {
14045
+ await withDocument(async (doc) => {
14046
+ const numbers = await take(doc, OUTLINE_READ_LIMITS.depth);
14047
+ await doc.apply({ ops: numbers.map((item, level) => outlineItem(item, `Level ${level}`, level ? numbers[level - 1] : void 0)) });
14048
+ const deepest = toOutlineItemRef(numbers[numbers.length - 1]);
14049
+ await expect(doc.apply({ ops: [{
14050
+ type: "outline.create",
14051
+ parent: deepest,
14052
+ data: { title: "Deeper" }
14053
+ }] })).rejects.toMatchObject({ code: EngineErrorCode.InvalidArg });
14054
+ const { items } = await doc.outline.get();
14055
+ expect(items).toHaveLength(OUTLINE_READ_LIMITS.depth);
14056
+ });
14057
+ });
14058
+ test("a deleted file comes back with its bytes, its description and its dates", async () => {
14059
+ await withDocument(async (doc) => {
14060
+ await doc.apply({ ops: [embedNotes()] });
14061
+ const [embedded] = (await doc.attachments.list()).attachments;
14062
+ const deleted = await doc.apply({ ops: [{
14063
+ type: "attachments.delete",
14064
+ ref: NOTES_REF
14065
+ }] });
14066
+ expect((await doc.attachments.list()).attachments).toEqual([]);
14067
+ const recorded = recordEvents(doc);
14068
+ const undo = await doc.apply({ undoOf: deleted.meta.opId });
14069
+ recorded.stop();
14070
+ expect(appliedOfType(undo, "attachments.restore").attachment).toEqual(embedded);
14071
+ expect((await doc.attachments.list()).attachments).toEqual([embedded]);
14072
+ expect(await doc.attachments.download(NOTES_REF)).toEqual(NOTES);
14073
+ expect(recorded.events.map((event) => event.type)).toEqual(["attachments.restored"]);
14074
+ });
14075
+ });
14076
+ test("the undo of a file added is skipped when someone added another under its name", async () => {
14077
+ await withDocument(async (doc) => {
14078
+ const added = await doc.apply({ ops: [embedNotes()] });
14079
+ const other = new TextEncoder().encode("Other notes.");
14080
+ await doc.apply({ ops: [{
14081
+ type: "attachments.delete",
14082
+ ref: NOTES_REF
14083
+ }] });
14084
+ await doc.apply({ ops: [embedNotes(other)] });
14085
+ const undo = await doc.apply({ undoOf: added.meta.opId });
14086
+ expect(isSkippedItem(itemOfType(undo, "attachments.delete"))).toBe(true);
14087
+ expect(await doc.attachments.download(NOTES_REF)).toEqual(other);
14088
+ });
14089
+ });
14090
+ test("the undo of a file removed is skipped when another file has its name", async () => {
14091
+ await withDocument(async (doc) => {
14092
+ await doc.apply({ ops: [embedNotes()] });
14093
+ const deleted = await doc.apply({ ops: [{
14094
+ type: "attachments.delete",
14095
+ ref: NOTES_REF
14096
+ }] });
14097
+ const other = new TextEncoder().encode("Other notes.");
14098
+ await doc.apply({ ops: [embedNotes(other)] });
14099
+ const undo = await doc.apply({ undoOf: deleted.meta.opId });
14100
+ expect(isSkippedItem(itemOfType(undo, "attachments.restore"))).toBe(true);
14101
+ expect((await doc.attachments.list()).attachments).toHaveLength(1);
14102
+ expect(await doc.attachments.download(NOTES_REF)).toEqual(other);
14103
+ });
14104
+ });
14105
+ test("a file whose name another file has is refused, and the change writes nothing", async () => {
14106
+ await withDocument(async (doc) => {
14107
+ await doc.apply({ ops: [embedNotes()] });
14108
+ const before = await factsOf(doc);
14109
+ await expect(doc.apply({ ops: [{
14110
+ type: "metadata.update",
14111
+ patch: { title: "Changed" }
14112
+ }, embedNotes()] })).rejects.toMatchObject({ code: EngineErrorCode.InvalidArg });
14113
+ expect(await factsOf(doc)).toEqual(before);
14114
+ });
14115
+ });
14116
+ test("undoing a change twice is harmless", async () => {
14117
+ await withDocument(async (doc) => {
14118
+ const before = await factsOf(doc);
14119
+ const mine = await doc.apply({ ops: [{
14120
+ type: "forms.setValue",
14121
+ field: NAME$3,
14122
+ value: { value: "Bea" }
14123
+ }] });
13125
14124
  await doc.apply({ undoOf: mine.meta.opId });
13126
14125
  const again = await doc.apply({ undoOf: mine.meta.opId });
13127
14126
  expect(again.items.every(isSkippedItem)).toBe(true);
@@ -13229,6 +14228,669 @@ function runChangeConformance(runner, opts) {
13229
14228
  expect(await factsOf(doc)).toEqual(before);
13230
14229
  });
13231
14230
  });
14231
+ test("adding and removing a file are one-op changes: their opIds undo them", async () => {
14232
+ await withDocument(async (doc) => {
14233
+ const before = await factsOf(doc);
14234
+ const picked = new File([NOTES], "notes.txt", { type: "text/plain" });
14235
+ const created = await doc.attachments.create({ data: picked });
14236
+ expect(created.attachment.ref).toEqual(NOTES_REF);
14237
+ expect(created.meta.undoable).toBe(true);
14238
+ const withFile = await factsOf(doc);
14239
+ const deleted = await doc.attachments.delete(NOTES_REF);
14240
+ expect(deleted.meta.undoable).toBe(true);
14241
+ await doc.apply({ undoOf: deleted.meta.opId });
14242
+ expect(await factsOf(doc)).toEqual(withFile);
14243
+ await doc.apply({ undoOf: created.meta.opId });
14244
+ expect(await factsOf(doc)).toEqual(before);
14245
+ });
14246
+ });
14247
+ });
14248
+ }
14249
+ //#endregion
14250
+ //#region src/conformance/runOutlineConformance.ts
14251
+ /**
14252
+ * Two pages of 300 × 300 and an outline, five items showing when it opens:
14253
+ *
14254
+ * - A (6), open: to page 3 at (20, 280) in the file; its child A1 (10) fits page 4.
14255
+ * - B (7): red and bold, a URI action.
14256
+ * - C (8), closed: the named destination `chapter-two` (page 4, `/FitH` 250);
14257
+ * its child C1 (11) goes nowhere.
14258
+ * - D (9): a JavaScript action.
14259
+ */
14260
+ const OUTLINE_FIXTURE_PDF = pdfOf([
14261
+ "<< /Type /Catalog /Pages 2 0 R /Outlines 5 0 R /Names << /Dests 12 0 R >> >>",
14262
+ "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>",
14263
+ "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>",
14264
+ "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>",
14265
+ "<< /Type /Outlines /First 6 0 R /Last 9 0 R /Count 5 >>",
14266
+ "<< /Title (A) /Parent 5 0 R /Next 7 0 R /First 10 0 R /Last 10 0 R /Count 1 /Dest [3 0 R /XYZ 20 280 null] >>",
14267
+ "<< /Title (B) /Parent 5 0 R /Prev 6 0 R /Next 8 0 R /C [1 0 0] /F 2 /A << /S /URI /URI (https://example.com/) >> >>",
14268
+ "<< /Title (C) /Parent 5 0 R /Prev 7 0 R /Next 9 0 R /First 11 0 R /Last 11 0 R /Count -1 /Dest (chapter-two) >>",
14269
+ "<< /Title (D) /Parent 5 0 R /Prev 8 0 R /A << /S /JavaScript /JS (app.alert\\(1\\)) >> >>",
14270
+ "<< /Title (A1) /Parent 6 0 R /Dest [4 0 R /Fit] >>",
14271
+ "<< /Title (C1) /Parent 8 0 R >>",
14272
+ "<< /Names [(chapter-two) [4 0 R /FitH 250]] >>"
14273
+ ]);
14274
+ /**
14275
+ * An outline a write never made: One (4) and Two (5), whose `/Next` comes
14276
+ * back to One, and after One a direct item no one can name.
14277
+ */
14278
+ const BROKEN_OUTLINE_FIXTURE_PDF = pdfOf([
14279
+ "<< /Type /Catalog /Pages 2 0 R /Outlines 3 0 R >>",
14280
+ "<< /Type /Pages /Kids [6 0 R] /Count 1 >>",
14281
+ "<< /Type /Outlines /First 4 0 R /Last 5 0 R /Count 2 >>",
14282
+ "<< /Title (One) /Parent 3 0 R /Next 5 0 R /First << /Title (Direct) >> >>",
14283
+ "<< /Title (Two) /Parent 3 0 R /Prev 4 0 R /Next 4 0 R >>",
14284
+ "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>"
14285
+ ]);
14286
+ /**
14287
+ * Targets stored every way files store them, on the same two pages:
14288
+ *
14289
+ * - Shared one (6) and Shared two (7): one action object (11), page 3 at
14290
+ * (10, 290) in the file, which the link on page 3 (14) also uses.
14291
+ * - Hyperref (8): an action written into the item, going to the named
14292
+ * destination `section.1` of the `/Names` tree (page 4 at (0, 200)).
14293
+ * - Old dests (9): the name `/chapter`, from the catalog's `/Dests`
14294
+ * dictionary, written as `<< /D [...] >>` (page 4, `/FitH` 250).
14295
+ * - By number (10): a destination naming its page by index, 1 (page 4).
14296
+ */
14297
+ const OUTLINE_TARGETS_FIXTURE_PDF = pdfOf([
14298
+ "<< /Type /Catalog /Pages 2 0 R /Outlines 5 0 R /Names << /Dests 12 0 R >> /Dests 13 0 R >>",
14299
+ "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>",
14300
+ "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Annots [14 0 R] >>",
14301
+ "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>",
14302
+ "<< /Type /Outlines /First 6 0 R /Last 10 0 R /Count 5 >>",
14303
+ "<< /Title (Shared one) /Parent 5 0 R /Next 7 0 R /A 11 0 R >>",
14304
+ "<< /Title (Shared two) /Parent 5 0 R /Prev 6 0 R /Next 8 0 R /A 11 0 R >>",
14305
+ "<< /Title (Hyperref) /Parent 5 0 R /Prev 7 0 R /Next 9 0 R /A << /S /GoTo /D (section.1) >> >>",
14306
+ "<< /Title (Old dests) /Parent 5 0 R /Prev 8 0 R /Next 10 0 R /Dest /chapter >>",
14307
+ "<< /Title (By number) /Parent 5 0 R /Prev 9 0 R /Dest [1 /Fit] >>",
14308
+ "<< /S /GoTo /D [3 0 R /XYZ 10 290 null] >>",
14309
+ "<< /Names [(section.1) [4 0 R /XYZ 0 200 null]] >>",
14310
+ "<< /chapter << /D [4 0 R /FitH 250] >> >>",
14311
+ "<< /Type /Annot /Subtype /Link /Rect [0 0 50 50] /P 3 0 R /A 11 0 R >>"
14312
+ ]);
14313
+ const A = toOutlineItemRef(6);
14314
+ const B = toOutlineItemRef(7);
14315
+ const C = toOutlineItemRef(8);
14316
+ const D = toOutlineItemRef(9);
14317
+ const A1 = toOutlineItemRef(10);
14318
+ const C1 = toOutlineItemRef(11);
14319
+ const FIRST_PAGE = toPageRef(3);
14320
+ const SECOND_PAGE = toPageRef(4);
14321
+ const SHARED_ONE = toOutlineItemRef(6);
14322
+ const SHARED_TWO = toOutlineItemRef(7);
14323
+ const HYPERREF = toOutlineItemRef(8);
14324
+ const OLD_DESTS = toOutlineItemRef(9);
14325
+ const BY_NUMBER = toOutlineItemRef(10);
14326
+ const titles = (outline) => outline.items.map((item) => item.title);
14327
+ const numberOf = (ref) => ref?.objectNumber ?? null;
14328
+ const childrenOf = (outline, parent) => outline.items.filter((item) => numberOf(item.parent) === numberOf(parent)).map((item) => item.title);
14329
+ const itemOf = (outline, ref) => outline.items.find((item) => item.ref.objectNumber === ref.objectNumber);
14330
+ /** The children a write's orders give `parent`, by object number. */
14331
+ const orderOf = (orders, parent) => orders.find((order) => numberOf(order.parent) === numberOf(parent))?.children.map((ref) => ref.objectNumber);
14332
+ /** Where the document's one link goes. */
14333
+ const linkTargetOf = async (doc) => {
14334
+ const link = (await doc.annotations.list()).annotations.find((annotation) => annotation.subtype === "link");
14335
+ return link?.subtype === "link" ? link.target : void 0;
14336
+ };
14337
+ /**
14338
+ * The document's outline (`doc.outline`), the same on every engine:
14339
+ *
14340
+ * - A read lists every item depth first, with its parent, its target read the
14341
+ * way a link's is (in page space, a named destination resolved), its `/A`
14342
+ * tree, its colour and style, and whether it opens.
14343
+ * - Writes name items by object number and places by neighbour; moves keep
14344
+ * subtrees and numbers; deletes take the items under an item with it, and
14345
+ * an undo brings the same items back.
14346
+ * - Each write publishes its `outline.*` event; without `doc.outline.modify`
14347
+ * every write is refused and reading still works.
14348
+ * - A target reads the same however the file stores it: written into the
14349
+ * item or shared, named in the `/Names` tree or the old `/Dests`, a page
14350
+ * by index. A new target or an undo never changes an action an item shares.
14351
+ * - A broken outline reads what it can and says `incomplete`.
14352
+ */
14353
+ function runOutlineConformance(runner, opts) {
14354
+ const { describe, test, beforeAll, afterAll, expect } = runner;
14355
+ describe(`outline conformance: ${opts.label}`, () => {
14356
+ let engine;
14357
+ beforeAll(async () => {
14358
+ engine = await opts.makeEngine();
14359
+ });
14360
+ afterAll(async () => {
14361
+ if (engine) await engine.destroy();
14362
+ });
14363
+ const withOutline = async (body, bytes = OUTLINE_FIXTURE_PDF) => {
14364
+ const doc = await opts.open(engine, bytes.slice());
14365
+ try {
14366
+ await body(doc);
14367
+ } finally {
14368
+ await doc.close();
14369
+ }
14370
+ };
14371
+ test("reads every item depth first, with its target, its look and whether it opens", async () => {
14372
+ await withOutline(async (doc) => {
14373
+ const outline = await doc.outline.get();
14374
+ expect(outline.incomplete).toBe(false);
14375
+ expect(titles(outline)).toEqual([
14376
+ "A",
14377
+ "A1",
14378
+ "B",
14379
+ "C",
14380
+ "C1",
14381
+ "D"
14382
+ ]);
14383
+ expect(itemOf(outline, A)).toMatchObject({
14384
+ parent: null,
14385
+ open: true,
14386
+ color: null,
14387
+ bold: false,
14388
+ italic: false,
14389
+ action: null,
14390
+ target: {
14391
+ kind: "goto",
14392
+ destination: {
14393
+ kind: "xyz",
14394
+ page: FIRST_PAGE,
14395
+ x: 20,
14396
+ y: 20
14397
+ }
14398
+ }
14399
+ });
14400
+ expect(itemOf(outline, A1)).toMatchObject({
14401
+ parent: A,
14402
+ open: false,
14403
+ target: {
14404
+ kind: "goto",
14405
+ destination: {
14406
+ kind: "fit",
14407
+ page: SECOND_PAGE
14408
+ }
14409
+ }
14410
+ });
14411
+ expect(itemOf(outline, B)).toMatchObject({
14412
+ color: "#ff0000",
14413
+ bold: true,
14414
+ target: {
14415
+ kind: "uri",
14416
+ uri: "https://example.com/"
14417
+ },
14418
+ action: {
14419
+ incomplete: false,
14420
+ root: {
14421
+ type: "uri",
14422
+ uri: "https://example.com/"
14423
+ }
14424
+ }
14425
+ });
14426
+ expect(itemOf(outline, C)).toMatchObject({
14427
+ open: false,
14428
+ target: {
14429
+ kind: "goto",
14430
+ destination: {
14431
+ kind: "fitH",
14432
+ page: SECOND_PAGE,
14433
+ y: 50
14434
+ }
14435
+ }
14436
+ });
14437
+ expect(itemOf(outline, C1)).toMatchObject({
14438
+ parent: C,
14439
+ target: null,
14440
+ action: null
14441
+ });
14442
+ expect(itemOf(outline, D)).toMatchObject({
14443
+ target: { kind: "javascript" },
14444
+ action: { root: {
14445
+ type: "javascript",
14446
+ script: "app.alert(1)"
14447
+ } }
14448
+ });
14449
+ });
14450
+ });
14451
+ test("a target reads the same however the file stores it", async () => {
14452
+ await withOutline(async (doc) => {
14453
+ const outline = await doc.outline.get();
14454
+ expect(outline.incomplete).toBe(false);
14455
+ const shared = {
14456
+ kind: "goto",
14457
+ destination: {
14458
+ kind: "xyz",
14459
+ page: FIRST_PAGE,
14460
+ x: 10,
14461
+ y: 10
14462
+ }
14463
+ };
14464
+ expect(itemOf(outline, SHARED_ONE)).toMatchObject({
14465
+ target: shared,
14466
+ action: { root: { type: "goto" } }
14467
+ });
14468
+ expect(itemOf(outline, SHARED_TWO)).toMatchObject({ target: shared });
14469
+ expect(await linkTargetOf(doc)).toMatchObject(shared);
14470
+ expect(itemOf(outline, HYPERREF)).toMatchObject({
14471
+ target: {
14472
+ kind: "goto",
14473
+ destination: {
14474
+ kind: "xyz",
14475
+ page: SECOND_PAGE,
14476
+ x: 0,
14477
+ y: 100
14478
+ }
14479
+ },
14480
+ action: { root: { type: "goto" } }
14481
+ });
14482
+ expect(itemOf(outline, OLD_DESTS)).toMatchObject({
14483
+ target: {
14484
+ kind: "goto",
14485
+ destination: {
14486
+ kind: "fitH",
14487
+ page: SECOND_PAGE,
14488
+ y: 50
14489
+ }
14490
+ },
14491
+ action: null
14492
+ });
14493
+ expect(itemOf(outline, BY_NUMBER)).toMatchObject({ target: {
14494
+ kind: "goto",
14495
+ destination: {
14496
+ kind: "fit",
14497
+ page: SECOND_PAGE
14498
+ }
14499
+ } });
14500
+ }, OUTLINE_TARGETS_FIXTURE_PDF);
14501
+ });
14502
+ test("a new target leaves an action the item shared alone, and an undo brings it back", async () => {
14503
+ await withOutline(async (doc) => {
14504
+ const before = await doc.outline.get();
14505
+ const link = await linkTargetOf(doc);
14506
+ const changed = await doc.outline.update(SHARED_ONE, { target: {
14507
+ kind: "uri",
14508
+ uri: "https://example.com/"
14509
+ } });
14510
+ expect(changed.item.target).toEqual({
14511
+ kind: "uri",
14512
+ uri: "https://example.com/"
14513
+ });
14514
+ const after = await doc.outline.get();
14515
+ expect(itemOf(after, SHARED_TWO)).toEqual(itemOf(before, SHARED_TWO));
14516
+ expect(await linkTargetOf(doc)).toEqual(link);
14517
+ await doc.apply({ undoOf: changed.meta.opId });
14518
+ expect(await doc.outline.get()).toEqual(before);
14519
+ }, OUTLINE_TARGETS_FIXTURE_PDF);
14520
+ });
14521
+ test("a delete and its undo keep a target however the file stored it", async () => {
14522
+ await withOutline(async (doc) => {
14523
+ const before = await doc.outline.get();
14524
+ const link = await linkTargetOf(doc);
14525
+ for (const ref of [
14526
+ SHARED_TWO,
14527
+ HYPERREF,
14528
+ OLD_DESTS,
14529
+ BY_NUMBER
14530
+ ]) {
14531
+ const deleted = await doc.outline.delete(ref);
14532
+ expect(itemOf(await doc.outline.get(), ref)).toBe(void 0);
14533
+ await doc.apply({ undoOf: deleted.meta.opId });
14534
+ expect(await doc.outline.get()).toEqual(before);
14535
+ }
14536
+ expect(await linkTargetOf(doc)).toEqual(link);
14537
+ }, OUTLINE_TARGETS_FIXTURE_PDF);
14538
+ });
14539
+ test("a document without an outline reads an empty one", async () => {
14540
+ const bytes = pdfOf([
14541
+ "<< /Type /Catalog /Pages 2 0 R >>",
14542
+ "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
14543
+ "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>"
14544
+ ]);
14545
+ await withOutline(async (doc) => {
14546
+ expect(await doc.outline.get()).toEqual({
14547
+ items: [],
14548
+ incomplete: false
14549
+ });
14550
+ await doc.outline.create({ title: "First" });
14551
+ expect(titles(await doc.outline.get())).toEqual(["First"]);
14552
+ }, bytes);
14553
+ });
14554
+ test("a create goes to its place, under its parent, at the number it names", async () => {
14555
+ await withOutline(async (doc) => {
14556
+ await doc.objectNumbers.reserve(1);
14557
+ const number = doc.objectNumbers.take();
14558
+ const first = await doc.outline.create({
14559
+ title: "A0",
14560
+ target: {
14561
+ kind: "goto",
14562
+ destination: {
14563
+ kind: "fit",
14564
+ page: FIRST_PAGE
14565
+ }
14566
+ }
14567
+ }, {
14568
+ parent: A,
14569
+ position: "start",
14570
+ objectNumber: number
14571
+ });
14572
+ expect(first.item).toMatchObject({
14573
+ ref: toOutlineItemRef(number),
14574
+ parent: A,
14575
+ title: "A0"
14576
+ });
14577
+ expect(first.order).toEqual({
14578
+ parent: A,
14579
+ children: [toOutlineItemRef(number), A1]
14580
+ });
14581
+ const after = await doc.outline.create({
14582
+ title: "B2",
14583
+ color: "#00ff00"
14584
+ }, { position: { after: B } });
14585
+ expect(after.order.parent).toBeNull();
14586
+ expect(after.order.children.map((ref) => ref.objectNumber)).toEqual([
14587
+ 6,
14588
+ 7,
14589
+ after.item.ref.objectNumber,
14590
+ 8,
14591
+ 9
14592
+ ]);
14593
+ const outline = await doc.outline.get();
14594
+ expect(titles(outline)).toEqual([
14595
+ "A",
14596
+ "A0",
14597
+ "A1",
14598
+ "B",
14599
+ "B2",
14600
+ "C",
14601
+ "C1",
14602
+ "D"
14603
+ ]);
14604
+ expect(itemOf(outline, after.item.ref)).toMatchObject({
14605
+ color: "#00ff00",
14606
+ target: null
14607
+ });
14608
+ });
14609
+ });
14610
+ test("an update writes the fields it names and leaves the others", async () => {
14611
+ await withOutline(async (doc) => {
14612
+ const renamed = await doc.outline.update(B, {
14613
+ title: "Bee",
14614
+ italic: true
14615
+ });
14616
+ expect(renamed.item).toMatchObject({
14617
+ title: "Bee",
14618
+ italic: true,
14619
+ bold: true,
14620
+ color: "#ff0000",
14621
+ target: { kind: "uri" }
14622
+ });
14623
+ const cleared = await doc.outline.update(B, {
14624
+ color: null,
14625
+ target: null,
14626
+ bold: false
14627
+ });
14628
+ expect(cleared.item).toMatchObject({
14629
+ color: null,
14630
+ target: null,
14631
+ bold: false,
14632
+ italic: true
14633
+ });
14634
+ const retargeted = await doc.outline.update(C1, { target: {
14635
+ kind: "goto",
14636
+ destination: {
14637
+ kind: "xyz",
14638
+ page: SECOND_PAGE,
14639
+ x: 10,
14640
+ y: 40
14641
+ }
14642
+ } });
14643
+ expect(retargeted.item.target).toMatchObject({
14644
+ kind: "goto",
14645
+ destination: {
14646
+ kind: "xyz",
14647
+ page: SECOND_PAGE,
14648
+ x: 10,
14649
+ y: 40
14650
+ }
14651
+ });
14652
+ expect(retargeted.item.action).toMatchObject({ root: { type: "goto" } });
14653
+ });
14654
+ });
14655
+ test("open takes effect on an item with children; a leaf has no open state", async () => {
14656
+ await withOutline(async (doc) => {
14657
+ expect((await doc.outline.update(C, { open: true })).item.open).toBe(true);
14658
+ expect((await doc.outline.update(A, { open: false })).item.open).toBe(false);
14659
+ expect((await doc.outline.update(D, { open: true })).item.open).toBe(false);
14660
+ });
14661
+ });
14662
+ test("a write answers what the prediction rules say it leaves", async () => {
14663
+ await withOutline(async (doc) => {
14664
+ const drafts = [
14665
+ { title: "Plain" },
14666
+ {
14667
+ title: "To a spot",
14668
+ target: {
14669
+ kind: "goto",
14670
+ destination: {
14671
+ kind: "xyz",
14672
+ page: SECOND_PAGE,
14673
+ x: 10,
14674
+ y: 40,
14675
+ zoom: null
14676
+ }
14677
+ },
14678
+ color: "#00AAFF",
14679
+ bold: true
14680
+ },
14681
+ {
14682
+ title: "To a site",
14683
+ target: {
14684
+ kind: "uri",
14685
+ uri: "https://example.com/"
14686
+ },
14687
+ italic: true
14688
+ },
14689
+ {
14690
+ title: "Next",
14691
+ target: {
14692
+ kind: "named",
14693
+ name: "NextPage"
14694
+ }
14695
+ },
14696
+ {
14697
+ title: "Fit",
14698
+ target: {
14699
+ kind: "goto",
14700
+ destination: {
14701
+ kind: "fit",
14702
+ page: FIRST_PAGE
14703
+ }
14704
+ }
14705
+ }
14706
+ ];
14707
+ for (const draft of drafts) {
14708
+ const { item } = await doc.outline.create(draft, { parent: C });
14709
+ expect(item).toEqual(outlineItemOfDraft(item.ref, C, draft));
14710
+ }
14711
+ const before = itemOf(await doc.outline.get(), B);
14712
+ const patches = [
14713
+ {
14714
+ title: "Bee",
14715
+ italic: true
14716
+ },
14717
+ { color: "#ABCDEF" },
14718
+ {
14719
+ color: null,
14720
+ target: null,
14721
+ bold: false
14722
+ },
14723
+ { target: {
14724
+ kind: "named",
14725
+ name: "LastPage"
14726
+ } },
14727
+ { open: true }
14728
+ ];
14729
+ let current = before;
14730
+ for (const patch of patches) {
14731
+ const { item } = await doc.outline.update(B, patch);
14732
+ expect(item).toEqual(outlineItemAfter(current, patch, false));
14733
+ current = item;
14734
+ }
14735
+ const parent = itemOf(await doc.outline.get(), C);
14736
+ const { item: opened } = await doc.outline.update(C, { open: true });
14737
+ expect(opened).toEqual(outlineItemAfter(parent, { open: true }, true));
14738
+ });
14739
+ });
14740
+ test("a move takes items together with the items under them, and names both parents", async () => {
14741
+ await withOutline(async (doc) => {
14742
+ const moved = await doc.outline.move([C, D], A, { after: A1 });
14743
+ expect(moved.meta.changed).toEqual([C, D]);
14744
+ expect(orderOf(moved.orders, null)).toEqual([6, 7]);
14745
+ expect(orderOf(moved.orders, A)).toEqual([
14746
+ 10,
14747
+ 8,
14748
+ 9
14749
+ ]);
14750
+ const outline = await doc.outline.get();
14751
+ expect(titles(outline)).toEqual([
14752
+ "A",
14753
+ "A1",
14754
+ "C",
14755
+ "C1",
14756
+ "D",
14757
+ "B"
14758
+ ]);
14759
+ expect(itemOf(outline, C)).toMatchObject({
14760
+ parent: A,
14761
+ open: false
14762
+ });
14763
+ expect(itemOf(outline, C1)).toMatchObject({ parent: C });
14764
+ await doc.outline.move([C], null, "start");
14765
+ expect(childrenOf(await doc.outline.get(), null)).toEqual([
14766
+ "C",
14767
+ "A",
14768
+ "B"
14769
+ ]);
14770
+ });
14771
+ });
14772
+ test("a move or a create that can not be made is refused", async () => {
14773
+ await withOutline(async (doc) => {
14774
+ const invalid = { code: EngineErrorCode.InvalidArg };
14775
+ const notFound = { code: EngineErrorCode.NotFound };
14776
+ await expect(doc.outline.move([A], A1, "end")).rejects.toMatchObject(invalid);
14777
+ await expect(doc.outline.move([A], A, "end")).rejects.toMatchObject(invalid);
14778
+ await expect(doc.outline.move([A, B], null, { after: B })).rejects.toMatchObject(invalid);
14779
+ await expect(doc.outline.move([A, A], null, "end")).rejects.toMatchObject(invalid);
14780
+ await expect(doc.outline.move([A, A1], null, "end")).rejects.toMatchObject(invalid);
14781
+ await expect(doc.outline.move([A], null, { after: C1 })).rejects.toMatchObject(notFound);
14782
+ await expect(doc.outline.move([toOutlineItemRef(99)], null, "end")).rejects.toMatchObject(notFound);
14783
+ await expect(doc.outline.create({ title: "x" }, { parent: toOutlineItemRef(3) })).rejects.toMatchObject(notFound);
14784
+ expect(titles(await doc.outline.get())).toEqual([
14785
+ "A",
14786
+ "A1",
14787
+ "B",
14788
+ "C",
14789
+ "C1",
14790
+ "D"
14791
+ ]);
14792
+ });
14793
+ });
14794
+ test("a delete takes the items under the item, and an undo brings the same ones back", async () => {
14795
+ await withOutline(async (doc) => {
14796
+ const before = await doc.outline.get();
14797
+ const deleted = await doc.outline.delete(C);
14798
+ expect(deleted.meta.changed).toEqual([C, C1]);
14799
+ expect(titles(await doc.outline.get())).toEqual([
14800
+ "A",
14801
+ "A1",
14802
+ "B",
14803
+ "D"
14804
+ ]);
14805
+ const undo = await doc.apply({ undoOf: deleted.meta.opId });
14806
+ expect(undo.items.map((item) => item.type)).toEqual(["outline.restore"]);
14807
+ expect(await doc.outline.get()).toEqual(before);
14808
+ });
14809
+ });
14810
+ test("expect refuses a change whose assumption does not hold", async () => {
14811
+ await withOutline(async (doc) => {
14812
+ await expect(doc.apply({ ops: [{
14813
+ type: "outline.update",
14814
+ ref: B,
14815
+ patch: { title: "Bee" },
14816
+ expect: { title: "Not B" }
14817
+ }] })).rejects.toMatchObject({ code: EngineErrorCode.ChangeConflict });
14818
+ expect(itemOf(await doc.outline.get(), B)?.title).toBe("B");
14819
+ });
14820
+ });
14821
+ test("each write publishes its outline event, with its result", async () => {
14822
+ await withOutline(async (doc) => {
14823
+ const events = [];
14824
+ const stop = doc.events.subscribe((event) => events.push(event));
14825
+ let created;
14826
+ try {
14827
+ created = (await doc.outline.create({ title: "New" }, { parent: C })).item.ref;
14828
+ await doc.outline.update(created, { title: "Newer" });
14829
+ await doc.outline.move([created], null, "end");
14830
+ await doc.outline.delete(created);
14831
+ } finally {
14832
+ stop();
14833
+ }
14834
+ const outline = events.filter((event) => event.type.startsWith("outline."));
14835
+ expect(outline.map((event) => event.type)).toEqual([
14836
+ "outline.created",
14837
+ "outline.updated",
14838
+ "outline.moved",
14839
+ "outline.deleted"
14840
+ ]);
14841
+ expect(outline[0]).toMatchObject({ item: {
14842
+ title: "New",
14843
+ parent: C
14844
+ } });
14845
+ expect(outline[1]).toMatchObject({ item: { title: "Newer" } });
14846
+ expect(outline[2]).toMatchObject({ meta: { changed: [created] } });
14847
+ expect(outline[3]).toMatchObject({ deleted: [created] });
14848
+ });
14849
+ });
14850
+ test("without doc.outline.modify every write is refused; reading still works", async () => {
14851
+ const doc = await opts.openReadOnly(engine, OUTLINE_FIXTURE_PDF.slice());
14852
+ try {
14853
+ expect(titles(await doc.outline.get())).toEqual([
14854
+ "A",
14855
+ "A1",
14856
+ "B",
14857
+ "C",
14858
+ "C1",
14859
+ "D"
14860
+ ]);
14861
+ const denied = { code: EngineErrorCode.Forbidden };
14862
+ await expect(doc.outline.create({ title: "x" })).rejects.toMatchObject(denied);
14863
+ await expect(doc.outline.update(A, { title: "x" })).rejects.toMatchObject(denied);
14864
+ await expect(doc.outline.move([A], null, "end")).rejects.toMatchObject(denied);
14865
+ await expect(doc.outline.delete(A)).rejects.toMatchObject(denied);
14866
+ } finally {
14867
+ await doc.close();
14868
+ }
14869
+ });
14870
+ test("an item whose page is gone keeps its place and goes nowhere it can be followed", async () => {
14871
+ await withOutline(async (doc) => {
14872
+ await doc.pages.delete([FIRST_PAGE]);
14873
+ const outline = await doc.outline.get();
14874
+ expect(titles(outline)).toEqual([
14875
+ "A",
14876
+ "A1",
14877
+ "B",
14878
+ "C",
14879
+ "C1",
14880
+ "D"
14881
+ ]);
14882
+ expect(itemOf(outline, A)?.target).toEqual({ kind: "unsupported" });
14883
+ });
14884
+ });
14885
+ test("a broken outline reads what it can and says incomplete", async () => {
14886
+ await withOutline(async (doc) => {
14887
+ const outline = await doc.outline.get();
14888
+ expect(outline.incomplete).toBe(true);
14889
+ expect(titles(outline)).toEqual(["One", "Two"]);
14890
+ await doc.outline.update(toOutlineItemRef(5), { title: "Second" });
14891
+ expect(titles(await doc.outline.get())).toEqual(["One", "Second"]);
14892
+ }, BROKEN_OUTLINE_FIXTURE_PDF);
14893
+ });
13232
14894
  });
13233
14895
  }
13234
14896
  //#endregion
@@ -19202,6 +20864,6 @@ function runFieldGroupsConformance(runner, opts) {
19202
20864
  });
19203
20865
  }
19204
20866
  //#endregion
19205
- export { APPEARANCE_STATES_FIXTURE_PDF, BARE_PAGE_FIXTURE_PDF, CHANGE_FIXTURE_PDF, COLOR_FIXTURE_PDF, CROP_OFFSET_PDF, DRAWING_FIXTURE_PDF, FIELD_GROUPS_PDF, FIELD_GROUP_TOKENS, OBJECT_NUMBER_FIXTURE_PDF, PAGE_SPACE_FIXTURES, PERMISSION_TOKENS, PREDICTION_FIXTURE_PDF, TEXT_DIVERGENCE_CASES, TURNED_FIELDS_PDF, TWO_PAGES_PDF, WIDGET_FINDING_CASES, annotationReadDriftOf, creatables, diffAnnotationList, iconRect, pdfOf, runActionsConformance, runAnnotationAppearanceConformance, runAnnotationAppearanceExportConformance, runAnnotationAttributionConformance, runAnnotationDeclarationConformance, runAnnotationExportConformance, runAnnotationFlattenConformance, runAnnotationImportConformance, runAnnotationMutationConformance, runAnnotationPredictionConformance, runAnnotationReadConformance, runAnnotationResourceConformance, runAnnotationRotationConformance, runAnnotationTransferConformance, runAppearanceStatesConformance, runAttachmentConformance, runChangeConformance, runColorConformance, runDateConformance, runDocumentEventsConformance, runDrawingDetailsConformance, runFieldGroupsConformance, runFormAttributionConformance, runFormConformance, runFormPredictionConformance, runFormScriptConformance, runFormTransferConformance, runMeasurementConformance, runMetadataConformance, runNamedPagesConformance, runObjectNumberConformance, runPageDeleteConformance, runPageExtractConformance, runPageFlattenConformance, runPageGeometryOrientationConformance, runPageInsertBlankConformance, runPageInsertConformance, runPageRenderConformance, runPageReorderConformance, runPageRotateConformance, runPageSpaceConformance, runPageTextConformance, runPermissionConformance, runPieceInfoConformance, runRedactionApplyConformance, runSearchConformance, runSignatureConformance, runTextDivergenceConformance, runWidgetFindingConformance, runWidgetRotationConformance };
20867
+ export { APPEARANCE_STATES_FIXTURE_PDF, BARE_PAGE_FIXTURE_PDF, BROKEN_OUTLINE_FIXTURE_PDF, CHANGE_FIXTURE_PDF, COLOR_FIXTURE_PDF, CROP_OFFSET_PDF, DRAWING_FIXTURE_PDF, FIELD_GROUPS_PDF, FIELD_GROUP_TOKENS, OBJECT_NUMBER_FIXTURE_PDF, OUTLINE_FIXTURE_PDF, PAGE_SPACE_FIXTURES, PERMISSION_TOKENS, PREDICTION_FIXTURE_PDF, PRINT_RULES_PDF, PRINT_SCOPES, TEXT_DIVERGENCE_CASES, TURNED_FIELDS_PDF, TWO_PAGES_PDF, WIDGET_FINDING_CASES, annotationReadDriftOf, creatables, diffAnnotationList, iconRect, pdfOf, runActionsConformance, runAnnotationAppearanceConformance, runAnnotationAppearanceExportConformance, runAnnotationAttributionConformance, runAnnotationDeclarationConformance, runAnnotationExportConformance, runAnnotationFlattenConformance, runAnnotationImportConformance, runAnnotationMutationConformance, runAnnotationPredictionConformance, runAnnotationReadConformance, runAnnotationResourceConformance, runAnnotationRotationConformance, runAnnotationTransferConformance, runAppearanceStatesConformance, runAttachmentConformance, runChangeConformance, runColorConformance, runDateConformance, runDocumentEventsConformance, runDrawingDetailsConformance, runFieldGroupsConformance, runFormAttributionConformance, runFormConformance, runFormPredictionConformance, runFormScriptConformance, runFormTransferConformance, runMeasurementConformance, runMetadataConformance, runNamedPagesConformance, runObjectNumberConformance, runOutlineConformance, runPageDeleteConformance, runPageExtractConformance, runPageFlattenConformance, runPageGeometryOrientationConformance, runPageInsertBlankConformance, runPageInsertConformance, runPageRenderConformance, runPageReorderConformance, runPageRotateConformance, runPageSpaceConformance, runPageTextConformance, runPermissionConformance, runPieceInfoConformance, runPrintCopyConformance, runPrintRenderConformance, runRedactionApplyConformance, runSearchConformance, runSignatureConformance, runTextDivergenceConformance, runWidgetFindingConformance, runWidgetRotationConformance };
19206
20868
 
19207
20869
  //# sourceMappingURL=conformance.js.map