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