fabricjs-document-engine 0.1.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 (70) hide show
  1. package/README.md +162 -21
  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 +3 -0
  19. package/dist/document/document-format.d.ts +3 -0
  20. package/dist/document/validate-document.cjs +5 -0
  21. package/dist/document/validate-document.js +5 -0
  22. package/dist/engine/create-document-engine.cjs +82 -18
  23. package/dist/engine/create-document-engine.d.cts +21 -5
  24. package/dist/engine/create-document-engine.d.ts +21 -5
  25. package/dist/engine/create-document-engine.js +82 -18
  26. package/dist/engine/errors.cjs +7 -0
  27. package/dist/engine/errors.d.cts +9 -1
  28. package/dist/engine/errors.d.ts +9 -1
  29. package/dist/engine/errors.js +7 -1
  30. package/dist/history/create-history.cjs +3 -1
  31. package/dist/history/create-history.js +3 -1
  32. package/dist/index.cjs +4 -1
  33. package/dist/index.d.cts +11 -3
  34. package/dist/index.d.ts +11 -3
  35. package/dist/index.js +3 -2
  36. package/dist/save/autosave-scheduler.cjs +28 -0
  37. package/dist/save/autosave-scheduler.d.cts +6 -0
  38. package/dist/save/autosave-scheduler.d.ts +6 -0
  39. package/dist/save/autosave-scheduler.js +28 -0
  40. package/dist/save/retry.cjs +33 -0
  41. package/dist/save/retry.d.cts +7 -0
  42. package/dist/save/retry.d.ts +7 -0
  43. package/dist/save/retry.js +31 -0
  44. package/dist/save/save-controller.cjs +149 -0
  45. package/dist/save/save-controller.d.cts +20 -0
  46. package/dist/save/save-controller.d.ts +20 -0
  47. package/dist/save/save-controller.js +149 -0
  48. package/dist/save/unsaved-changes-warning.cjs +12 -0
  49. package/dist/save/unsaved-changes-warning.d.cts +6 -0
  50. package/dist/save/unsaved-changes-warning.d.ts +6 -0
  51. package/dist/save/unsaved-changes-warning.js +12 -0
  52. package/dist/storage/key-value-storage.cjs +35 -0
  53. package/dist/storage/key-value-storage.d.cts +14 -0
  54. package/dist/storage/key-value-storage.d.ts +14 -0
  55. package/dist/storage/key-value-storage.js +35 -0
  56. package/dist/storage/local-storage.cjs +24 -0
  57. package/dist/storage/local-storage.d.cts +8 -0
  58. package/dist/storage/local-storage.d.ts +8 -0
  59. package/dist/storage/local-storage.js +24 -0
  60. package/dist/storage/memory-storage.cjs +17 -0
  61. package/dist/storage/memory-storage.d.cts +4 -0
  62. package/dist/storage/memory-storage.d.ts +4 -0
  63. package/dist/storage/memory-storage.js +17 -0
  64. package/dist/storage/storage-contract.d.cts +14 -0
  65. package/dist/storage/storage-contract.d.ts +14 -0
  66. package/dist/storage.cjs +7 -0
  67. package/dist/storage.d.cts +5 -0
  68. package/dist/storage.d.ts +5 -0
  69. package/dist/storage.js +4 -0
  70. package/package.json +14 -3
