fabricjs-document-engine 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +39 -1
  2. package/README.zh-CN.md +39 -1
  3. package/dist/assets/asset-pipeline.cjs +10 -8
  4. package/dist/assets/asset-pipeline.d.cts +1 -1
  5. package/dist/assets/asset-pipeline.d.ts +1 -1
  6. package/dist/assets/asset-pipeline.js +10 -8
  7. package/dist/assets/image-check.cjs +4 -3
  8. package/dist/assets/image-check.d.cts +1 -1
  9. package/dist/assets/image-check.d.ts +1 -1
  10. package/dist/assets/image-check.js +4 -3
  11. package/dist/commands/clipboard.cjs +5 -0
  12. package/dist/commands/clipboard.js +5 -0
  13. package/dist/document/document-format.d.cts +15 -0
  14. package/dist/document/document-format.d.ts +15 -0
  15. package/dist/document/validate-document.cjs +8 -0
  16. package/dist/document/validate-document.js +8 -0
  17. package/dist/engine/create-document-engine.cjs +115 -21
  18. package/dist/engine/create-document-engine.d.cts +24 -6
  19. package/dist/engine/create-document-engine.d.ts +24 -6
  20. package/dist/engine/create-document-engine.js +116 -22
  21. package/dist/engine/errors.d.cts +1 -1
  22. package/dist/engine/errors.d.ts +1 -1
  23. package/dist/export/export-options.cjs +3 -0
  24. package/dist/export/export-options.d.cts +6 -0
  25. package/dist/export/export-options.d.ts +6 -0
  26. package/dist/export/export-options.js +3 -0
  27. package/dist/export/preflight-export.d.cts +1 -1
  28. package/dist/export/preflight-export.d.ts +1 -1
  29. package/dist/export/render-export.cjs +42 -4
  30. package/dist/export/render-export.js +43 -5
  31. package/dist/export/svg/clip-masks.cjs +39 -0
  32. package/dist/export/svg/clip-masks.js +37 -0
  33. package/dist/export/svg/embed-assets.cjs +1 -0
  34. package/dist/export/svg/embed-assets.js +1 -0
  35. package/dist/export/svg/raster-object.cjs +82 -0
  36. package/dist/export/svg/raster-object.js +81 -0
  37. package/dist/{pdf → export/svg}/text-decorations.cjs +3 -3
  38. package/dist/{pdf → export/svg}/text-decorations.js +3 -3
  39. package/dist/export/svg/text-on-path.cjs +0 -10
  40. package/dist/export/svg/text-on-path.js +1 -10
  41. package/dist/fabric/fabric-adapter.cjs +5 -3
  42. package/dist/fabric/fabric-adapter.js +5 -3
  43. package/dist/fabric/page-state.cjs +69 -0
  44. package/dist/fabric/page-state.js +64 -0
  45. package/dist/history/apply-state.cjs +19 -2
  46. package/dist/history/apply-state.js +19 -2
  47. package/dist/history/create-history.cjs +53 -13
  48. package/dist/history/create-history.d.cts +8 -0
  49. package/dist/history/create-history.d.ts +8 -0
  50. package/dist/history/create-history.js +53 -13
  51. package/dist/history/snapshot.cjs +10 -1
  52. package/dist/history/snapshot.js +10 -2
  53. package/dist/index.d.cts +3 -3
  54. package/dist/index.d.ts +3 -3
  55. package/dist/migrations/migrate-document.cjs +2 -1
  56. package/dist/migrations/migrate-document.js +2 -1
  57. package/dist/pdf/export-pdf.cjs +8 -67
  58. package/dist/pdf/export-pdf.js +8 -67
  59. package/dist/recovery/recovery-controller.cjs +148 -29
  60. package/dist/recovery/recovery-controller.d.cts +8 -0
  61. package/dist/recovery/recovery-controller.d.ts +8 -0
  62. package/dist/recovery/recovery-controller.js +148 -29
  63. package/dist/security/content-limits.cjs +43 -3
  64. package/dist/security/content-limits.d.cts +8 -0
  65. package/dist/security/content-limits.d.ts +8 -0
  66. package/dist/security/content-limits.js +39 -4
  67. package/dist/util/concurrency.cjs +16 -3
  68. package/dist/util/concurrency.js +16 -3
  69. package/package.json +1 -1
  70. package/schema/document-v1.schema.json +5 -1
