@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.
- package/dist/{AnnotationList-CkijRHet.d.cts → AnnotationList-DcyhF2kU.d.cts} +1836 -1308
- package/dist/{AnnotationList-CkijRHet.d.ts → AnnotationList-DcyhF2kU.d.ts} +1836 -1308
- package/dist/{Change-CjImsrN0.cjs → Change-Ct-dB8D5.cjs} +115 -2
- package/dist/Change-Ct-dB8D5.cjs.map +1 -0
- package/dist/{Change-CKoxfTNY.js → Change-tMR34hTU.js} +92 -3
- package/dist/Change-tMR34hTU.js.map +1 -0
- package/dist/{DocumentRenderService-BfFdFSwI.cjs → DocumentRenderService-9IjSQEIU.cjs} +2 -1
- package/dist/{DocumentRenderService-BfFdFSwI.cjs.map → DocumentRenderService-9IjSQEIU.cjs.map} +1 -1
- package/dist/{DocumentRenderService-DV3J9lMz.js → DocumentRenderService-DFrZ9wfW.js} +2 -1
- package/dist/{DocumentRenderService-DV3J9lMz.js.map → DocumentRenderService-DFrZ9wfW.js.map} +1 -1
- package/dist/{DocumentSecurityService-e6vp2de8.d.cts → DocumentSecurityService-D2zA4NKi.d.cts} +2 -2
- package/dist/{DocumentSecurityService-CU1M6Q7m.d.ts → DocumentSecurityService-YJV9df8P.d.ts} +2 -2
- package/dist/{LocalEngine-DFoBm4B3.d.cts → LocalEngine-DBt2-GWF.d.ts} +102 -18
- package/dist/{LocalEngine-DPriFujS.d.ts → LocalEngine-IA9zh3Jh.d.cts} +102 -18
- package/dist/{authority-DLMNR7ff.cjs → authority-BFp6XM04.cjs} +373 -52
- package/dist/authority-BFp6XM04.cjs.map +1 -0
- package/dist/{authority-B3BklsB4.js → authority-PuxpGFhJ.js} +320 -53
- package/dist/authority-PuxpGFhJ.js.map +1 -0
- package/dist/{builders-CNj0POeY.js → builders-DV62HA3I.js} +6 -2
- package/dist/builders-DV62HA3I.js.map +1 -0
- package/dist/{builders-Cgdp_YCn.cjs → builders-DgTAj_tP.cjs} +6 -2
- package/dist/builders-DgTAj_tP.cjs.map +1 -0
- package/dist/{calibrate-DJBXnhLE.cjs → calibrate-DJbi1wuK.cjs} +118 -25
- package/dist/calibrate-DJbi1wuK.cjs.map +1 -0
- package/dist/{calibrate-7T3fnP8-.js → calibrate-nQFgCuki.js} +83 -26
- package/dist/calibrate-nQFgCuki.js.map +1 -0
- package/dist/charmap-Cprti1ta.js.map +1 -1
- package/dist/charmap-aYlefQP9.cjs.map +1 -1
- package/dist/conformance.cjs +1744 -75
- package/dist/conformance.cjs.map +1 -1
- package/dist/conformance.d.cts +105 -10
- package/dist/conformance.d.ts +105 -10
- package/dist/conformance.js +1738 -76
- package/dist/conformance.js.map +1 -1
- package/dist/index.cjs +25 -6
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -7
- package/dist/public.cjs +6 -4
- package/dist/public.d.cts +5 -5
- package/dist/public.d.ts +5 -5
- package/dist/public.js +5 -5
- package/dist/{resolver-CzxmD9Ck.js → resolver-BBggAxQc.js} +6 -3
- package/dist/resolver-BBggAxQc.js.map +1 -0
- package/dist/{resolver-D2OCtE8F.cjs → resolver-BWSWSF6R.cjs} +6 -3
- package/dist/resolver-BWSWSF6R.cjs.map +1 -0
- package/dist/{runtime-Cl0v56mm.d.ts → runtime-BaDKjxiu.d.ts} +67 -140
- package/dist/{runtime-p1WDzDo4.d.cts → runtime-CWEuiwbk.d.cts} +67 -140
- package/dist/runtime.cjs +267 -7
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +5 -5
- package/dist/runtime.d.ts +5 -5
- package/dist/runtime.js +246 -8
- package/dist/runtime.js.map +1 -1
- package/dist/{schema-DzlUNk38.js → schema-6S2VmlAj.js} +8 -2
- package/dist/schema-6S2VmlAj.js.map +1 -0
- package/dist/{schema-C-gU4vj7.cjs → schema-miH6tWQC.cjs} +13 -1
- package/dist/schema-miH6tWQC.cjs.map +1 -0
- package/dist/{schemas-D6xOGjqg.js → schemas-BE2QGOit.js} +256 -59
- package/dist/schemas-BE2QGOit.js.map +1 -0
- package/dist/{schemas-Berf1swF.cjs → schemas-DljJR9ut.cjs} +397 -80
- package/dist/schemas-DljJR9ut.cjs.map +1 -0
- package/dist/validation.cjs +1 -1
- package/dist/validation.d.cts +1 -1
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +1 -1
- package/dist/{viewport-DlL5Bxbg.cjs → viewport-CHG9qQUb.cjs} +2 -2
- package/dist/{viewport-DlL5Bxbg.cjs.map → viewport-CHG9qQUb.cjs.map} +1 -1
- package/dist/{viewport-BJW13EGZ.js → viewport-DQR5LyX3.js} +2 -2
- package/dist/{viewport-BJW13EGZ.js.map → viewport-DQR5LyX3.js.map} +1 -1
- package/dist/wire.cjs +225 -57
- package/dist/wire.cjs.map +1 -1
- package/dist/wire.d.cts +3548 -1151
- package/dist/wire.d.ts +3548 -1151
- package/dist/wire.js +198 -53
- package/dist/wire.js.map +1 -1
- package/package.json +1 -1
- package/dist/Change-CKoxfTNY.js.map +0 -1
- package/dist/Change-CjImsrN0.cjs.map +0 -1
- package/dist/authority-B3BklsB4.js.map +0 -1
- package/dist/authority-DLMNR7ff.cjs.map +0 -1
- package/dist/builders-CNj0POeY.js.map +0 -1
- package/dist/builders-Cgdp_YCn.cjs.map +0 -1
- package/dist/calibrate-7T3fnP8-.js.map +0 -1
- package/dist/calibrate-DJBXnhLE.cjs.map +0 -1
- package/dist/resolver-CzxmD9Ck.js.map +0 -1
- package/dist/resolver-D2OCtE8F.cjs.map +0 -1
- package/dist/schema-C-gU4vj7.cjs.map +0 -1
- package/dist/schema-DzlUNk38.js.map +0 -1
- package/dist/schemas-Berf1swF.cjs.map +0 -1
- package/dist/schemas-D6xOGjqg.js.map +0 -1
package/dist/conformance.cjs
CHANGED
|
@@ -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-
|
|
9
|
-
const require_schemas = require("./schemas-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
8726
|
-
expect(isDark$
|
|
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$
|
|
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$
|
|
8742
|
-
const beside = await decode$
|
|
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$
|
|
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$
|
|
8775
|
-
const [r, g, b, a] = pixel$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
8902
|
+
async function decode$3(image) {
|
|
8903
8903
|
return decodePng(new Uint8Array(await (await image.blob()).arrayBuffer()));
|
|
8904
8904
|
}
|
|
8905
|
-
function pixel$
|
|
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$
|
|
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$
|
|
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
|
|
11506
|
-
*
|
|
11507
|
-
*
|
|
11508
|
-
*
|
|
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(
|
|
11514
|
-
*
|
|
11515
|
-
* 3. Unknown
|
|
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()
|
|
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
|
|
11565
|
-
expect(
|
|
11566
|
-
if (item.
|
|
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
|
-
|
|
11604
|
-
expect((
|
|
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
|
|
11642
|
-
expect(
|
|
11643
|
-
expect(
|
|
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
|
|
12721
|
-
|
|
12722
|
-
doc.page(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
13121
|
-
const
|
|
13122
|
-
|
|
13123
|
-
|
|
13124
|
-
|
|
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;
|