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