fabricjs-document-engine 0.2.0 → 0.4.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 +113 -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 +175 -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 +112 -10
- package/dist/engine/create-document-engine.d.cts +28 -0
- package/dist/engine/create-document-engine.d.ts +28 -0
- package/dist/engine/create-document-engine.js +112 -10
- 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 +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +1 -1
- package/dist/recovery/indexed-db-recovery.cjs +80 -0
- package/dist/recovery/indexed-db-recovery.d.cts +8 -0
- package/dist/recovery/indexed-db-recovery.d.ts +8 -0
- package/dist/recovery/indexed-db-recovery.js +80 -0
- package/dist/recovery/recovery-controller.cjs +159 -0
- package/dist/recovery/recovery-controller.d.cts +19 -0
- package/dist/recovery/recovery-controller.d.ts +19 -0
- package/dist/recovery/recovery-controller.js +158 -0
- package/dist/recovery/recovery-store.cjs +19 -0
- package/dist/recovery/recovery-store.d.cts +10 -0
- package/dist/recovery/recovery-store.d.ts +10 -0
- package/dist/recovery/recovery-store.js +19 -0
- package/dist/recovery.cjs +5 -0
- package/dist/recovery.d.cts +4 -0
- package/dist/recovery.d.ts +4 -0
- package/dist/recovery.js +3 -0
- package/dist/save/save-controller.cjs +6 -5
- package/dist/save/save-controller.js +6 -5
- package/package.json +13 -2
|
@@ -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, rewriteUrls };
|
|
@@ -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 {
|