fabricjs-document-engine 0.2.0 → 0.3.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 (36) hide show
  1. package/README.md +77 -13
  2. package/dist/assets/asset-manifest.cjs +43 -0
  3. package/dist/assets/asset-manifest.d.cts +16 -0
  4. package/dist/assets/asset-manifest.d.ts +16 -0
  5. package/dist/assets/asset-manifest.js +41 -0
  6. package/dist/assets/asset-pipeline.cjs +174 -0
  7. package/dist/assets/asset-pipeline.d.cts +31 -0
  8. package/dist/assets/asset-pipeline.d.ts +31 -0
  9. package/dist/assets/asset-pipeline.js +172 -0
  10. package/dist/assets/asset-references.cjs +83 -0
  11. package/dist/assets/asset-references.js +82 -0
  12. package/dist/assets/font-check.cjs +59 -0
  13. package/dist/assets/font-check.d.cts +4 -0
  14. package/dist/assets/font-check.d.ts +4 -0
  15. package/dist/assets/font-check.js +57 -0
  16. package/dist/assets/image-check.cjs +36 -0
  17. package/dist/assets/image-check.js +33 -0
  18. package/dist/document/document-format.d.cts +2 -0
  19. package/dist/document/document-format.d.ts +2 -0
  20. package/dist/document/validate-document.cjs +4 -0
  21. package/dist/document/validate-document.js +4 -0
  22. package/dist/engine/create-document-engine.cjs +56 -5
  23. package/dist/engine/create-document-engine.d.cts +10 -0
  24. package/dist/engine/create-document-engine.d.ts +10 -0
  25. package/dist/engine/create-document-engine.js +56 -5
  26. package/dist/engine/errors.cjs +2 -0
  27. package/dist/engine/errors.d.cts +6 -1
  28. package/dist/engine/errors.d.ts +6 -1
  29. package/dist/engine/errors.js +2 -0
  30. package/dist/index.cjs +1 -1
  31. package/dist/index.d.cts +4 -1
  32. package/dist/index.d.ts +4 -1
  33. package/dist/index.js +1 -1
  34. package/dist/save/save-controller.cjs +6 -5
  35. package/dist/save/save-controller.js +6 -5
  36. package/package.json +1 -1
