fabricjs-document-engine 1.0.1 → 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.
Files changed (89) hide show
  1. package/README.md +277 -36
  2. package/README.zh-CN.md +697 -0
  3. package/dist/assets/asset-manifest.d.cts +3 -0
  4. package/dist/assets/asset-manifest.d.ts +3 -0
  5. package/dist/assets/asset-pipeline.cjs +22 -10
  6. package/dist/assets/asset-pipeline.d.cts +5 -1
  7. package/dist/assets/asset-pipeline.d.ts +5 -1
  8. package/dist/assets/asset-pipeline.js +22 -10
  9. package/dist/assets/font-check.cjs +9 -1
  10. package/dist/assets/font-check.js +9 -1
  11. package/dist/assets/image-check.cjs +88 -6
  12. package/dist/assets/image-check.d.cts +17 -0
  13. package/dist/assets/image-check.d.ts +17 -0
  14. package/dist/assets/image-check.js +88 -7
  15. package/dist/commands/clipboard.cjs +136 -0
  16. package/dist/commands/clipboard.d.cts +47 -0
  17. package/dist/commands/clipboard.d.ts +47 -0
  18. package/dist/commands/clipboard.js +135 -0
  19. package/dist/commands/layers.cjs +97 -0
  20. package/dist/commands/layers.d.cts +39 -0
  21. package/dist/commands/layers.d.ts +39 -0
  22. package/dist/commands/layers.js +92 -0
  23. package/dist/engine/create-document-engine.cjs +85 -14
  24. package/dist/engine/create-document-engine.d.cts +20 -1
  25. package/dist/engine/create-document-engine.d.ts +20 -1
  26. package/dist/engine/create-document-engine.js +86 -15
  27. package/dist/engine/errors.d.cts +1 -1
  28. package/dist/engine/errors.d.ts +1 -1
  29. package/dist/export/batch-render.cjs +172 -0
  30. package/dist/export/batch-render.d.cts +43 -0
  31. package/dist/export/batch-render.d.ts +43 -0
  32. package/dist/export/batch-render.js +171 -0
  33. package/dist/export/export-options.cjs +18 -1
  34. package/dist/export/export-options.d.cts +23 -0
  35. package/dist/export/export-options.d.ts +23 -0
  36. package/dist/export/export-options.js +18 -2
  37. package/dist/export/preflight-export.cjs +11 -2
  38. package/dist/export/preflight-export.d.cts +1 -1
  39. package/dist/export/preflight-export.d.ts +1 -1
  40. package/dist/export/preflight-export.js +11 -2
  41. package/dist/export/render-export.cjs +18 -3
  42. package/dist/export/render-export.js +18 -4
  43. package/dist/export/svg/embed-assets.cjs +249 -0
  44. package/dist/export/svg/embed-assets.js +249 -0
  45. package/dist/export/svg/overrides.cjs +32 -0
  46. package/dist/export/svg/overrides.js +32 -0
  47. package/dist/export/svg/text-on-path.cjs +252 -0
  48. package/dist/export/svg/text-on-path.js +249 -0
  49. package/dist/fabric/fabric-adapter.cjs +46 -5
  50. package/dist/fabric/fabric-adapter.js +46 -5
  51. package/dist/fabric/object-registry.cjs +1 -1
  52. package/dist/fabric/object-registry.js +1 -1
  53. package/dist/import/svg-import.cjs +256 -0
  54. package/dist/import/svg-import.d.cts +50 -0
  55. package/dist/import/svg-import.d.ts +50 -0
  56. package/dist/import/svg-import.js +256 -0
  57. package/dist/index.cjs +12 -0
  58. package/dist/index.d.cts +9 -4
  59. package/dist/index.d.ts +9 -4
  60. package/dist/index.js +4 -1
  61. package/dist/pdf/export-pdf.cjs +440 -0
  62. package/dist/pdf/export-pdf.d.cts +73 -0
  63. package/dist/pdf/export-pdf.d.ts +73 -0
  64. package/dist/pdf/export-pdf.js +440 -0
  65. package/dist/pdf/fonts.cjs +115 -0
  66. package/dist/pdf/fonts.d.cts +12 -0
  67. package/dist/pdf/fonts.d.ts +12 -0
  68. package/dist/pdf/fonts.js +112 -0
  69. package/dist/pdf/page-layout.cjs +79 -0
  70. package/dist/pdf/page-layout.d.cts +51 -0
  71. package/dist/pdf/page-layout.d.ts +51 -0
  72. package/dist/pdf/page-layout.js +76 -0
  73. package/dist/pdf/text-decorations.cjs +108 -0
  74. package/dist/pdf/text-decorations.js +107 -0
  75. package/dist/pdf.cjs +7 -0
  76. package/dist/pdf.d.cts +4 -0
  77. package/dist/pdf.d.ts +4 -0
  78. package/dist/pdf.js +3 -0
  79. package/dist/react/use-layers.cjs +53 -0
  80. package/dist/react/use-layers.d.cts +10 -0
  81. package/dist/react/use-layers.d.ts +10 -0
  82. package/dist/react/use-layers.js +53 -0
  83. package/dist/react.cjs +2 -0
  84. package/dist/react.d.cts +3 -1
  85. package/dist/react.d.ts +3 -1
  86. package/dist/react.js +2 -1
  87. package/dist/util/concurrency.cjs +28 -0
  88. package/dist/util/concurrency.js +27 -0
  89. package/package.json +55 -14
@@ -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)) {