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.
- package/README.md +77 -13
- package/dist/assets/asset-manifest.cjs +43 -0
- package/dist/assets/asset-manifest.d.cts +16 -0
- package/dist/assets/asset-manifest.d.ts +16 -0
- package/dist/assets/asset-manifest.js +41 -0
- package/dist/assets/asset-pipeline.cjs +174 -0
- package/dist/assets/asset-pipeline.d.cts +31 -0
- package/dist/assets/asset-pipeline.d.ts +31 -0
- package/dist/assets/asset-pipeline.js +172 -0
- package/dist/assets/asset-references.cjs +83 -0
- package/dist/assets/asset-references.js +82 -0
- package/dist/assets/font-check.cjs +59 -0
- package/dist/assets/font-check.d.cts +4 -0
- package/dist/assets/font-check.d.ts +4 -0
- package/dist/assets/font-check.js +57 -0
- package/dist/assets/image-check.cjs +36 -0
- package/dist/assets/image-check.js +33 -0
- package/dist/document/document-format.d.cts +2 -0
- package/dist/document/document-format.d.ts +2 -0
- package/dist/document/validate-document.cjs +4 -0
- package/dist/document/validate-document.js +4 -0
- package/dist/engine/create-document-engine.cjs +56 -5
- package/dist/engine/create-document-engine.d.cts +10 -0
- package/dist/engine/create-document-engine.d.ts +10 -0
- package/dist/engine/create-document-engine.js +56 -5
- package/dist/engine/errors.cjs +2 -0
- package/dist/engine/errors.d.cts +6 -1
- package/dist/engine/errors.d.ts +6 -1
- package/dist/engine/errors.js +2 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -1
- package/dist/save/save-controller.cjs +6 -5
- package/dist/save/save-controller.js +6 -5
- 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,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: () =>
|
|
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
|
|
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("
|
|
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
|
}
|