@@ -0,0 +1,83 @@
1
+ //#region src/assets/asset-references.ts
2
+ function isPlainObject(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ function visitObjects(objects, visit) {
6
+ const pending = objects.map((object) => ({
7
+ object,
8
+ ownerId: object.id
9
+ })).reverse();
10
+ while (pending.length > 0) {
11
+ const { object, ownerId } = pending.pop();
12
+ const id = object.id ?? ownerId;
13
+ visit(object, id);
14
+ const children = (object.objects ?? []).map((child) => ({
15
+ object: child,
16
+ ownerId: child.id ?? id
17
+ }));
18
+ if (object.clipPath) children.push({
19
+ object: object.clipPath,
20
+ ownerId: id
21
+ });
22
+ pending.push(...children.reverse());
23
+ }
24
+ }
25
+ function findImageReferences(objects) {
26
+ const references = [];
27
+ visitObjects(objects, (object, objectId) => {
28
+ if (typeof object.src === "string" && object.src.length > 0) references.push({
29
+ url: object.src,
30
+ objectId,
31
+ crossOrigin: object.crossOrigin,
32
+ holder: object,
33
+ key: "src"
34
+ });
35
+ for (const paintKey of ["fill", "stroke"]) {
36
+ const paint = object[paintKey];
37
+ if (isPlainObject(paint) && typeof paint.source === "string" && paint.source.length > 0) references.push({
38
+ url: paint.source,
39
+ objectId,
40
+ crossOrigin: paint.crossOrigin,
41
+ holder: paint,
42
+ key: "source"
43
+ });
44
+ }
45
+ });
46
+ return references;
47
+ }
48
+ function characterStylesOf(styles) {
49
+ if (Array.isArray(styles)) return styles.map((range) => isPlainObject(range) && isPlainObject(range.style) ? range.style : {});
50
+ if (!isPlainObject(styles)) return [];
51
+ return Object.values(styles).flatMap((line) => isPlainObject(line) ? Object.values(line).filter(isPlainObject) : []);
52
+ }
53
+ function textOf(value, fallback) {
54
+ if (typeof value === "string" && value.length > 0) return value;
55
+ if (typeof value === "number") return String(value);
56
+ return fallback;
57
+ }
58
+ function findFontReferences(objects) {
59
+ const references = [];
60
+ visitObjects(objects, (object, objectId) => {
61
+ if (typeof object.fontFamily !== "string") return;
62
+ const base = {
63
+ family: object.fontFamily,
64
+ weight: textOf(object.fontWeight, "normal"),
65
+ style: textOf(object.fontStyle, "normal"),
66
+ objectId
67
+ };
68
+ references.push(base);
69
+ for (const characterStyle of characterStylesOf(object.styles)) {
70
+ if (characterStyle.fontFamily === void 0 && characterStyle.fontWeight === void 0 && characterStyle.fontStyle === void 0) continue;
71
+ references.push({
72
+ family: textOf(characterStyle.fontFamily, base.family),
73
+ weight: textOf(characterStyle.fontWeight, base.weight),
74
+ style: textOf(characterStyle.fontStyle, base.style),
75
+ objectId
76
+ });
77
+ }
78
+ });
79
+ return references;
80
+ }
81
+ //#endregion
82
+ exports.findFontReferences = findFontReferences;
83
+ exports.findImageReferences = findImageReferences;
@@ -0,0 +1,82 @@
1
+ //#region src/assets/asset-references.ts
2
+ function isPlainObject(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ function visitObjects(objects, visit) {
6
+ const pending = objects.map((object) => ({
7
+ object,
8
+ ownerId: object.id
9
+ })).reverse();
10
+ while (pending.length > 0) {
11
+ const { object, ownerId } = pending.pop();
12
+ const id = object.id ?? ownerId;
13
+ visit(object, id);
14
+ const children = (object.objects ?? []).map((child) => ({
15
+ object: child,
16
+ ownerId: child.id ?? id
17
+ }));
18
+ if (object.clipPath) children.push({
19
+ object: object.clipPath,
20
+ ownerId: id
21
+ });
22
+ pending.push(...children.reverse());
23
+ }
24
+ }
25
+ function findImageReferences(objects) {
26
+ const references = [];
27
+ visitObjects(objects, (object, objectId) => {
28
+ if (typeof object.src === "string" && object.src.length > 0) references.push({
29
+ url: object.src,
30
+ objectId,
31
+ crossOrigin: object.crossOrigin,
32
+ holder: object,
33
+ key: "src"
34
+ });
35
+ for (const paintKey of ["fill", "stroke"]) {
36
+ const paint = object[paintKey];
37
+ if (isPlainObject(paint) && typeof paint.source === "string" && paint.source.length > 0) references.push({
38
+ url: paint.source,
39
+ objectId,
40
+ crossOrigin: paint.crossOrigin,
41
+ holder: paint,
42
+ key: "source"
43
+ });
44
+ }
45
+ });
46
+ return references;
47
+ }
48
+ function characterStylesOf(styles) {
49
+ if (Array.isArray(styles)) return styles.map((range) => isPlainObject(range) && isPlainObject(range.style) ? range.style : {});
50
+ if (!isPlainObject(styles)) return [];
51
+ return Object.values(styles).flatMap((line) => isPlainObject(line) ? Object.values(line).filter(isPlainObject) : []);
52
+ }
53
+ function textOf(value, fallback) {
54
+ if (typeof value === "string" && value.length > 0) return value;
55
+ if (typeof value === "number") return String(value);
56
+ return fallback;
57
+ }
58
+ function findFontReferences(objects) {
59
+ const references = [];
60
+ visitObjects(objects, (object, objectId) => {
61
+ if (typeof object.fontFamily !== "string") return;
62
+ const base = {
63
+ family: object.fontFamily,
64
+ weight: textOf(object.fontWeight, "normal"),
65
+ style: textOf(object.fontStyle, "normal"),
66
+ objectId
67
+ };
68
+ references.push(base);
69
+ for (const characterStyle of characterStylesOf(object.styles)) {
70
+ if (characterStyle.fontFamily === void 0 && characterStyle.fontWeight === void 0 && characterStyle.fontStyle === void 0) continue;
71
+ references.push({
72
+ family: textOf(characterStyle.fontFamily, base.family),
73
+ weight: textOf(characterStyle.fontWeight, base.weight),
74
+ style: textOf(characterStyle.fontStyle, base.style),
75
+ objectId
76
+ });
77
+ }
78
+ });
79
+ return references;
80
+ }
81
+ //#endregion
82
+ export { findFontReferences, findImageReferences };
@@ -0,0 +1,59 @@
1
+ //#region src/assets/font-check.ts
2
+ const genericFamilies = /* @__PURE__ */ new Set([
3
+ "serif",
4
+ "sans-serif",
5
+ "monospace",
6
+ "cursive",
7
+ "fantasy",
8
+ "system-ui",
9
+ "ui-serif",
10
+ "ui-sans-serif",
11
+ "ui-monospace",
12
+ "ui-rounded",
13
+ "emoji",
14
+ "math",
15
+ "fangsong"
16
+ ]);
17
+ const fallbackFamilies = [
18
+ "monospace",
19
+ "serif",
20
+ "sans-serif"
21
+ ];
22
+ const sampleText = "mmmmmmmmmmlli WwQq 0123456789";
23
+ function firstFamily(family) {
24
+ return (family.split(",")[0] ?? family).trim().replace(/^["']|["']$/g, "");
25
+ }
26
+ function cssFamily(family) {
27
+ return genericFamilies.has(family.toLowerCase()) ? family : `"${family.replace(/"/g, "\\\"")}"`;
28
+ }
29
+ function fontDescriptor(font, size = 32) {
30
+ return `${font.style} ${font.weight} ${size}px ${cssFamily(firstFamily(font.family))}`;
31
+ }
32
+ function createMeasuringContext() {
33
+ if (typeof document === "undefined") return null;
34
+ return document.createElement("canvas").getContext("2d");
35
+ }
36
+ function isFontRenderable(font, context = createMeasuringContext()) {
37
+ const family = firstFamily(font.family);
38
+ if (context === null || genericFamilies.has(family.toLowerCase())) return true;
39
+ return fallbackFamilies.some((fallback) => {
40
+ context.font = `${font.style} ${font.weight} 72px ${fallback}`;
41
+ const fallbackWidth = context.measureText(sampleText).width;
42
+ context.font = `${font.style} ${font.weight} 72px ${cssFamily(family)}, ${fallback}`;
43
+ return context.measureText(sampleText).width !== fallbackWidth;
44
+ });
45
+ }
46
+ async function prepareFont(font, loadFont) {
47
+ if (loadFont) await loadFont(font);
48
+ const fontSet = typeof document === "undefined" ? void 0 : document.fonts;
49
+ if (fontSet) await fontSet.load(fontDescriptor(font), sampleText);
50
+ }
51
+ async function findUnavailableFonts(fonts, loadFont) {
52
+ await Promise.allSettled(fonts.map((font) => prepareFont(font, loadFont)));
53
+ const context = createMeasuringContext();
54
+ return fonts.filter((font) => !isFontRenderable(font, context));
55
+ }
56
+ //#endregion
57
+ exports.findUnavailableFonts = findUnavailableFonts;
58
+ exports.fontDescriptor = fontDescriptor;
59
+ exports.isFontRenderable = isFontRenderable;
@@ -0,0 +1,4 @@
1
+ import { FontAsset } from "./asset-manifest.cjs";
2
+ //#region src/assets/font-check.d.ts
3
+ export type FontLoader = (font: FontAsset) => void | Promise<void>;
4
+ //#endregion
@@ -0,0 +1,4 @@
1
+ import { FontAsset } from "./asset-manifest.js";
2
+ //#region src/assets/font-check.d.ts
3
+ export type FontLoader = (font: FontAsset) => void | Promise<void>;
4
+ //#endregion
@@ -0,0 +1,57 @@
1
+ //#region src/assets/font-check.ts
2
+ const genericFamilies = /* @__PURE__ */ new Set([
3
+ "serif",
4
+ "sans-serif",
5
+ "monospace",
6
+ "cursive",
7
+ "fantasy",
8
+ "system-ui",
9
+ "ui-serif",
10
+ "ui-sans-serif",
11
+ "ui-monospace",
12
+ "ui-rounded",
13
+ "emoji",
14
+ "math",
15
+ "fangsong"
16
+ ]);
17
+ const fallbackFamilies = [
18
+ "monospace",
19
+ "serif",
20
+ "sans-serif"
21
+ ];
22
+ const sampleText = "mmmmmmmmmmlli WwQq 0123456789";
23
+ function firstFamily(family) {
24
+ return (family.split(",")[0] ?? family).trim().replace(/^["']|["']$/g, "");
25
+ }
26
+ function cssFamily(family) {
27
+ return genericFamilies.has(family.toLowerCase()) ? family : `"${family.replace(/"/g, "\\\"")}"`;
28
+ }
29
+ function fontDescriptor(font, size = 32) {
30
+ return `${font.style} ${font.weight} ${size}px ${cssFamily(firstFamily(font.family))}`;
31
+ }
32
+ function createMeasuringContext() {
33
+ if (typeof document === "undefined") return null;
34
+ return document.createElement("canvas").getContext("2d");
35
+ }
36
+ function isFontRenderable(font, context = createMeasuringContext()) {
37
+ const family = firstFamily(font.family);
38
+ if (context === null || genericFamilies.has(family.toLowerCase())) return true;
39
+ return fallbackFamilies.some((fallback) => {
40
+ context.font = `${font.style} ${font.weight} 72px ${fallback}`;
41
+ const fallbackWidth = context.measureText(sampleText).width;
42
+ context.font = `${font.style} ${font.weight} 72px ${cssFamily(family)}, ${fallback}`;
43
+ return context.measureText(sampleText).width !== fallbackWidth;
44
+ });
45
+ }
46
+ async function prepareFont(font, loadFont) {
47
+ if (loadFont) await loadFont(font);
48
+ const fontSet = typeof document === "undefined" ? void 0 : document.fonts;
49
+ if (fontSet) await fontSet.load(fontDescriptor(font), sampleText);
50
+ }
51
+ async function findUnavailableFonts(fonts, loadFont) {
52
+ await Promise.allSettled(fonts.map((font) => prepareFont(font, loadFont)));
53
+ const context = createMeasuringContext();
54
+ return fonts.filter((font) => !isFontRenderable(font, context));
55
+ }
56
+ //#endregion
57
+ export { findUnavailableFonts, fontDescriptor, isFontRenderable };
@@ -0,0 +1,36 @@
1
+ let fabric = require("fabric");
2
+ //#region src/assets/image-check.ts
3
+ function isCrossOriginUrl(url) {
4
+ const location = globalThis.location;
5
+ if (!location || url.startsWith("data:") || url.startsWith("blob:")) return false;
6
+ try {
7
+ const parsed = new URL(url, location.href);
8
+ return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.origin !== location.origin;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+ function isPortableUrl(url) {
14
+ return !url.startsWith("blob:");
15
+ }
16
+ function isSameUrl(first, second) {
17
+ if (first === second) return true;
18
+ const location = globalThis.location;
19
+ try {
20
+ return new URL(first, location?.href).href === new URL(second, location?.href).href;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+ async function findMissingImages(images, signal) {
26
+ const results = await Promise.allSettled(images.map((image) => fabric.util.loadImage(image.url, {
27
+ signal,
28
+ crossOrigin: image.crossOrigin ?? null
29
+ })));
30
+ return images.filter((_, index) => results[index].status === "rejected").map((image) => image.url);
31
+ }
32
+ //#endregion
33
+ exports.findMissingImages = findMissingImages;
34
+ exports.isCrossOriginUrl = isCrossOriginUrl;
35
+ exports.isPortableUrl = isPortableUrl;
36
+ exports.isSameUrl = isSameUrl;
@@ -0,0 +1,33 @@
1
+ import { util } from "fabric";
2
+ //#region src/assets/image-check.ts
3
+ function isCrossOriginUrl(url) {
4
+ const location = globalThis.location;
5
+ if (!location || url.startsWith("data:") || url.startsWith("blob:")) return false;
6
+ try {
7
+ const parsed = new URL(url, location.href);
8
+ return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.origin !== location.origin;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+ function isPortableUrl(url) {
14
+ return !url.startsWith("blob:");
15
+ }
16
+ function isSameUrl(first, second) {
17
+ if (first === second) return true;
18
+ const location = globalThis.location;
19
+ try {
20
+ return new URL(first, location?.href).href === new URL(second, location?.href).href;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+ async function findMissingImages(images, signal) {
26
+ const results = await Promise.allSettled(images.map((image) => util.loadImage(image.url, {
27
+ signal,
28
+ crossOrigin: image.crossOrigin ?? null
29
+ })));
30
+ return images.filter((_, index) => results[index].status === "rejected").map((image) => image.url);
31
+ }
32
+ //#endregion
33
+ export { findMissingImages, isCrossOriginUrl, isPortableUrl, isSameUrl };
@@ -1,3 +1,4 @@
1
+ import { AssetManifest } from "../assets/asset-manifest.cjs";
1
2
  //#region src/document/document-format.d.ts
2
3
  export declare const CURRENT_SCHEMA_VERSION = 1;
3
4
  export interface SerializedFabricObject {
@@ -21,6 +22,7 @@ export interface FabricDocument {
21
22
  fabricVersion?: string;
22
23
  canvas: DocumentCanvas;
23
24
  objects: SerializedFabricObject[];
25
+ assets?: AssetManifest;
24
26
  metadata: Record<string, unknown>;
25
27
  }
26
28
  export interface DocumentInfo {
@@ -1,3 +1,4 @@
1
+ import { AssetManifest } from "../assets/asset-manifest.js";
1
2
  //#region src/document/document-format.d.ts
2
3
  export declare const CURRENT_SCHEMA_VERSION = 1;
3
4
  export interface SerializedFabricObject {
@@ -21,6 +22,7 @@ export interface FabricDocument {
21
22
  fabricVersion?: string;
22
23
  canvas: DocumentCanvas;
23
24
  objects: SerializedFabricObject[];
25
+ assets?: AssetManifest;
24
26
  metadata: Record<string, unknown>;
25
27
  }
26
28
  export interface DocumentInfo {
@@ -58,6 +58,10 @@ function validateDocument(value) {
58
58
  if (typeof value.createdAt !== "string") issues.push(invalid("createdAt", "must be an ISO date string"));
59
59
  if (typeof value.updatedAt !== "string") issues.push(invalid("updatedAt", "must be an ISO date string"));
60
60
  if (value.revision !== void 0 && !(Number.isInteger(value.revision) && value.revision >= 0)) issues.push(invalid("revision", "must be a whole number of zero or more when present"));
61
+ if (value.assets !== void 0) {
62
+ const assets = value.assets;
63
+ if (!isPlainObject(assets) || !Array.isArray(assets.images) || !Array.isArray(assets.fonts)) issues.push(invalid("assets", "must be an object with images and fonts arrays when present"));
64
+ }
61
65
  if (!isPlainObject(value.metadata)) issues.push(invalid("metadata", "must be an object"));
62
66
  if (!isPlainObject(value.canvas)) issues.push(invalid("canvas", "must be an object"));
63
67
  else {
@@ -58,6 +58,10 @@ function validateDocument(value) {
58
58
  if (typeof value.createdAt !== "string") issues.push(invalid("createdAt", "must be an ISO date string"));
59
59
  if (typeof value.updatedAt !== "string") issues.push(invalid("updatedAt", "must be an ISO date string"));
60
60
  if (value.revision !== void 0 && !(Number.isInteger(value.revision) && value.revision >= 0)) issues.push(invalid("revision", "must be a whole number of zero or more when present"));
61
+ if (value.assets !== void 0) {
62
+ const assets = value.assets;
63
+ if (!isPlainObject(assets) || !Array.isArray(assets.images) || !Array.isArray(assets.fonts)) issues.push(invalid("assets", "must be an object with images and fonts arrays when present"));
64
+ }
61
65
  if (!isPlainObject(value.metadata)) issues.push(invalid("metadata", "must be an object"));
62
66
  if (!isPlainObject(value.canvas)) issues.push(invalid("canvas", "must be an object"));
63
67
  else {
@@ -1,3 +1,7 @@
1
+ const require_asset_manifest = require("../assets/asset-manifest.cjs");
2
+ const require_errors = require("./errors.cjs");
3
+ const require_image_check = require("../assets/image-check.cjs");
4
+ const require_asset_pipeline = require("../assets/asset-pipeline.cjs");
1
5
  require("../document/document-format.cjs");
2
6
  const require_ids = require("../document/ids.cjs");
3
7
  const require_create_document = require("../document/create-document.cjs");
@@ -5,12 +9,15 @@ const require_validate_document = require("../document/validate-document.cjs");
5
9
  const require_walk_objects = require("../fabric/walk-objects.cjs");
6
10
  const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
7
11
  const require_object_ids = require("../fabric/object-ids.cjs");
8
- const require_errors = require("./errors.cjs");
9
12
  const require_object_registry = require("../fabric/object-registry.cjs");
10
13
  const require_create_history = require("../history/create-history.cjs");
11
14
  const require_save_controller = require("../save/save-controller.cjs");
12
15
  const require_event_emitter = require("./event-emitter.cjs");
13
16
  //#region src/engine/create-document-engine.ts
17
+ function isReplaceableImage(object) {
18
+ const candidate = object;
19
+ return typeof candidate.getSrc === "function" && typeof candidate.setSrc === "function";
20
+ }
14
21
  function describeDocumentId(value) {
15
22
  const id = value?.id;
16
23
  return typeof id === "string" ? id : void 0;
@@ -21,6 +28,8 @@ function createDocumentEngine(options) {
21
28
  const registry = require_object_registry.createObjectRegistry(options.customObjects);
22
29
  const events = require_event_emitter.createEventEmitter();
23
30
  const objectsById = /* @__PURE__ */ new Map();
31
+ const assetOptions = options.assets ?? {};
32
+ const uploadedUrls = /* @__PURE__ */ new Map();
24
33
  let documentInfo = require_create_document.createDocumentInfo(options.document);
25
34
  let activeLoad = null;
26
35
  let destroyed = false;
@@ -65,7 +74,11 @@ function createDocumentEngine(options) {
65
74
  });
66
75
  const saving = require_save_controller.createSaveController({
67
76
  getStorage: () => requireStorage(),
68
- createDocument: () => toDocument(),
77
+ createDocument: async () => {
78
+ const prepared = await require_asset_pipeline.prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls);
79
+ if (prepared.warnings.length > 0) events.emit("assets:warning", { warnings: prepared.warnings });
80
+ return prepared.document;
81
+ },
69
82
  retry: options.saveRetry,
70
83
  autosave: options.autosave === true ? {} : options.autosave ?? false,
71
84
  onStateChange: (state) => events.emit("save:status", state),
@@ -93,6 +106,7 @@ function createDocumentEngine(options) {
93
106
  height: canvas.getHeight()
94
107
  },
95
108
  objects: serialized.objects,
109
+ assets: require_asset_manifest.buildAssetManifest(serialized.objects),
96
110
  metadata: { ...documentInfo.metadata }
97
111
  };
98
112
  if (serialized.version !== void 0) document.fabricVersion = serialized.version;
@@ -100,8 +114,8 @@ function createDocumentEngine(options) {
100
114
  return document;
101
115
  }
102
116
  function toLoadError(error, signal) {
103
- if (require_errors.isDocumentEngineError(error)) return error;
104
117
  if (signal.aborted) return new require_errors.DocumentEngineError("LOAD_ABORTED", "Loading stopped because a newer load started", { cause: error });
118
+ if (require_errors.isDocumentEngineError(error)) return error;
105
119
  const reason = error instanceof Error ? error.message : String(error);
106
120
  return new require_errors.DocumentEngineError("LOAD_FAILED", `Fabric could not load the document: ${reason}`, { cause: error });
107
121
  }
@@ -123,7 +137,9 @@ function createDocumentEngine(options) {
123
137
  activeLoad = controller;
124
138
  events.emit("load:start", { documentId: describeDocumentId(input) });
125
139
  try {
126
- const document = checkDocument(input);
140
+ const checked = checkDocument(input);
141
+ const { document, warnings } = await require_asset_pipeline.prepareAssetsForLoad(checked, assetOptions, controller.signal);
142
+ if (controller.signal.aborted) throw new Error("aborted");
127
143
  await history.withoutRecording(() => require_fabric_adapter.loadIntoCanvas(canvas, {
128
144
  version: document.fabricVersion,
129
145
  background: document.canvas.background,
@@ -144,7 +160,11 @@ function createDocumentEngine(options) {
144
160
  history.reset();
145
161
  saving.startSession(document.revision ?? 0);
146
162
  canvas.requestRenderAll();
147
- events.emit("load:success", { document });
163
+ if (warnings.length > 0) events.emit("assets:warning", { warnings });
164
+ events.emit("load:success", {
165
+ document,
166
+ warnings
167
+ });
148
168
  return document;
149
169
  } catch (error) {
150
170
  const engineError = toLoadError(error, controller.signal);
@@ -186,6 +206,7 @@ function createDocumentEngine(options) {
186
206
  history.withoutRecording(() => canvas.clear());
187
207
  objectsById.clear();
188
208
  documentInfo = require_create_document.createDocumentInfo(newOptions);
209
+ uploadedUrls.clear();
189
210
  history.reset();
190
211
  saving.startSession(0);
191
212
  }
@@ -196,6 +217,30 @@ function createDocumentEngine(options) {
196
217
  rebuildIndex();
197
218
  return objectsById.get(id);
198
219
  }
220
+ function checkAssets() {
221
+ ensureUsable();
222
+ return require_asset_pipeline.inspectAssets(toDocument(), assetOptions, new AbortController().signal);
223
+ }
224
+ async function replaceImage(oldUrl, newUrl) {
225
+ ensureUsable();
226
+ const images = [];
227
+ require_walk_objects.walkObjects(canvas.getObjects(), (object) => {
228
+ if (isReplaceableImage(object) && require_image_check.isSameUrl(object.getSrc(), oldUrl)) images.push(object);
229
+ });
230
+ if (images.length === 0) return 0;
231
+ await history.transaction("Replace image", () => Promise.all(images.map(async (image) => {
232
+ const displayedWidth = image.width * image.scaleX;
233
+ const displayedHeight = image.height * image.scaleY;
234
+ await image.setSrc(newUrl, { crossOrigin: image.crossOrigin ?? null });
235
+ image.set({
236
+ scaleX: displayedWidth / image.width,
237
+ scaleY: displayedHeight / image.height
238
+ });
239
+ image.setCoords();
240
+ })));
241
+ canvas.requestRenderAll();
242
+ return images.length;
243
+ }
199
244
  async function travelThroughHistory(direction) {
200
245
  ensureUsable();
201
246
  try {
@@ -247,6 +292,12 @@ function createDocumentEngine(options) {
247
292
  registry.register(definition);
248
293
  },
249
294
  getObjectById,
295
+ getAssetManifest: () => toDocument().assets ?? {
296
+ images: [],
297
+ fonts: []
298
+ },
299
+ checkAssets,
300
+ replaceImage,
250
301
  transaction(label, work) {
251
302
  ensureUsable();
252
303
  return history.transaction(label, work);
@@ -1,4 +1,6 @@
1
1
  import { DocumentInfo, FabricDocument } from "../document/document-format.cjs";
2
+ import { AssetManifest } from "../assets/asset-manifest.cjs";
3
+ import { AssetOptions, AssetReport, AssetWarning } from "../assets/asset-pipeline.cjs";
2
4
  import { NewDocumentOptions } from "../document/create-document.cjs";
3
5
  import { CustomObjectDefinition } from "../fabric/object-registry.cjs";
4
6
  import { HistoryOptions, HistoryState } from "../history/create-history.cjs";
@@ -18,6 +20,7 @@ export interface DocumentEngineOptions {
18
20
  history?: HistoryOptions;
19
21
  autosave?: boolean | AutosaveOptions;
20
22
  saveRetry?: RetryOptions;
23
+ assets?: AssetOptions;
21
24
  }
22
25
  export interface LoadOptions {
23
26
  restoreCanvasSize?: boolean;
@@ -28,6 +31,10 @@ export interface DocumentEngineEvents {
28
31
  };
29
32
  "load:success": {
30
33
  document: FabricDocument;
34
+ warnings: AssetWarning[];
35
+ };
36
+ "assets:warning": {
37
+ warnings: AssetWarning[];
31
38
  };
32
39
  "load:error": {
33
40
  error: DocumentEngineError;
@@ -72,6 +79,9 @@ export interface DocumentEngine {
72
79
  };
73
80
  clearHistory(): void;
74
81
  getObjectById(id: string): FabricObject | undefined;
82
+ getAssetManifest(): AssetManifest;
83
+ checkAssets(): Promise<AssetReport>;
84
+ replaceImage(oldUrl: string, newUrl: string): Promise<number>;
75
85
  on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
76
86
  destroy(): void;
77
87
  }
@@ -1,4 +1,6 @@
1
1
  import { DocumentInfo, FabricDocument } from "../document/document-format.js";
2
+ import { AssetManifest } from "../assets/asset-manifest.js";
3
+ import { AssetOptions, AssetReport, AssetWarning } from "../assets/asset-pipeline.js";
2
4
  import { NewDocumentOptions } from "../document/create-document.js";
3
5
  import { CustomObjectDefinition } from "../fabric/object-registry.js";
4
6
  import { HistoryOptions, HistoryState } from "../history/create-history.js";
@@ -18,6 +20,7 @@ export interface DocumentEngineOptions {
18
20
  history?: HistoryOptions;
19
21
  autosave?: boolean | AutosaveOptions;
20
22
  saveRetry?: RetryOptions;
23
+ assets?: AssetOptions;
21
24
  }
22
25
  export interface LoadOptions {
23
26
  restoreCanvasSize?: boolean;
@@ -28,6 +31,10 @@ export interface DocumentEngineEvents {
28
31
  };
29
32
  "load:success": {
30
33
  document: FabricDocument;
34
+ warnings: AssetWarning[];
35
+ };
36
+ "assets:warning": {
37
+ warnings: AssetWarning[];
31
38
  };
32
39
  "load:error": {
33
40
  error: DocumentEngineError;
@@ -72,6 +79,9 @@ export interface DocumentEngine {
72
79
  };
73
80
  clearHistory(): void;
74
81
  getObjectById(id: string): FabricObject | undefined;
82
+ getAssetManifest(): AssetManifest;
83
+ checkAssets(): Promise<AssetReport>;
84
+ replaceImage(oldUrl: string, newUrl: string): Promise<number>;
75
85
  on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
76
86
  destroy(): void;
77
87
  }