@@ -0,0 +1,172 @@
1
+ import { findImageReferences } from "./asset-references.js";
2
+ import { buildAssetManifest, isEmbeddedUrl } from "./asset-manifest.js";
3
+ import { DocumentEngineError, isDocumentEngineError } from "../engine/errors.js";
4
+ import { findUnavailableFonts } from "./font-check.js";
5
+ import { findMissingImages, isCrossOriginUrl, isPortableUrl } from "./image-check.js";
6
+ //#region src/assets/asset-pipeline.ts
7
+ function cloneDocument(document) {
8
+ return JSON.parse(JSON.stringify(document));
9
+ }
10
+ function groupByUrl(references) {
11
+ const groups = /* @__PURE__ */ new Map();
12
+ for (const reference of references) {
13
+ const group = groups.get(reference.url) ?? [];
14
+ group.push(reference);
15
+ groups.set(reference.url, group);
16
+ }
17
+ return groups;
18
+ }
19
+ function objectIdsOf(references) {
20
+ return [...new Set(references.map((reference) => reference.objectId).filter((id) => id !== void 0))];
21
+ }
22
+ function pointTo(references, url) {
23
+ for (const reference of references) {
24
+ reference.holder[reference.key] = url;
25
+ reference.url = url;
26
+ }
27
+ }
28
+ async function rewriteUrls(document, rewrite) {
29
+ const groups = groupByUrl(findImageReferences(document.objects));
30
+ await Promise.all([...groups].map(async ([url, references]) => {
31
+ const next = await rewrite(url, references);
32
+ if (next !== url) pointTo(references, next);
33
+ }));
34
+ }
35
+ function crossOriginWarnings(document) {
36
+ const warnings = [];
37
+ for (const [url, references] of groupByUrl(findImageReferences(document.objects))) {
38
+ if (!isCrossOriginUrl(url) || references.every((reference) => reference.crossOrigin)) continue;
39
+ warnings.push({
40
+ code: "IMAGE_CROSS_ORIGIN",
41
+ message: `The image ${url} comes from another site without crossOrigin set, so exporting the canvas will fail`,
42
+ url,
43
+ objectIds: objectIdsOf(references)
44
+ });
45
+ }
46
+ return warnings;
47
+ }
48
+ function fontWarnings(fonts) {
49
+ return fonts.map((font) => ({
50
+ code: "FONT_UNAVAILABLE",
51
+ message: `The font "${font.family}" (${font.weight}, ${font.style}) is not available, text will use a fallback font`,
52
+ family: font.family,
53
+ objectIds: font.objectIds
54
+ }));
55
+ }
56
+ async function inspectAssets(document, options, signal) {
57
+ const manifest = buildAssetManifest(document.objects);
58
+ const uniqueImages = [...groupByUrl(findImageReferences(document.objects).filter((reference) => !isEmbeddedUrl(reference.url)))].map(([url, references]) => ({
59
+ url,
60
+ crossOrigin: references[0].crossOrigin
61
+ }));
62
+ const [missingUrls, unavailableFonts] = await Promise.all([options.checkImages === false ? Promise.resolve([]) : findMissingImages(uniqueImages, signal), findUnavailableFonts(manifest.fonts, options.loadFont)]);
63
+ const missing = new Set(missingUrls);
64
+ return {
65
+ manifest,
66
+ missingImages: manifest.images.filter((image) => missing.has(image.url)),
67
+ unavailableFonts,
68
+ warnings: [...crossOriginWarnings(document), ...fontWarnings(unavailableFonts)]
69
+ };
70
+ }
71
+ async function replaceMissingImages(document, missingImages, options, signal) {
72
+ const replace = options.replaceMissingImage;
73
+ if (!replace || missingImages.length === 0) return {
74
+ stillMissing: [...missingImages],
75
+ warnings: []
76
+ };
77
+ const replacements = /* @__PURE__ */ new Map();
78
+ for (const image of missingImages) {
79
+ const replacement = await replace(image);
80
+ if (typeof replacement === "string" && replacement.length > 0) replacements.set(image.url, replacement);
81
+ }
82
+ const brokenReplacements = new Set(await findMissingImages([...replacements.values()].map((url) => ({
83
+ url,
84
+ crossOrigin: null
85
+ })), signal));
86
+ const stillMissing = [];
87
+ const warnings = [];
88
+ const groups = groupByUrl(findImageReferences(document.objects));
89
+ for (const image of missingImages) {
90
+ const replacement = replacements.get(image.url);
91
+ if (replacement === void 0 || brokenReplacements.has(replacement)) {
92
+ stillMissing.push(image);
93
+ continue;
94
+ }
95
+ pointTo(groups.get(image.url) ?? [], replacement);
96
+ warnings.push({
97
+ code: "IMAGE_REPLACED",
98
+ message: `The missing image ${image.url} was replaced with ${replacement}`,
99
+ url: image.url,
100
+ objectIds: image.objectIds
101
+ });
102
+ }
103
+ return {
104
+ stillMissing,
105
+ warnings
106
+ };
107
+ }
108
+ async function prepareAssetsForLoad(input, options, signal) {
109
+ const document = cloneDocument(input);
110
+ const { resolveUrl } = options;
111
+ if (resolveUrl) await rewriteUrls(document, async (url) => resolveUrl(url));
112
+ const report = await inspectAssets(document, options, signal);
113
+ if (options.requireFonts && report.unavailableFonts.length > 0) {
114
+ const families = report.unavailableFonts.map((font) => font.family).join(", ");
115
+ throw new DocumentEngineError("MISSING_FONTS", `These fonts are not available: ${families}`, { missingFonts: report.unavailableFonts });
116
+ }
117
+ const { stillMissing, warnings } = await replaceMissingImages(document, report.missingImages, options, signal);
118
+ if (stillMissing.length > 0) {
119
+ const urls = stillMissing.map((image) => image.url).join(", ");
120
+ throw new DocumentEngineError("MISSING_ASSETS", `These images could not be loaded: ${urls}`, { missingAssets: stillMissing });
121
+ }
122
+ return {
123
+ document,
124
+ warnings: [...report.warnings, ...warnings]
125
+ };
126
+ }
127
+ async function uploadOnce(url, references, upload) {
128
+ try {
129
+ return await upload({
130
+ url,
131
+ blob: await (await fetch(url)).blob(),
132
+ objectIds: objectIdsOf(references)
133
+ });
134
+ } catch (error) {
135
+ if (isDocumentEngineError(error)) throw error;
136
+ const reason = error instanceof Error ? error.message : String(error);
137
+ throw new DocumentEngineError("ASSET_UPLOAD_FAILED", `Could not upload the image ${url.slice(0, 60)}: ${reason}`, {
138
+ cause: error,
139
+ retryable: true
140
+ });
141
+ }
142
+ }
143
+ async function prepareAssetsForSave(document, options, uploadedUrls) {
144
+ const { upload } = options;
145
+ const warnings = [];
146
+ await rewriteUrls(document, async (url, references) => {
147
+ if (!(url.startsWith("blob:") || url.startsWith("data:"))) return url;
148
+ if (!upload) {
149
+ if (!isPortableUrl(url)) warnings.push({
150
+ code: "ASSET_NOT_PORTABLE",
151
+ message: `The image ${url} only exists in this browser tab. Pass assets.upload to store it before saving.`,
152
+ url,
153
+ objectIds: objectIdsOf(references)
154
+ });
155
+ return url;
156
+ }
157
+ let uploading = uploadedUrls.get(url);
158
+ if (!uploading) {
159
+ uploading = uploadOnce(url, references, upload);
160
+ uploadedUrls.set(url, uploading);
161
+ uploading.catch(() => uploadedUrls.delete(url));
162
+ }
163
+ return uploading;
164
+ });
165
+ document.assets = buildAssetManifest(document.objects);
166
+ return {
167
+ document,
168
+ warnings
169
+ };
170
+ }
171
+ //#endregion
172
+ export { inspectAssets, prepareAssetsForLoad, prepareAssetsForSave };
@@ -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 {
@@ -17,9 +18,11 @@ export interface FabricDocument {
17
18
  id: string;
18
19
  createdAt: string;
19
20
  updatedAt: string;
21
+ revision?: number;
20
22
  fabricVersion?: string;
21
23
  canvas: DocumentCanvas;
22
24
  objects: SerializedFabricObject[];
25
+ assets?: AssetManifest;
23
26
  metadata: Record<string, unknown>;
24
27
  }
25
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 {
@@ -17,9 +18,11 @@ export interface FabricDocument {
17
18
  id: string;
18
19
  createdAt: string;
19
20
  updatedAt: string;
21
+ revision?: number;
20
22
  fabricVersion?: string;
21
23
  canvas: DocumentCanvas;
22
24
  objects: SerializedFabricObject[];
25
+ assets?: AssetManifest;
23
26
  metadata: Record<string, unknown>;
24
27
  }
25
28
  export interface DocumentInfo {
@@ -57,6 +57,11 @@ function validateDocument(value) {
57
57
  if (typeof value.id !== "string" || value.id.length === 0) issues.push(invalid("id", "must be a non-empty string"));
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
+ 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
+ }
60
65
  if (!isPlainObject(value.metadata)) issues.push(invalid("metadata", "must be an object"));
61
66
  if (!isPlainObject(value.canvas)) issues.push(invalid("canvas", "must be an object"));
62
67
  else {
@@ -57,6 +57,11 @@ function validateDocument(value) {
57
57
  if (typeof value.id !== "string" || value.id.length === 0) issues.push(invalid("id", "must be a non-empty string"));
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
+ 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
+ }
60
65
  if (!isPlainObject(value.metadata)) issues.push(invalid("metadata", "must be an object"));
61
66
  if (!isPlainObject(value.canvas)) issues.push(invalid("canvas", "must be an object"));
62
67
  else {