@@ -0,0 +1,82 @@
1
+ require("../../security/content-limits.cjs");
2
+ const require_walk_objects = require("../../fabric/walk-objects.cjs");
3
+ //#region src/export/svg/raster-object.ts
4
+ /**
5
+ * The largest multiplier that keeps a `width × height` area within the
6
+ * canvas limits. A picture over the browser's limit would fail, so a large
7
+ * page is drawn at a lower resolution instead: O(1) arithmetic.
8
+ */
9
+ function safeMultiplier(multiplier, width, height, limits) {
10
+ const maxPixels = limits?.maxCanvasPixels ?? 67108864;
11
+ const maxSide = limits?.maxCanvasSide ?? 16384;
12
+ const byArea = Math.sqrt(maxPixels / Math.max(1, width * height));
13
+ const bySide = maxSide / Math.max(1, width, height);
14
+ return Math.min(multiplier, byArea, bySide);
15
+ }
16
+ function shadowPadding(object) {
17
+ let padding = 0;
18
+ require_walk_objects.walkObjects([object], (child) => {
19
+ const shadow = child.shadow;
20
+ if (shadow) padding = Math.max(padding, shadow.blur + Math.max(Math.abs(shadow.offsetX), Math.abs(shadow.offsetY)));
21
+ });
22
+ return padding + 2;
23
+ }
24
+ function usesBlendMode(object) {
25
+ let blends = false;
26
+ require_walk_objects.walkObjects([object], (child) => {
27
+ if (child.globalCompositeOperation && child.globalCompositeOperation !== "source-over") blends = true;
28
+ });
29
+ return blends;
30
+ }
31
+ /**
32
+ * An object as a picture placed where it sits on the canvas. Usually the
33
+ * object alone, so the vectors underneath show through. An object with a
34
+ * blend mode mixes with what lies below it, so its picture includes
35
+ * everything below it within its bounds, which covers those vectors exactly.
36
+ */
37
+ function rasterMarkup(canvas, object, multiplier, limits) {
38
+ object.setCoords();
39
+ const bounds = object.getBoundingRect();
40
+ const padding = shadowPadding(object);
41
+ const left = Math.max(0, Math.floor(bounds.left - padding));
42
+ const top = Math.max(0, Math.floor(bounds.top - padding));
43
+ const right = Math.min(canvas.getWidth(), Math.ceil(bounds.left + bounds.width + padding));
44
+ const bottom = Math.min(canvas.getHeight(), Math.ceil(bounds.top + bounds.height + padding));
45
+ if (right <= left || bottom <= top) return "";
46
+ const blends = usesBlendMode(object);
47
+ const stack = canvas.getObjects();
48
+ const position = stack.indexOf(object);
49
+ const saved = {
50
+ backgroundColor: canvas.backgroundColor,
51
+ backgroundImage: canvas.backgroundImage,
52
+ overlayColor: canvas.overlayColor,
53
+ overlayImage: canvas.overlayImage
54
+ };
55
+ if (!blends) {
56
+ canvas.backgroundColor = "";
57
+ canvas.backgroundImage = void 0;
58
+ }
59
+ canvas.overlayColor = "";
60
+ canvas.overlayImage = void 0;
61
+ let element;
62
+ try {
63
+ const filter = (candidate) => blends ? stack.indexOf(candidate) <= position : candidate === object;
64
+ const safe = safeMultiplier(multiplier, right - left, bottom - top, limits);
65
+ element = canvas.toCanvasElement(safe, {
66
+ left,
67
+ top,
68
+ width: right - left,
69
+ height: bottom - top,
70
+ filter
71
+ });
72
+ } finally {
73
+ Object.assign(canvas, saved);
74
+ }
75
+ const url = element.toDataURL("image/png");
76
+ element.width = 0;
77
+ element.height = 0;
78
+ return `<image x="${left}" y="${top}" width="${right - left}" height="${bottom - top}" preserveAspectRatio="none" xlink:href="${url}" />\n`;
79
+ }
80
+ //#endregion
81
+ exports.rasterMarkup = rasterMarkup;
82
+ exports.safeMultiplier = safeMultiplier;
@@ -0,0 +1,81 @@
1
+ import "../../security/content-limits.js";
2
+ import { walkObjects } from "../../fabric/walk-objects.js";
3
+ //#region src/export/svg/raster-object.ts
4
+ /**
5
+ * The largest multiplier that keeps a `width × height` area within the
6
+ * canvas limits. A picture over the browser's limit would fail, so a large
7
+ * page is drawn at a lower resolution instead: O(1) arithmetic.
8
+ */
9
+ function safeMultiplier(multiplier, width, height, limits) {
10
+ const maxPixels = limits?.maxCanvasPixels ?? 67108864;
11
+ const maxSide = limits?.maxCanvasSide ?? 16384;
12
+ const byArea = Math.sqrt(maxPixels / Math.max(1, width * height));
13
+ const bySide = maxSide / Math.max(1, width, height);
14
+ return Math.min(multiplier, byArea, bySide);
15
+ }
16
+ function shadowPadding(object) {
17
+ let padding = 0;
18
+ walkObjects([object], (child) => {
19
+ const shadow = child.shadow;
20
+ if (shadow) padding = Math.max(padding, shadow.blur + Math.max(Math.abs(shadow.offsetX), Math.abs(shadow.offsetY)));
21
+ });
22
+ return padding + 2;
23
+ }
24
+ function usesBlendMode(object) {
25
+ let blends = false;
26
+ walkObjects([object], (child) => {
27
+ if (child.globalCompositeOperation && child.globalCompositeOperation !== "source-over") blends = true;
28
+ });
29
+ return blends;
30
+ }
31
+ /**
32
+ * An object as a picture placed where it sits on the canvas. Usually the
33
+ * object alone, so the vectors underneath show through. An object with a
34
+ * blend mode mixes with what lies below it, so its picture includes
35
+ * everything below it within its bounds, which covers those vectors exactly.
36
+ */
37
+ function rasterMarkup(canvas, object, multiplier, limits) {
38
+ object.setCoords();
39
+ const bounds = object.getBoundingRect();
40
+ const padding = shadowPadding(object);
41
+ const left = Math.max(0, Math.floor(bounds.left - padding));
42
+ const top = Math.max(0, Math.floor(bounds.top - padding));
43
+ const right = Math.min(canvas.getWidth(), Math.ceil(bounds.left + bounds.width + padding));
44
+ const bottom = Math.min(canvas.getHeight(), Math.ceil(bounds.top + bounds.height + padding));
45
+ if (right <= left || bottom <= top) return "";
46
+ const blends = usesBlendMode(object);
47
+ const stack = canvas.getObjects();
48
+ const position = stack.indexOf(object);
49
+ const saved = {
50
+ backgroundColor: canvas.backgroundColor,
51
+ backgroundImage: canvas.backgroundImage,
52
+ overlayColor: canvas.overlayColor,
53
+ overlayImage: canvas.overlayImage
54
+ };
55
+ if (!blends) {
56
+ canvas.backgroundColor = "";
57
+ canvas.backgroundImage = void 0;
58
+ }
59
+ canvas.overlayColor = "";
60
+ canvas.overlayImage = void 0;
61
+ let element;
62
+ try {
63
+ const filter = (candidate) => blends ? stack.indexOf(candidate) <= position : candidate === object;
64
+ const safe = safeMultiplier(multiplier, right - left, bottom - top, limits);
65
+ element = canvas.toCanvasElement(safe, {
66
+ left,
67
+ top,
68
+ width: right - left,
69
+ height: bottom - top,
70
+ filter
71
+ });
72
+ } finally {
73
+ Object.assign(canvas, saved);
74
+ }
75
+ const url = element.toDataURL("image/png");
76
+ element.width = 0;
77
+ element.height = 0;
78
+ return `<image x="${left}" y="${top}" width="${right - left}" height="${bottom - top}" preserveAspectRatio="none" xlink:href="${url}" />\n`;
79
+ }
80
+ //#endregion
81
+ export { rasterMarkup, safeMultiplier };
@@ -1,4 +1,4 @@
1
- //#region src/pdf/text-decorations.ts
1
+ //#region src/export/svg/text-decorations.ts
2
2
  const DECORATIONS = [
3
3
  "underline",
4
4
  "overline",
@@ -16,7 +16,7 @@ function isText(object) {
16
16
  }
17
17
  function hasStraightDecorations(object) {
18
18
  if (!isText(object)) return false;
19
- return DECORATIONS.some((type) => object[type] || object.styleHas(type));
19
+ return DECORATIONS.some((type) => object[type] || object.styleHas(type)) || object.styleHas("deltaY");
20
20
  }
21
21
  function rect(x, y, width, height, color) {
22
22
  if (typeof color !== "string" || width <= 0 || height <= 0) return "";
@@ -90,7 +90,7 @@ function decorationRects(text, type) {
90
90
  /** SVG for straight text with its decorations as boxes instead of CSS. */
91
91
  function decoratedTextToSVG(object, reviver) {
92
92
  const text = object;
93
- const inner = text._toSVG().join("").replace(/\stext-decoration="[^"]*"/g, "").replace(/text-decoration:[^;"]*;?\s*/g, "");
93
+ const inner = text._toSVG().join("").replace(/\stext-decoration="[^"]*"/g, "").replace(/text-decoration:[^;"]*;?\s*/g, "").replace(/baseline-shift:[^;"]*;?\s*/g, "");
94
94
  const markup = [
95
95
  decorationRects(text, "underline"),
96
96
  inner,
@@ -1,4 +1,4 @@
1
- //#region src/pdf/text-decorations.ts
1
+ //#region src/export/svg/text-decorations.ts
2
2
  const DECORATIONS = [
3
3
  "underline",
4
4
  "overline",
@@ -16,7 +16,7 @@ function isText(object) {
16
16
  }
17
17
  function hasStraightDecorations(object) {
18
18
  if (!isText(object)) return false;
19
- return DECORATIONS.some((type) => object[type] || object.styleHas(type));
19
+ return DECORATIONS.some((type) => object[type] || object.styleHas(type)) || object.styleHas("deltaY");
20
20
  }
21
21
  function rect(x, y, width, height, color) {
22
22
  if (typeof color !== "string" || width <= 0 || height <= 0) return "";
@@ -90,7 +90,7 @@ function decorationRects(text, type) {
90
90
  /** SVG for straight text with its decorations as boxes instead of CSS. */
91
91
  function decoratedTextToSVG(object, reviver) {
92
92
  const text = object;
93
- const inner = text._toSVG().join("").replace(/\stext-decoration="[^"]*"/g, "").replace(/text-decoration:[^;"]*;?\s*/g, "");
93
+ const inner = text._toSVG().join("").replace(/\stext-decoration="[^"]*"/g, "").replace(/text-decoration:[^;"]*;?\s*/g, "").replace(/baseline-shift:[^;"]*;?\s*/g, "");
94
94
  const markup = [
95
95
  decorationRects(text, "underline"),
96
96
  inner,
@@ -1,5 +1,4 @@
1
1
  const require_walk_objects = require("../../fabric/walk-objects.cjs");
2
- const require_overrides = require("./overrides.cjs");
3
2
  let fabric = require("fabric");
4
3
  //#region src/export/svg/text-on-path.ts
5
4
  function isTextOnPath(object) {
@@ -230,14 +229,6 @@ function textOnPathToSVG(object, reviver) {
230
229
  withShadow: true
231
230
  });
232
231
  }
233
- /**
234
- * Runs `work` while every text on a path on the canvas, including text in
235
- * groups, writes the SVG from this module. Everything else keeps Fabric's
236
- * SVG. Returns how many objects were handled.
237
- */
238
- function withTextOnPathSVG(canvas, work) {
239
- return require_overrides.withSvgOverrides(canvas, (object) => isTextOnPath(object) ? (reviver) => textOnPathToSVG(object, reviver) : void 0, work);
240
- }
241
232
  function findTextOnPath(canvas) {
242
233
  const found = [];
243
234
  require_walk_objects.walkObjects(canvas.getObjects(), (object) => {
@@ -249,4 +240,3 @@ function findTextOnPath(canvas) {
249
240
  exports.findTextOnPath = findTextOnPath;
250
241
  exports.isTextOnPath = isTextOnPath;
251
242
  exports.textOnPathToSVG = textOnPathToSVG;
252
- exports.withTextOnPathSVG = withTextOnPathSVG;
@@ -1,5 +1,4 @@
1
1
  import { walkObjects } from "../../fabric/walk-objects.js";
2
- import { withSvgOverrides } from "./overrides.js";
3
2
  import { Color, util } from "fabric";
4
3
  //#region src/export/svg/text-on-path.ts
5
4
  function isTextOnPath(object) {
@@ -230,14 +229,6 @@ function textOnPathToSVG(object, reviver) {
230
229
  withShadow: true
231
230
  });
232
231
  }
233
- /**
234
- * Runs `work` while every text on a path on the canvas, including text in
235
- * groups, writes the SVG from this module. Everything else keeps Fabric's
236
- * SVG. Returns how many objects were handled.
237
- */
238
- function withTextOnPathSVG(canvas, work) {
239
- return withSvgOverrides(canvas, (object) => isTextOnPath(object) ? (reviver) => textOnPathToSVG(object, reviver) : void 0, work);
240
- }
241
232
  function findTextOnPath(canvas) {
242
233
  const found = [];
243
234
  walkObjects(canvas.getObjects(), (object) => {
@@ -246,4 +237,4 @@ function findTextOnPath(canvas) {
246
237
  return found;
247
238
  }
248
239
  //#endregion
249
- export { findTextOnPath, isTextOnPath, textOnPathToSVG, withTextOnPathSVG };
240
+ export { findTextOnPath, isTextOnPath, textOnPathToSVG };
@@ -1,3 +1,4 @@
1
+ const require_page_state = require("./page-state.cjs");
1
2
  const require_concurrency = require("../util/concurrency.cjs");
2
3
  const require_walk_objects = require("./walk-objects.cjs");
3
4
  let fabric = require("fabric");
@@ -25,7 +26,7 @@ function serializeCanvas(canvas, propertiesToInclude) {
25
26
  keepOriginExplicit(canvas.getObjects(), objects);
26
27
  return {
27
28
  version: serialized.version,
28
- background: serialized.background,
29
+ ...require_page_state.pickPageFields(serialized),
29
30
  objects
30
31
  };
31
32
  }
@@ -73,11 +74,11 @@ function abortError() {
73
74
  * freeze the page. Like Fabric, every object is created before the canvas is
74
75
  * cleared: a failure or cancel leaves the old content in place.
75
76
  */
76
- async function loadIntoCanvas(canvas, content, { signal, chunkSize = 100, onObjects }) {
77
+ async function loadIntoCanvas(canvas, content, { signal, chunkSize = 100, onObjects, beforeSwap }) {
77
78
  const total = content.objects.length;
78
79
  const size = Math.max(1, Math.floor(chunkSize));
79
80
  const created = [];
80
- const background = fabric.util.enlivenObjectEnlivables({ backgroundColor: content.background }, { signal });
81
+ const background = require_page_state.enlivenPage(content, signal);
81
82
  background.catch(() => void 0);
82
83
  try {
83
84
  onObjects?.(0, total);
@@ -93,6 +94,7 @@ async function loadIntoCanvas(canvas, content, { signal, chunkSize = 100, onObje
93
94
  }
94
95
  const enlivenedBackground = await background;
95
96
  if (signal.aborted) throw abortError();
97
+ beforeSwap?.();
96
98
  const renderOnAddRemove = canvas.renderOnAddRemove;
97
99
  canvas.renderOnAddRemove = false;
98
100
  try {
@@ -1,3 +1,4 @@
1
+ import { enlivenPage, pickPageFields } from "./page-state.js";
1
2
  import { yieldToEventLoop } from "../util/concurrency.js";
2
3
  import { childrenOf, walkObjects } from "./walk-objects.js";
3
4
  import { util } from "fabric";
@@ -25,7 +26,7 @@ function serializeCanvas(canvas, propertiesToInclude) {
25
26
  keepOriginExplicit(canvas.getObjects(), objects);
26
27
  return {
27
28
  version: serialized.version,
28
- background: serialized.background,
29
+ ...pickPageFields(serialized),
29
30
  objects
30
31
  };
31
32
  }
@@ -73,11 +74,11 @@ function abortError() {
73
74
  * freeze the page. Like Fabric, every object is created before the canvas is
74
75
  * cleared: a failure or cancel leaves the old content in place.
75
76
  */
76
- async function loadIntoCanvas(canvas, content, { signal, chunkSize = 100, onObjects }) {
77
+ async function loadIntoCanvas(canvas, content, { signal, chunkSize = 100, onObjects, beforeSwap }) {
77
78
  const total = content.objects.length;
78
79
  const size = Math.max(1, Math.floor(chunkSize));
79
80
  const created = [];
80
- const background = util.enlivenObjectEnlivables({ backgroundColor: content.background }, { signal });
81
+ const background = enlivenPage(content, signal);
81
82
  background.catch(() => void 0);
82
83
  try {
83
84
  onObjects?.(0, total);
@@ -93,6 +94,7 @@ async function loadIntoCanvas(canvas, content, { signal, chunkSize = 100, onObje
93
94
  }
94
95
  const enlivenedBackground = await background;
95
96
  if (signal.aborted) throw abortError();
97
+ beforeSwap?.();
96
98
  const renderOnAddRemove = canvas.renderOnAddRemove;
97
99
  canvas.renderOnAddRemove = false;
98
100
  try {
@@ -0,0 +1,69 @@
1
+ let fabric = require("fabric");
2
+ //#region src/fabric/page-state.ts
3
+ /** The page fields saved with a document, besides its size. */
4
+ const PAGE_FIELDS = [
5
+ "background",
6
+ "backgroundImage",
7
+ "overlay",
8
+ "overlayImage",
9
+ "clipPath"
10
+ ];
11
+ /** Copies the page fields that are set from serialized canvas output. */
12
+ function pickPageFields(source) {
13
+ const picked = {};
14
+ for (const field of PAGE_FIELDS) if (source[field] !== void 0 && source[field] !== null) picked[field] = source[field];
15
+ return picked;
16
+ }
17
+ /** The canvas-level objects that can hold images or other objects: background and overlay images and the mask. */
18
+ function pageObjects(page) {
19
+ const objects = [];
20
+ for (const field of [
21
+ "backgroundImage",
22
+ "overlayImage",
23
+ "clipPath"
24
+ ]) {
25
+ const value = page[field];
26
+ if (typeof value === "object" && value !== null && typeof value.type === "string") objects.push(value);
27
+ }
28
+ return objects;
29
+ }
30
+ /**
31
+ * Creates the live values for `canvas.set`: Fabric objects for the images
32
+ * and mask, gradients and patterns for colors. Missing fields become
33
+ * `undefined`, which clears them on the canvas.
34
+ */
35
+ function enlivenPage(page, signal) {
36
+ return fabric.util.enlivenObjectEnlivables({
37
+ backgroundColor: page.background,
38
+ backgroundImage: page.backgroundImage,
39
+ overlayColor: page.overlay,
40
+ overlayImage: page.overlayImage,
41
+ clipPath: page.clipPath
42
+ }, signal ? { signal } : {});
43
+ }
44
+ /** Applies a page state: the size first, then the enlivened fields. */
45
+ async function applyPage(canvas, page) {
46
+ const live = await enlivenPage(page);
47
+ if (canvas.getWidth() !== page.width || canvas.getHeight() !== page.height) canvas.setDimensions({
48
+ width: page.width,
49
+ height: page.height
50
+ });
51
+ canvas.set(live);
52
+ canvas.requestRenderAll();
53
+ }
54
+ /**
55
+ * Every top-level object a document holds: the objects on the page, then its
56
+ * background image, overlay image and mask. Asset checks, address checks and
57
+ * type checks walk this list, so they cover the page as well.
58
+ */
59
+ function documentObjects(document) {
60
+ const page = document.canvas ? pageObjects(document.canvas) : [];
61
+ return page.length === 0 ? [...document.objects] : [...document.objects, ...page];
62
+ }
63
+ //#endregion
64
+ exports.PAGE_FIELDS = PAGE_FIELDS;
65
+ exports.applyPage = applyPage;
66
+ exports.documentObjects = documentObjects;
67
+ exports.enlivenPage = enlivenPage;
68
+ exports.pageObjects = pageObjects;
69
+ exports.pickPageFields = pickPageFields;
@@ -0,0 +1,64 @@
1
+ import { util } from "fabric";
2
+ //#region src/fabric/page-state.ts
3
+ /** The page fields saved with a document, besides its size. */
4
+ const PAGE_FIELDS = [
5
+ "background",
6
+ "backgroundImage",
7
+ "overlay",
8
+ "overlayImage",
9
+ "clipPath"
10
+ ];
11
+ /** Copies the page fields that are set from serialized canvas output. */
12
+ function pickPageFields(source) {
13
+ const picked = {};
14
+ for (const field of PAGE_FIELDS) if (source[field] !== void 0 && source[field] !== null) picked[field] = source[field];
15
+ return picked;
16
+ }
17
+ /** The canvas-level objects that can hold images or other objects: background and overlay images and the mask. */
18
+ function pageObjects(page) {
19
+ const objects = [];
20
+ for (const field of [
21
+ "backgroundImage",
22
+ "overlayImage",
23
+ "clipPath"
24
+ ]) {
25
+ const value = page[field];
26
+ if (typeof value === "object" && value !== null && typeof value.type === "string") objects.push(value);
27
+ }
28
+ return objects;
29
+ }
30
+ /**
31
+ * Creates the live values for `canvas.set`: Fabric objects for the images
32
+ * and mask, gradients and patterns for colors. Missing fields become
33
+ * `undefined`, which clears them on the canvas.
34
+ */
35
+ function enlivenPage(page, signal) {
36
+ return util.enlivenObjectEnlivables({
37
+ backgroundColor: page.background,
38
+ backgroundImage: page.backgroundImage,
39
+ overlayColor: page.overlay,
40
+ overlayImage: page.overlayImage,
41
+ clipPath: page.clipPath
42
+ }, signal ? { signal } : {});
43
+ }
44
+ /** Applies a page state: the size first, then the enlivened fields. */
45
+ async function applyPage(canvas, page) {
46
+ const live = await enlivenPage(page);
47
+ if (canvas.getWidth() !== page.width || canvas.getHeight() !== page.height) canvas.setDimensions({
48
+ width: page.width,
49
+ height: page.height
50
+ });
51
+ canvas.set(live);
52
+ canvas.requestRenderAll();
53
+ }
54
+ /**
55
+ * Every top-level object a document holds: the objects on the page, then its
56
+ * background image, overlay image and mask. Asset checks, address checks and
57
+ * type checks walk this list, so they cover the page as well.
58
+ */
59
+ function documentObjects(document) {
60
+ const page = document.canvas ? pageObjects(document.canvas) : [];
61
+ return page.length === 0 ? [...document.objects] : [...document.objects, ...page];
62
+ }
63
+ //#endregion
64
+ export { PAGE_FIELDS, applyPage, documentObjects, enlivenPage, pageObjects, pickPageFields };
@@ -1,5 +1,7 @@
1
+ const require_page_state = require("../fabric/page-state.cjs");
1
2
  const require_object_ids = require("../fabric/object-ids.cjs");
2
3
  const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
4
+ const require_snapshot = require("./snapshot.cjs");
3
5
  //#region src/history/apply-state.ts
4
6
  function arrangeObjects(current, change, created) {
5
7
  const arranged = [];
@@ -20,10 +22,24 @@ function arrangeObjects(current, change, created) {
20
22
  const position = new Map(change.order.map((id, index) => [id, index]));
21
23
  return arranged.sort((first, second) => (position.get(require_object_ids.readObjectId(first) ?? "") ?? Infinity) - (position.get(require_object_ids.readObjectId(second) ?? "") ?? Infinity));
22
24
  }
23
- async function applyStateChange(canvas, change) {
25
+ /**
26
+ * Applies a change. `isStale` is checked after every wait: when it returns
27
+ * true the canvas now shows another document, so nothing is applied and the
28
+ * call returns false.
29
+ */
30
+ async function applyStateChange(canvas, change, isStale = () => false) {
31
+ const page = change.objects.get(require_snapshot.PAGE_KEY);
32
+ if (page) {
33
+ if (isStale()) return false;
34
+ await require_page_state.applyPage(canvas, JSON.parse(page));
35
+ }
24
36
  const serializedObjects = [];
25
- for (const json of change.objects.values()) if (json !== null) serializedObjects.push(JSON.parse(json));
37
+ for (const [id, json] of change.objects) if (id !== "__page__" && json !== null) serializedObjects.push(JSON.parse(json));
26
38
  const createdObjects = await require_fabric_adapter.createObjects(serializedObjects);
39
+ if (isStale()) {
40
+ createdObjects.forEach((object) => object.dispose?.());
41
+ return false;
42
+ }
27
43
  const createdById = new Map(createdObjects.map((object) => [require_object_ids.readObjectId(object) ?? "", object]));
28
44
  canvas.discardActiveObject?.();
29
45
  const current = canvas.getObjects();
@@ -45,6 +61,7 @@ async function applyStateChange(canvas, change) {
45
61
  canvas.renderOnAddRemove = renderOnAddRemove;
46
62
  }
47
63
  canvas.requestRenderAll();
64
+ return true;
48
65
  }
49
66
  //#endregion
50
67
  exports.applyStateChange = applyStateChange;
@@ -1,5 +1,7 @@
1
+ import { applyPage } from "../fabric/page-state.js";
1
2
  import { readObjectId } from "../fabric/object-ids.js";
2
3
  import { createObjects } from "../fabric/fabric-adapter.js";
4
+ import { PAGE_KEY } from "./snapshot.js";
3
5
  //#region src/history/apply-state.ts
4
6
  function arrangeObjects(current, change, created) {
5
7
  const arranged = [];
@@ -20,10 +22,24 @@ function arrangeObjects(current, change, created) {
20
22
  const position = new Map(change.order.map((id, index) => [id, index]));
21
23
  return arranged.sort((first, second) => (position.get(readObjectId(first) ?? "") ?? Infinity) - (position.get(readObjectId(second) ?? "") ?? Infinity));
22
24
  }
23
- async function applyStateChange(canvas, change) {
25
+ /**
26
+ * Applies a change. `isStale` is checked after every wait: when it returns
27
+ * true the canvas now shows another document, so nothing is applied and the
28
+ * call returns false.
29
+ */
30
+ async function applyStateChange(canvas, change, isStale = () => false) {
31
+ const page = change.objects.get(PAGE_KEY);
32
+ if (page) {
33
+ if (isStale()) return false;
34
+ await applyPage(canvas, JSON.parse(page));
35
+ }
24
36
  const serializedObjects = [];
25
- for (const json of change.objects.values()) if (json !== null) serializedObjects.push(JSON.parse(json));
37
+ for (const [id, json] of change.objects) if (id !== "__page__" && json !== null) serializedObjects.push(JSON.parse(json));
26
38
  const createdObjects = await createObjects(serializedObjects);
39
+ if (isStale()) {
40
+ createdObjects.forEach((object) => object.dispose?.());
41
+ return false;
42
+ }
27
43
  const createdById = new Map(createdObjects.map((object) => [readObjectId(object) ?? "", object]));
28
44
  canvas.discardActiveObject?.();
29
45
  const current = canvas.getObjects();
@@ -45,6 +61,7 @@ async function applyStateChange(canvas, change) {
45
61
  canvas.renderOnAddRemove = renderOnAddRemove;
46
62
  }
47
63
  canvas.requestRenderAll();
64
+ return true;
48
65
  }
49
66
  //#endregion
50
67
  export { applyStateChange };