fabricjs-document-engine 1.0.2 → 1.1.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 +181 -10
- package/README.zh-CN.md +181 -10
- package/dist/assets/asset-manifest.d.cts +3 -0
- package/dist/assets/asset-manifest.d.ts +3 -0
- package/dist/assets/asset-pipeline.cjs +22 -10
- package/dist/assets/asset-pipeline.d.cts +5 -1
- package/dist/assets/asset-pipeline.d.ts +5 -1
- package/dist/assets/asset-pipeline.js +22 -10
- package/dist/assets/font-check.cjs +9 -1
- package/dist/assets/font-check.js +9 -1
- package/dist/assets/image-check.cjs +88 -6
- package/dist/assets/image-check.d.cts +17 -0
- package/dist/assets/image-check.d.ts +17 -0
- package/dist/assets/image-check.js +88 -7
- package/dist/commands/clipboard.cjs +136 -0
- package/dist/commands/clipboard.d.cts +47 -0
- package/dist/commands/clipboard.d.ts +47 -0
- package/dist/commands/clipboard.js +135 -0
- package/dist/commands/layers.cjs +97 -0
- package/dist/commands/layers.d.cts +39 -0
- package/dist/commands/layers.d.ts +39 -0
- package/dist/commands/layers.js +92 -0
- package/dist/engine/create-document-engine.cjs +85 -14
- package/dist/engine/create-document-engine.d.cts +20 -1
- package/dist/engine/create-document-engine.d.ts +20 -1
- package/dist/engine/create-document-engine.js +86 -15
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -1
- package/dist/export/batch-render.cjs +172 -0
- package/dist/export/batch-render.d.cts +43 -0
- package/dist/export/batch-render.d.ts +43 -0
- package/dist/export/batch-render.js +171 -0
- package/dist/export/export-options.cjs +18 -1
- package/dist/export/export-options.d.cts +23 -0
- package/dist/export/export-options.d.ts +23 -0
- package/dist/export/export-options.js +18 -2
- package/dist/export/preflight-export.cjs +11 -2
- package/dist/export/preflight-export.d.cts +1 -1
- package/dist/export/preflight-export.d.ts +1 -1
- package/dist/export/preflight-export.js +11 -2
- package/dist/export/render-export.cjs +18 -3
- package/dist/export/render-export.js +18 -4
- package/dist/export/svg/embed-assets.cjs +249 -0
- package/dist/export/svg/embed-assets.js +249 -0
- package/dist/export/svg/overrides.cjs +32 -0
- package/dist/export/svg/overrides.js +32 -0
- package/dist/export/svg/text-on-path.cjs +252 -0
- package/dist/export/svg/text-on-path.js +249 -0
- package/dist/fabric/fabric-adapter.cjs +46 -5
- package/dist/fabric/fabric-adapter.js +46 -5
- package/dist/fabric/object-registry.cjs +1 -1
- package/dist/fabric/object-registry.js +1 -1
- package/dist/import/svg-import.cjs +256 -0
- package/dist/import/svg-import.d.cts +50 -0
- package/dist/import/svg-import.d.ts +50 -0
- package/dist/import/svg-import.js +256 -0
- package/dist/index.cjs +12 -0
- package/dist/index.d.cts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -1
- package/dist/pdf/export-pdf.cjs +440 -0
- package/dist/pdf/export-pdf.d.cts +73 -0
- package/dist/pdf/export-pdf.d.ts +73 -0
- package/dist/pdf/export-pdf.js +440 -0
- package/dist/pdf/fonts.cjs +115 -0
- package/dist/pdf/fonts.d.cts +12 -0
- package/dist/pdf/fonts.d.ts +12 -0
- package/dist/pdf/fonts.js +112 -0
- package/dist/pdf/page-layout.cjs +79 -0
- package/dist/pdf/page-layout.d.cts +51 -0
- package/dist/pdf/page-layout.d.ts +51 -0
- package/dist/pdf/page-layout.js +76 -0
- package/dist/pdf/text-decorations.cjs +108 -0
- package/dist/pdf/text-decorations.js +107 -0
- package/dist/pdf.cjs +7 -0
- package/dist/pdf.d.cts +4 -0
- package/dist/pdf.d.ts +4 -0
- package/dist/pdf.js +3 -0
- package/dist/react/use-layers.cjs +53 -0
- package/dist/react/use-layers.d.cts +10 -0
- package/dist/react/use-layers.d.ts +10 -0
- package/dist/react/use-layers.js +53 -0
- package/dist/react.cjs +2 -0
- package/dist/react.d.cts +3 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.js +2 -1
- package/dist/util/concurrency.cjs +28 -0
- package/dist/util/concurrency.js +27 -0
- package/package.json +30 -3
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const require_errors = require("../engine/errors.cjs");
|
|
2
|
+
const require_walk_objects = require("../fabric/walk-objects.cjs");
|
|
3
|
+
const require_create_document_engine = require("../engine/create-document-engine.cjs");
|
|
4
|
+
let fabric = require("fabric");
|
|
5
|
+
//#region src/export/batch-render.ts
|
|
6
|
+
/**
|
|
7
|
+
* Releases a canvas's pixels now. Fabric's `dispose` drops its references,
|
|
8
|
+
* but in browsers the memory behind a canvas comes back only much later,
|
|
9
|
+
* when the garbage collector gets to it.
|
|
10
|
+
*/
|
|
11
|
+
function releaseCanvas(value) {
|
|
12
|
+
if (typeof HTMLCanvasElement !== "undefined" && value instanceof HTMLCanvasElement) {
|
|
13
|
+
value.width = 0;
|
|
14
|
+
value.height = 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function disposeObject(object) {
|
|
18
|
+
const owned = object;
|
|
19
|
+
for (const key of [
|
|
20
|
+
"_cacheCanvas",
|
|
21
|
+
"_filteredEl",
|
|
22
|
+
"_element",
|
|
23
|
+
"_originalElement"
|
|
24
|
+
]) releaseCanvas(owned[key]);
|
|
25
|
+
object.dispose?.();
|
|
26
|
+
}
|
|
27
|
+
/** Frees the objects on a canvas: their cache canvases and image elements. */
|
|
28
|
+
function disposeObjects(canvas) {
|
|
29
|
+
const objects = canvas.getObjects();
|
|
30
|
+
canvas.remove(...objects);
|
|
31
|
+
require_walk_objects.walkObjects(objects, (object) => {
|
|
32
|
+
disposeObject(object);
|
|
33
|
+
if (object.clipPath) disposeObject(object.clipPath);
|
|
34
|
+
});
|
|
35
|
+
for (const key of ["backgroundImage", "overlayImage"]) {
|
|
36
|
+
const image = canvas[key];
|
|
37
|
+
if (image) {
|
|
38
|
+
disposeObject(image);
|
|
39
|
+
canvas[key] = void 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function createSlot(options) {
|
|
44
|
+
if (typeof document === "undefined") throw new require_errors.DocumentEngineError("EXPORT_FAILED", "Rendering documents needs a browser DOM");
|
|
45
|
+
const element = document.createElement("canvas");
|
|
46
|
+
const canvas = new fabric.StaticCanvas(element, {
|
|
47
|
+
renderOnAddRemove: false,
|
|
48
|
+
enableRetinaScaling: false
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
element,
|
|
52
|
+
canvas,
|
|
53
|
+
engine: require_create_document_engine.createDocumentEngine({
|
|
54
|
+
canvas,
|
|
55
|
+
customObjects: options.customObjects,
|
|
56
|
+
assets: options.assets,
|
|
57
|
+
limits: options.limits,
|
|
58
|
+
history: { limit: 1 }
|
|
59
|
+
})
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function releaseSlot(slot) {
|
|
63
|
+
disposeObjects(slot.canvas);
|
|
64
|
+
slot.engine.destroy();
|
|
65
|
+
await slot.canvas.dispose();
|
|
66
|
+
slot.element.width = 0;
|
|
67
|
+
slot.element.height = 0;
|
|
68
|
+
}
|
|
69
|
+
function toEngineError(error) {
|
|
70
|
+
if (require_errors.isDocumentEngineError(error)) return error;
|
|
71
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
72
|
+
return new require_errors.DocumentEngineError("EXPORT_FAILED", `The document could not be rendered: ${reason}`, { cause: error });
|
|
73
|
+
}
|
|
74
|
+
function documentIdOf(value) {
|
|
75
|
+
const id = value?.id;
|
|
76
|
+
return typeof id === "string" ? id : void 0;
|
|
77
|
+
}
|
|
78
|
+
async function renderOne(slot, input, options, signal) {
|
|
79
|
+
const { concurrency: _concurrency, onProgress: _onProgress, customObjects: _custom, assets: _assets, limits: _limits, ...exportOptions } = options;
|
|
80
|
+
try {
|
|
81
|
+
await slot.engine.loadDocument(input, {
|
|
82
|
+
signal,
|
|
83
|
+
discardUnsavedChanges: true
|
|
84
|
+
});
|
|
85
|
+
return await slot.engine.export({
|
|
86
|
+
...exportOptions,
|
|
87
|
+
signal
|
|
88
|
+
});
|
|
89
|
+
} finally {
|
|
90
|
+
disposeObjects(slot.canvas);
|
|
91
|
+
slot.engine.clearHistory();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Renders each document and yields the results as they finish, so they can
|
|
96
|
+
* be uploaded or saved one by one instead of all being held in memory.
|
|
97
|
+
* Documents can be `FabricDocument`s or plain Fabric JSON, and can come from
|
|
98
|
+
* an async iterable, such as pages of a database query.
|
|
99
|
+
*/
|
|
100
|
+
async function* renderDocuments(documents, options) {
|
|
101
|
+
const concurrency = Math.max(1, Math.floor(options.concurrency ?? 2));
|
|
102
|
+
const total = Array.isArray(documents) ? documents.length : void 0;
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const stop = () => controller.abort();
|
|
105
|
+
options.signal?.addEventListener("abort", stop, { once: true });
|
|
106
|
+
if (options.signal?.aborted) stop();
|
|
107
|
+
const iterator = Symbol.asyncIterator in documents ? documents[Symbol.asyncIterator]() : documents[Symbol.iterator]();
|
|
108
|
+
const slots = [];
|
|
109
|
+
const idle = [];
|
|
110
|
+
const running = /* @__PURE__ */ new Map();
|
|
111
|
+
let nextIndex = 0;
|
|
112
|
+
let exhausted = false;
|
|
113
|
+
let done = 0;
|
|
114
|
+
async function take() {
|
|
115
|
+
if (exhausted) return void 0;
|
|
116
|
+
const next = await iterator.next();
|
|
117
|
+
if (next.done) {
|
|
118
|
+
exhausted = true;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
index: nextIndex++,
|
|
123
|
+
value: next.value
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function start(slot, job) {
|
|
127
|
+
const task = renderOne(slot, job.value, options, controller.signal).then((result) => ({
|
|
128
|
+
index: job.index,
|
|
129
|
+
documentId: documentIdOf(job.value),
|
|
130
|
+
result
|
|
131
|
+
}), (error) => ({
|
|
132
|
+
index: job.index,
|
|
133
|
+
documentId: documentIdOf(job.value),
|
|
134
|
+
error: toEngineError(error)
|
|
135
|
+
}));
|
|
136
|
+
running.set(job.index, task.finally(() => idle.push(slot)));
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
while (true) {
|
|
140
|
+
if (controller.signal.aborted) throw new require_errors.DocumentEngineError("EXPORT_ABORTED", "Rendering was cancelled");
|
|
141
|
+
while (running.size < concurrency && !exhausted) {
|
|
142
|
+
const job = await take();
|
|
143
|
+
if (!job) break;
|
|
144
|
+
let slot = idle.pop();
|
|
145
|
+
if (!slot) {
|
|
146
|
+
slot = createSlot(options);
|
|
147
|
+
slots.push(slot);
|
|
148
|
+
}
|
|
149
|
+
start(slot, job);
|
|
150
|
+
}
|
|
151
|
+
if (running.size === 0) break;
|
|
152
|
+
const finished = await Promise.race(running.values());
|
|
153
|
+
running.delete(finished.index);
|
|
154
|
+
done += 1;
|
|
155
|
+
options.onProgress?.({
|
|
156
|
+
done,
|
|
157
|
+
total
|
|
158
|
+
});
|
|
159
|
+
if (controller.signal.aborted) throw new require_errors.DocumentEngineError("EXPORT_ABORTED", "Rendering was cancelled");
|
|
160
|
+
yield finished;
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
controller.abort();
|
|
164
|
+
options.signal?.removeEventListener("abort", stop);
|
|
165
|
+
await Promise.allSettled(running.values());
|
|
166
|
+
await Promise.all(slots.map(releaseSlot));
|
|
167
|
+
if (!exhausted) await iterator.return?.();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
exports.disposeObjects = disposeObjects;
|
|
172
|
+
exports.renderDocuments = renderDocuments;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AssetOptions } from "../assets/asset-pipeline.cjs";
|
|
2
|
+
import { ExportOptions } from "./export-options.cjs";
|
|
3
|
+
import { ContentLimits } from "../security/content-limits.cjs";
|
|
4
|
+
import { CustomObjectDefinition } from "../fabric/object-registry.cjs";
|
|
5
|
+
import { DocumentEngineError } from "../engine/errors.cjs";
|
|
6
|
+
import { ExportResult } from "../engine/create-document-engine.cjs";
|
|
7
|
+
import { StaticCanvas } from "fabric";
|
|
8
|
+
//#region src/export/batch-render.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* Renders many documents with a few reusable off-screen canvases
|
|
11
|
+
* (fabric.js #4848). Each job loads a document, exports it, and then frees
|
|
12
|
+
* everything the job created: objects, their cache canvases and image
|
|
13
|
+
* elements. Memory therefore stays flat however many documents go through.
|
|
14
|
+
*/
|
|
15
|
+
export interface RenderDocumentsOptions extends Omit<ExportOptions, "signal"> {
|
|
16
|
+
/** How many documents render at the same time, each on its own canvas. Default 2. */
|
|
17
|
+
concurrency?: number;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
/** Called after each document, rendered or failed. */
|
|
20
|
+
onProgress?: (progress: {
|
|
21
|
+
done: number;
|
|
22
|
+
total: number | undefined;
|
|
23
|
+
}) => void;
|
|
24
|
+
customObjects?: CustomObjectDefinition[];
|
|
25
|
+
assets?: AssetOptions;
|
|
26
|
+
limits?: ContentLimits;
|
|
27
|
+
}
|
|
28
|
+
export interface RenderedDocument {
|
|
29
|
+
/** Position of the document in the input. */
|
|
30
|
+
index: number;
|
|
31
|
+
documentId: string | undefined;
|
|
32
|
+
result?: ExportResult;
|
|
33
|
+
/** Set when this document failed. The other documents still render. */
|
|
34
|
+
error?: DocumentEngineError;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Renders each document and yields the results as they finish, so they can
|
|
38
|
+
* be uploaded or saved one by one instead of all being held in memory.
|
|
39
|
+
* Documents can be `FabricDocument`s or plain Fabric JSON, and can come from
|
|
40
|
+
* an async iterable, such as pages of a database query.
|
|
41
|
+
*/
|
|
42
|
+
export declare function renderDocuments(documents: Iterable<unknown> | AsyncIterable<unknown>, options: RenderDocumentsOptions): AsyncGenerator<RenderedDocument, void, undefined>;
|
|
43
|
+
//#endregion
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AssetOptions } from "../assets/asset-pipeline.js";
|
|
2
|
+
import { ExportOptions } from "./export-options.js";
|
|
3
|
+
import { ContentLimits } from "../security/content-limits.js";
|
|
4
|
+
import { CustomObjectDefinition } from "../fabric/object-registry.js";
|
|
5
|
+
import { DocumentEngineError } from "../engine/errors.js";
|
|
6
|
+
import { ExportResult } from "../engine/create-document-engine.js";
|
|
7
|
+
import { StaticCanvas } from "fabric";
|
|
8
|
+
//#region src/export/batch-render.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* Renders many documents with a few reusable off-screen canvases
|
|
11
|
+
* (fabric.js #4848). Each job loads a document, exports it, and then frees
|
|
12
|
+
* everything the job created: objects, their cache canvases and image
|
|
13
|
+
* elements. Memory therefore stays flat however many documents go through.
|
|
14
|
+
*/
|
|
15
|
+
export interface RenderDocumentsOptions extends Omit<ExportOptions, "signal"> {
|
|
16
|
+
/** How many documents render at the same time, each on its own canvas. Default 2. */
|
|
17
|
+
concurrency?: number;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
/** Called after each document, rendered or failed. */
|
|
20
|
+
onProgress?: (progress: {
|
|
21
|
+
done: number;
|
|
22
|
+
total: number | undefined;
|
|
23
|
+
}) => void;
|
|
24
|
+
customObjects?: CustomObjectDefinition[];
|
|
25
|
+
assets?: AssetOptions;
|
|
26
|
+
limits?: ContentLimits;
|
|
27
|
+
}
|
|
28
|
+
export interface RenderedDocument {
|
|
29
|
+
/** Position of the document in the input. */
|
|
30
|
+
index: number;
|
|
31
|
+
documentId: string | undefined;
|
|
32
|
+
result?: ExportResult;
|
|
33
|
+
/** Set when this document failed. The other documents still render. */
|
|
34
|
+
error?: DocumentEngineError;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Renders each document and yields the results as they finish, so they can
|
|
38
|
+
* be uploaded or saved one by one instead of all being held in memory.
|
|
39
|
+
* Documents can be `FabricDocument`s or plain Fabric JSON, and can come from
|
|
40
|
+
* an async iterable, such as pages of a database query.
|
|
41
|
+
*/
|
|
42
|
+
export declare function renderDocuments(documents: Iterable<unknown> | AsyncIterable<unknown>, options: RenderDocumentsOptions): AsyncGenerator<RenderedDocument, void, undefined>;
|
|
43
|
+
//#endregion
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { DocumentEngineError, isDocumentEngineError } from "../engine/errors.js";
|
|
2
|
+
import { walkObjects } from "../fabric/walk-objects.js";
|
|
3
|
+
import { createDocumentEngine } from "../engine/create-document-engine.js";
|
|
4
|
+
import { StaticCanvas } from "fabric";
|
|
5
|
+
//#region src/export/batch-render.ts
|
|
6
|
+
/**
|
|
7
|
+
* Releases a canvas's pixels now. Fabric's `dispose` drops its references,
|
|
8
|
+
* but in browsers the memory behind a canvas comes back only much later,
|
|
9
|
+
* when the garbage collector gets to it.
|
|
10
|
+
*/
|
|
11
|
+
function releaseCanvas(value) {
|
|
12
|
+
if (typeof HTMLCanvasElement !== "undefined" && value instanceof HTMLCanvasElement) {
|
|
13
|
+
value.width = 0;
|
|
14
|
+
value.height = 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function disposeObject(object) {
|
|
18
|
+
const owned = object;
|
|
19
|
+
for (const key of [
|
|
20
|
+
"_cacheCanvas",
|
|
21
|
+
"_filteredEl",
|
|
22
|
+
"_element",
|
|
23
|
+
"_originalElement"
|
|
24
|
+
]) releaseCanvas(owned[key]);
|
|
25
|
+
object.dispose?.();
|
|
26
|
+
}
|
|
27
|
+
/** Frees the objects on a canvas: their cache canvases and image elements. */
|
|
28
|
+
function disposeObjects(canvas) {
|
|
29
|
+
const objects = canvas.getObjects();
|
|
30
|
+
canvas.remove(...objects);
|
|
31
|
+
walkObjects(objects, (object) => {
|
|
32
|
+
disposeObject(object);
|
|
33
|
+
if (object.clipPath) disposeObject(object.clipPath);
|
|
34
|
+
});
|
|
35
|
+
for (const key of ["backgroundImage", "overlayImage"]) {
|
|
36
|
+
const image = canvas[key];
|
|
37
|
+
if (image) {
|
|
38
|
+
disposeObject(image);
|
|
39
|
+
canvas[key] = void 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function createSlot(options) {
|
|
44
|
+
if (typeof document === "undefined") throw new DocumentEngineError("EXPORT_FAILED", "Rendering documents needs a browser DOM");
|
|
45
|
+
const element = document.createElement("canvas");
|
|
46
|
+
const canvas = new StaticCanvas(element, {
|
|
47
|
+
renderOnAddRemove: false,
|
|
48
|
+
enableRetinaScaling: false
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
element,
|
|
52
|
+
canvas,
|
|
53
|
+
engine: createDocumentEngine({
|
|
54
|
+
canvas,
|
|
55
|
+
customObjects: options.customObjects,
|
|
56
|
+
assets: options.assets,
|
|
57
|
+
limits: options.limits,
|
|
58
|
+
history: { limit: 1 }
|
|
59
|
+
})
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function releaseSlot(slot) {
|
|
63
|
+
disposeObjects(slot.canvas);
|
|
64
|
+
slot.engine.destroy();
|
|
65
|
+
await slot.canvas.dispose();
|
|
66
|
+
slot.element.width = 0;
|
|
67
|
+
slot.element.height = 0;
|
|
68
|
+
}
|
|
69
|
+
function toEngineError(error) {
|
|
70
|
+
if (isDocumentEngineError(error)) return error;
|
|
71
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
72
|
+
return new DocumentEngineError("EXPORT_FAILED", `The document could not be rendered: ${reason}`, { cause: error });
|
|
73
|
+
}
|
|
74
|
+
function documentIdOf(value) {
|
|
75
|
+
const id = value?.id;
|
|
76
|
+
return typeof id === "string" ? id : void 0;
|
|
77
|
+
}
|
|
78
|
+
async function renderOne(slot, input, options, signal) {
|
|
79
|
+
const { concurrency: _concurrency, onProgress: _onProgress, customObjects: _custom, assets: _assets, limits: _limits, ...exportOptions } = options;
|
|
80
|
+
try {
|
|
81
|
+
await slot.engine.loadDocument(input, {
|
|
82
|
+
signal,
|
|
83
|
+
discardUnsavedChanges: true
|
|
84
|
+
});
|
|
85
|
+
return await slot.engine.export({
|
|
86
|
+
...exportOptions,
|
|
87
|
+
signal
|
|
88
|
+
});
|
|
89
|
+
} finally {
|
|
90
|
+
disposeObjects(slot.canvas);
|
|
91
|
+
slot.engine.clearHistory();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Renders each document and yields the results as they finish, so they can
|
|
96
|
+
* be uploaded or saved one by one instead of all being held in memory.
|
|
97
|
+
* Documents can be `FabricDocument`s or plain Fabric JSON, and can come from
|
|
98
|
+
* an async iterable, such as pages of a database query.
|
|
99
|
+
*/
|
|
100
|
+
async function* renderDocuments(documents, options) {
|
|
101
|
+
const concurrency = Math.max(1, Math.floor(options.concurrency ?? 2));
|
|
102
|
+
const total = Array.isArray(documents) ? documents.length : void 0;
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const stop = () => controller.abort();
|
|
105
|
+
options.signal?.addEventListener("abort", stop, { once: true });
|
|
106
|
+
if (options.signal?.aborted) stop();
|
|
107
|
+
const iterator = Symbol.asyncIterator in documents ? documents[Symbol.asyncIterator]() : documents[Symbol.iterator]();
|
|
108
|
+
const slots = [];
|
|
109
|
+
const idle = [];
|
|
110
|
+
const running = /* @__PURE__ */ new Map();
|
|
111
|
+
let nextIndex = 0;
|
|
112
|
+
let exhausted = false;
|
|
113
|
+
let done = 0;
|
|
114
|
+
async function take() {
|
|
115
|
+
if (exhausted) return void 0;
|
|
116
|
+
const next = await iterator.next();
|
|
117
|
+
if (next.done) {
|
|
118
|
+
exhausted = true;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
index: nextIndex++,
|
|
123
|
+
value: next.value
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function start(slot, job) {
|
|
127
|
+
const task = renderOne(slot, job.value, options, controller.signal).then((result) => ({
|
|
128
|
+
index: job.index,
|
|
129
|
+
documentId: documentIdOf(job.value),
|
|
130
|
+
result
|
|
131
|
+
}), (error) => ({
|
|
132
|
+
index: job.index,
|
|
133
|
+
documentId: documentIdOf(job.value),
|
|
134
|
+
error: toEngineError(error)
|
|
135
|
+
}));
|
|
136
|
+
running.set(job.index, task.finally(() => idle.push(slot)));
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
while (true) {
|
|
140
|
+
if (controller.signal.aborted) throw new DocumentEngineError("EXPORT_ABORTED", "Rendering was cancelled");
|
|
141
|
+
while (running.size < concurrency && !exhausted) {
|
|
142
|
+
const job = await take();
|
|
143
|
+
if (!job) break;
|
|
144
|
+
let slot = idle.pop();
|
|
145
|
+
if (!slot) {
|
|
146
|
+
slot = createSlot(options);
|
|
147
|
+
slots.push(slot);
|
|
148
|
+
}
|
|
149
|
+
start(slot, job);
|
|
150
|
+
}
|
|
151
|
+
if (running.size === 0) break;
|
|
152
|
+
const finished = await Promise.race(running.values());
|
|
153
|
+
running.delete(finished.index);
|
|
154
|
+
done += 1;
|
|
155
|
+
options.onProgress?.({
|
|
156
|
+
done,
|
|
157
|
+
total
|
|
158
|
+
});
|
|
159
|
+
if (controller.signal.aborted) throw new DocumentEngineError("EXPORT_ABORTED", "Rendering was cancelled");
|
|
160
|
+
yield finished;
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
controller.abort();
|
|
164
|
+
options.signal?.removeEventListener("abort", stop);
|
|
165
|
+
await Promise.allSettled(running.values());
|
|
166
|
+
await Promise.all(slots.map(releaseSlot));
|
|
167
|
+
if (!exhausted) await iterator.return?.();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
export { disposeObjects, renderDocuments };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const require_errors = require("../engine/errors.cjs");
|
|
2
2
|
//#region src/export/export-options.ts
|
|
3
|
+
const DEFAULT_MAX_EMBEDDED_IMAGE_BYTES = 26214400;
|
|
3
4
|
const formats = /* @__PURE__ */ new Set([
|
|
4
5
|
"png",
|
|
5
6
|
"jpeg",
|
|
@@ -32,6 +33,15 @@ function normalizeExportOptions(options) {
|
|
|
32
33
|
"content",
|
|
33
34
|
"selection"
|
|
34
35
|
].includes(area)) throw invalid(`Unknown export area "${String(area)}", use canvas, content, selection or a rectangle`);
|
|
36
|
+
const svg = options.svg ?? {};
|
|
37
|
+
const textOnPath = svg.textOnPath ?? "vector";
|
|
38
|
+
if (textOnPath !== "vector" && textOnPath !== "fabric") throw invalid("svg.textOnPath must be \"vector\" or \"fabric\"");
|
|
39
|
+
const embedImages = svg.embedImages ?? false;
|
|
40
|
+
if (embedImages !== true && embedImages !== false && embedImages !== "require") throw invalid("svg.embedImages must be true, false or \"require\"");
|
|
41
|
+
const maxEmbeddedImageBytes = svg.maxEmbeddedImageBytes ?? 26214400;
|
|
42
|
+
if (!isPositive(maxEmbeddedImageBytes)) throw invalid("svg.maxEmbeddedImageBytes must be greater than zero");
|
|
43
|
+
const embedFonts = svg.embedFonts ?? {};
|
|
44
|
+
if (typeof embedFonts !== "object" || Array.isArray(embedFonts)) throw invalid("svg.embedFonts must map font families to font files");
|
|
35
45
|
return {
|
|
36
46
|
format: options.format,
|
|
37
47
|
scale,
|
|
@@ -39,9 +49,16 @@ function normalizeExportOptions(options) {
|
|
|
39
49
|
area,
|
|
40
50
|
padding,
|
|
41
51
|
background: options.background ?? "keep",
|
|
42
|
-
signal: options.signal
|
|
52
|
+
signal: options.signal,
|
|
53
|
+
svg: {
|
|
54
|
+
textOnPath,
|
|
55
|
+
embedImages,
|
|
56
|
+
maxEmbeddedImageBytes,
|
|
57
|
+
embedFonts
|
|
58
|
+
}
|
|
43
59
|
};
|
|
44
60
|
}
|
|
45
61
|
//#endregion
|
|
62
|
+
exports.DEFAULT_MAX_EMBEDDED_IMAGE_BYTES = DEFAULT_MAX_EMBEDDED_IMAGE_BYTES;
|
|
46
63
|
exports.isRasterFormat = isRasterFormat;
|
|
47
64
|
exports.normalizeExportOptions = normalizeExportOptions;
|
|
@@ -8,6 +8,27 @@ export interface ExportRect {
|
|
|
8
8
|
}
|
|
9
9
|
export type ExportArea = "canvas" | "content" | "selection" | ExportRect;
|
|
10
10
|
export type ExportBackground = "keep" | "transparent" | (string & {});
|
|
11
|
+
/** A font file for `embedFonts`: a URL to fetch, or the file's bytes. */
|
|
12
|
+
export type FontSource = string | ArrayBuffer | Uint8Array | Blob;
|
|
13
|
+
export interface SvgExportOptions {
|
|
14
|
+
/**
|
|
15
|
+
* How text that follows a path is written. `vector` (the default) writes
|
|
16
|
+
* each character where Fabric draws it on the canvas. `fabric` keeps
|
|
17
|
+
* Fabric's own output, which ignores `pathAlign` and draws text
|
|
18
|
+
* backgrounds and underlines straight.
|
|
19
|
+
*/
|
|
20
|
+
textOnPath?: "vector" | "fabric";
|
|
21
|
+
/**
|
|
22
|
+
* Puts every image into the file as data, so the SVG opens anywhere
|
|
23
|
+
* without the original image URLs. `require` blocks the export when an
|
|
24
|
+
* image cannot be embedded; `true` exports anyway with a warning.
|
|
25
|
+
*/
|
|
26
|
+
embedImages?: boolean | "require";
|
|
27
|
+
/** The largest image, in bytes, to embed. Default 25 MB. */
|
|
28
|
+
maxEmbeddedImageBytes?: number;
|
|
29
|
+
/** Font files to put into the file, by font family, for the families the canvas uses. */
|
|
30
|
+
embedFonts?: Record<string, FontSource>;
|
|
31
|
+
}
|
|
11
32
|
export interface ExportOptions {
|
|
12
33
|
format: ExportFormat;
|
|
13
34
|
scale?: number;
|
|
@@ -16,5 +37,7 @@ export interface ExportOptions {
|
|
|
16
37
|
padding?: number;
|
|
17
38
|
background?: ExportBackground;
|
|
18
39
|
signal?: AbortSignal;
|
|
40
|
+
/** Options for SVG exports. */
|
|
41
|
+
svg?: SvgExportOptions;
|
|
19
42
|
}
|
|
20
43
|
//#endregion
|
|
@@ -8,6 +8,27 @@ export interface ExportRect {
|
|
|
8
8
|
}
|
|
9
9
|
export type ExportArea = "canvas" | "content" | "selection" | ExportRect;
|
|
10
10
|
export type ExportBackground = "keep" | "transparent" | (string & {});
|
|
11
|
+
/** A font file for `embedFonts`: a URL to fetch, or the file's bytes. */
|
|
12
|
+
export type FontSource = string | ArrayBuffer | Uint8Array | Blob;
|
|
13
|
+
export interface SvgExportOptions {
|
|
14
|
+
/**
|
|
15
|
+
* How text that follows a path is written. `vector` (the default) writes
|
|
16
|
+
* each character where Fabric draws it on the canvas. `fabric` keeps
|
|
17
|
+
* Fabric's own output, which ignores `pathAlign` and draws text
|
|
18
|
+
* backgrounds and underlines straight.
|
|
19
|
+
*/
|
|
20
|
+
textOnPath?: "vector" | "fabric";
|
|
21
|
+
/**
|
|
22
|
+
* Puts every image into the file as data, so the SVG opens anywhere
|
|
23
|
+
* without the original image URLs. `require` blocks the export when an
|
|
24
|
+
* image cannot be embedded; `true` exports anyway with a warning.
|
|
25
|
+
*/
|
|
26
|
+
embedImages?: boolean | "require";
|
|
27
|
+
/** The largest image, in bytes, to embed. Default 25 MB. */
|
|
28
|
+
maxEmbeddedImageBytes?: number;
|
|
29
|
+
/** Font files to put into the file, by font family, for the families the canvas uses. */
|
|
30
|
+
embedFonts?: Record<string, FontSource>;
|
|
31
|
+
}
|
|
11
32
|
export interface ExportOptions {
|
|
12
33
|
format: ExportFormat;
|
|
13
34
|
scale?: number;
|
|
@@ -16,5 +37,7 @@ export interface ExportOptions {
|
|
|
16
37
|
padding?: number;
|
|
17
38
|
background?: ExportBackground;
|
|
18
39
|
signal?: AbortSignal;
|
|
40
|
+
/** Options for SVG exports. */
|
|
41
|
+
svg?: SvgExportOptions;
|
|
19
42
|
}
|
|
20
43
|
//#endregion
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DocumentEngineError } from "../engine/errors.js";
|
|
2
2
|
//#region src/export/export-options.ts
|
|
3
|
+
const DEFAULT_MAX_EMBEDDED_IMAGE_BYTES = 26214400;
|
|
3
4
|
const formats = /* @__PURE__ */ new Set([
|
|
4
5
|
"png",
|
|
5
6
|
"jpeg",
|
|
@@ -32,6 +33,15 @@ function normalizeExportOptions(options) {
|
|
|
32
33
|
"content",
|
|
33
34
|
"selection"
|
|
34
35
|
].includes(area)) throw invalid(`Unknown export area "${String(area)}", use canvas, content, selection or a rectangle`);
|
|
36
|
+
const svg = options.svg ?? {};
|
|
37
|
+
const textOnPath = svg.textOnPath ?? "vector";
|
|
38
|
+
if (textOnPath !== "vector" && textOnPath !== "fabric") throw invalid("svg.textOnPath must be \"vector\" or \"fabric\"");
|
|
39
|
+
const embedImages = svg.embedImages ?? false;
|
|
40
|
+
if (embedImages !== true && embedImages !== false && embedImages !== "require") throw invalid("svg.embedImages must be true, false or \"require\"");
|
|
41
|
+
const maxEmbeddedImageBytes = svg.maxEmbeddedImageBytes ?? 26214400;
|
|
42
|
+
if (!isPositive(maxEmbeddedImageBytes)) throw invalid("svg.maxEmbeddedImageBytes must be greater than zero");
|
|
43
|
+
const embedFonts = svg.embedFonts ?? {};
|
|
44
|
+
if (typeof embedFonts !== "object" || Array.isArray(embedFonts)) throw invalid("svg.embedFonts must map font families to font files");
|
|
35
45
|
return {
|
|
36
46
|
format: options.format,
|
|
37
47
|
scale,
|
|
@@ -39,8 +49,14 @@ function normalizeExportOptions(options) {
|
|
|
39
49
|
area,
|
|
40
50
|
padding,
|
|
41
51
|
background: options.background ?? "keep",
|
|
42
|
-
signal: options.signal
|
|
52
|
+
signal: options.signal,
|
|
53
|
+
svg: {
|
|
54
|
+
textOnPath,
|
|
55
|
+
embedImages,
|
|
56
|
+
maxEmbeddedImageBytes,
|
|
57
|
+
embedFonts
|
|
58
|
+
}
|
|
43
59
|
};
|
|
44
60
|
}
|
|
45
61
|
//#endregion
|
|
46
|
-
export { isRasterFormat, normalizeExportOptions };
|
|
62
|
+
export { DEFAULT_MAX_EMBEDDED_IMAGE_BYTES, isRasterFormat, normalizeExportOptions };
|
|
@@ -3,6 +3,7 @@ const require_image_check = require("../assets/image-check.cjs");
|
|
|
3
3
|
const require_export_options = require("./export-options.cjs");
|
|
4
4
|
const require_object_ids = require("../fabric/object-ids.cjs");
|
|
5
5
|
const require_walk_objects = require("../fabric/walk-objects.cjs");
|
|
6
|
+
const require_text_on_path = require("./svg/text-on-path.cjs");
|
|
6
7
|
//#region src/export/preflight-export.ts
|
|
7
8
|
function imageElementsOf(object) {
|
|
8
9
|
const elements = [];
|
|
@@ -49,7 +50,7 @@ function isBroken(element) {
|
|
|
49
50
|
function shortUrl(url) {
|
|
50
51
|
return url.length > 80 ? `${url.slice(0, 77)}...` : url;
|
|
51
52
|
}
|
|
52
|
-
async function preflightExport(canvas, format, fonts, assetOptions) {
|
|
53
|
+
async function preflightExport(canvas, format, fonts, assetOptions, svgOptions) {
|
|
53
54
|
const problems = [];
|
|
54
55
|
const warnings = [];
|
|
55
56
|
if (format === "json") return {
|
|
@@ -71,13 +72,21 @@ async function preflightExport(canvas, format, fonts, assetOptions) {
|
|
|
71
72
|
url,
|
|
72
73
|
objectIds
|
|
73
74
|
});
|
|
74
|
-
else if (format === "svg" && !require_image_check.isPortableUrl(url)) warnings.push({
|
|
75
|
+
else if (format === "svg" && !require_image_check.isPortableUrl(url) && !svgOptions?.embedImages) warnings.push({
|
|
75
76
|
code: "ASSET_NOT_PORTABLE",
|
|
76
77
|
message: `The SVG links to ${shortUrl(url)}, which only exists in this tab`,
|
|
77
78
|
url,
|
|
78
79
|
objectIds
|
|
79
80
|
});
|
|
80
81
|
}
|
|
82
|
+
if (format === "svg" && svgOptions?.textOnPath === "fabric") {
|
|
83
|
+
const texts = require_text_on_path.findTextOnPath(canvas);
|
|
84
|
+
if (texts.length > 0) warnings.push({
|
|
85
|
+
code: "TEXT_ON_PATH_APPROXIMATED",
|
|
86
|
+
message: "Text on a path is written with Fabric's own SVG, which ignores pathAlign and draws text backgrounds and underlines straight. Use svg.textOnPath: 'vector' to match the canvas.",
|
|
87
|
+
objectIds: texts.map((text) => require_object_ids.readObjectId(text)).filter((id) => id !== void 0)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
81
90
|
const fontSet = typeof document === "undefined" ? void 0 : document.fonts;
|
|
82
91
|
if (fontSet) await fontSet.ready;
|
|
83
92
|
for (const font of await require_font_check.findUnavailableFonts(fonts, assetOptions.loadFont)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AssetWarning } from "../assets/asset-pipeline.cjs";
|
|
2
2
|
import { StaticCanvas } from "fabric";
|
|
3
3
|
//#region src/export/preflight-export.d.ts
|
|
4
|
-
export type ExportProblemCode = "MISSING_IMAGE" | "CROSS_ORIGIN_IMAGE" | "MISSING_FONT";
|
|
4
|
+
export type ExportProblemCode = "MISSING_IMAGE" | "CROSS_ORIGIN_IMAGE" | "MISSING_FONT" | "IMAGE_NOT_EMBEDDED";
|
|
5
5
|
export interface ExportProblem {
|
|
6
6
|
code: ExportProblemCode;
|
|
7
7
|
message: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AssetWarning } from "../assets/asset-pipeline.js";
|
|
2
2
|
import { StaticCanvas } from "fabric";
|
|
3
3
|
//#region src/export/preflight-export.d.ts
|
|
4
|
-
export type ExportProblemCode = "MISSING_IMAGE" | "CROSS_ORIGIN_IMAGE" | "MISSING_FONT";
|
|
4
|
+
export type ExportProblemCode = "MISSING_IMAGE" | "CROSS_ORIGIN_IMAGE" | "MISSING_FONT" | "IMAGE_NOT_EMBEDDED";
|
|
5
5
|
export interface ExportProblem {
|
|
6
6
|
code: ExportProblemCode;
|
|
7
7
|
message: string;
|
|
@@ -3,6 +3,7 @@ import { isCrossOriginUrl, isPortableUrl } from "../assets/image-check.js";
|
|
|
3
3
|
import { isRasterFormat } from "./export-options.js";
|
|
4
4
|
import { readObjectId } from "../fabric/object-ids.js";
|
|
5
5
|
import { childrenOf } from "../fabric/walk-objects.js";
|
|
6
|
+
import { findTextOnPath } from "./svg/text-on-path.js";
|
|
6
7
|
//#region src/export/preflight-export.ts
|
|
7
8
|
function imageElementsOf(object) {
|
|
8
9
|
const elements = [];
|
|
@@ -49,7 +50,7 @@ function isBroken(element) {
|
|
|
49
50
|
function shortUrl(url) {
|
|
50
51
|
return url.length > 80 ? `${url.slice(0, 77)}...` : url;
|
|
51
52
|
}
|
|
52
|
-
async function preflightExport(canvas, format, fonts, assetOptions) {
|
|
53
|
+
async function preflightExport(canvas, format, fonts, assetOptions, svgOptions) {
|
|
53
54
|
const problems = [];
|
|
54
55
|
const warnings = [];
|
|
55
56
|
if (format === "json") return {
|
|
@@ -71,13 +72,21 @@ async function preflightExport(canvas, format, fonts, assetOptions) {
|
|
|
71
72
|
url,
|
|
72
73
|
objectIds
|
|
73
74
|
});
|
|
74
|
-
else if (format === "svg" && !isPortableUrl(url)) warnings.push({
|
|
75
|
+
else if (format === "svg" && !isPortableUrl(url) && !svgOptions?.embedImages) warnings.push({
|
|
75
76
|
code: "ASSET_NOT_PORTABLE",
|
|
76
77
|
message: `The SVG links to ${shortUrl(url)}, which only exists in this tab`,
|
|
77
78
|
url,
|
|
78
79
|
objectIds
|
|
79
80
|
});
|
|
80
81
|
}
|
|
82
|
+
if (format === "svg" && svgOptions?.textOnPath === "fabric") {
|
|
83
|
+
const texts = findTextOnPath(canvas);
|
|
84
|
+
if (texts.length > 0) warnings.push({
|
|
85
|
+
code: "TEXT_ON_PATH_APPROXIMATED",
|
|
86
|
+
message: "Text on a path is written with Fabric's own SVG, which ignores pathAlign and draws text backgrounds and underlines straight. Use svg.textOnPath: 'vector' to match the canvas.",
|
|
87
|
+
objectIds: texts.map((text) => readObjectId(text)).filter((id) => id !== void 0)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
81
90
|
const fontSet = typeof document === "undefined" ? void 0 : document.fonts;
|
|
82
91
|
if (fontSet) await fontSet.ready;
|
|
83
92
|
for (const font of await findUnavailableFonts(fonts, assetOptions.loadFont)) {
|