fabricjs-document-engine 1.0.2 → 1.2.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 (118) hide show
  1. package/README.md +220 -11
  2. package/README.zh-CN.md +220 -11
  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 +31 -17
  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 +31 -17
  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 +89 -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 +89 -7
  15. package/dist/commands/clipboard.cjs +141 -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 +140 -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/document/document-format.d.cts +15 -0
  24. package/dist/document/document-format.d.ts +15 -0
  25. package/dist/document/validate-document.cjs +8 -0
  26. package/dist/document/validate-document.js +8 -0
  27. package/dist/engine/create-document-engine.cjs +196 -31
  28. package/dist/engine/create-document-engine.d.cts +44 -7
  29. package/dist/engine/create-document-engine.d.ts +44 -7
  30. package/dist/engine/create-document-engine.js +197 -32
  31. package/dist/engine/errors.d.cts +1 -1
  32. package/dist/engine/errors.d.ts +1 -1
  33. package/dist/export/batch-render.cjs +172 -0
  34. package/dist/export/batch-render.d.cts +43 -0
  35. package/dist/export/batch-render.d.ts +43 -0
  36. package/dist/export/batch-render.js +171 -0
  37. package/dist/export/export-options.cjs +21 -1
  38. package/dist/export/export-options.d.cts +29 -0
  39. package/dist/export/export-options.d.ts +29 -0
  40. package/dist/export/export-options.js +21 -2
  41. package/dist/export/preflight-export.cjs +11 -2
  42. package/dist/export/preflight-export.d.cts +1 -1
  43. package/dist/export/preflight-export.d.ts +1 -1
  44. package/dist/export/preflight-export.js +11 -2
  45. package/dist/export/render-export.cjs +57 -4
  46. package/dist/export/render-export.js +57 -5
  47. package/dist/export/svg/clip-masks.cjs +39 -0
  48. package/dist/export/svg/clip-masks.js +37 -0
  49. package/dist/export/svg/embed-assets.cjs +250 -0
  50. package/dist/export/svg/embed-assets.js +250 -0
  51. package/dist/export/svg/overrides.cjs +32 -0
  52. package/dist/export/svg/overrides.js +32 -0
  53. package/dist/export/svg/raster-object.cjs +82 -0
  54. package/dist/export/svg/raster-object.js +81 -0
  55. package/dist/export/svg/text-decorations.cjs +108 -0
  56. package/dist/export/svg/text-decorations.js +107 -0
  57. package/dist/export/svg/text-on-path.cjs +242 -0
  58. package/dist/export/svg/text-on-path.js +240 -0
  59. package/dist/fabric/fabric-adapter.cjs +49 -6
  60. package/dist/fabric/fabric-adapter.js +49 -6
  61. package/dist/fabric/object-registry.cjs +1 -1
  62. package/dist/fabric/object-registry.js +1 -1
  63. package/dist/fabric/page-state.cjs +69 -0
  64. package/dist/fabric/page-state.js +64 -0
  65. package/dist/history/apply-state.cjs +19 -2
  66. package/dist/history/apply-state.js +19 -2
  67. package/dist/history/create-history.cjs +53 -13
  68. package/dist/history/create-history.d.cts +8 -0
  69. package/dist/history/create-history.d.ts +8 -0
  70. package/dist/history/create-history.js +53 -13
  71. package/dist/history/snapshot.cjs +10 -1
  72. package/dist/history/snapshot.js +10 -2
  73. package/dist/import/svg-import.cjs +256 -0
  74. package/dist/import/svg-import.d.cts +50 -0
  75. package/dist/import/svg-import.d.ts +50 -0
  76. package/dist/import/svg-import.js +256 -0
  77. package/dist/index.cjs +12 -0
  78. package/dist/index.d.cts +10 -5
  79. package/dist/index.d.ts +10 -5
  80. package/dist/index.js +4 -1
  81. package/dist/migrations/migrate-document.cjs +2 -1
  82. package/dist/migrations/migrate-document.js +2 -1
  83. package/dist/pdf/export-pdf.cjs +381 -0
  84. package/dist/pdf/export-pdf.d.cts +73 -0
  85. package/dist/pdf/export-pdf.d.ts +73 -0
  86. package/dist/pdf/export-pdf.js +381 -0
  87. package/dist/pdf/fonts.cjs +115 -0
  88. package/dist/pdf/fonts.d.cts +12 -0
  89. package/dist/pdf/fonts.d.ts +12 -0
  90. package/dist/pdf/fonts.js +112 -0
  91. package/dist/pdf/page-layout.cjs +79 -0
  92. package/dist/pdf/page-layout.d.cts +51 -0
  93. package/dist/pdf/page-layout.d.ts +51 -0
  94. package/dist/pdf/page-layout.js +76 -0
  95. package/dist/pdf.cjs +7 -0
  96. package/dist/pdf.d.cts +4 -0
  97. package/dist/pdf.d.ts +4 -0
  98. package/dist/pdf.js +3 -0
  99. package/dist/react/use-layers.cjs +53 -0
  100. package/dist/react/use-layers.d.cts +10 -0
  101. package/dist/react/use-layers.d.ts +10 -0
  102. package/dist/react/use-layers.js +53 -0
  103. package/dist/react.cjs +2 -0
  104. package/dist/react.d.cts +3 -1
  105. package/dist/react.d.ts +3 -1
  106. package/dist/react.js +2 -1
  107. package/dist/recovery/recovery-controller.cjs +148 -29
  108. package/dist/recovery/recovery-controller.d.cts +8 -0
  109. package/dist/recovery/recovery-controller.d.ts +8 -0
  110. package/dist/recovery/recovery-controller.js +148 -29
  111. package/dist/security/content-limits.cjs +43 -3
  112. package/dist/security/content-limits.d.cts +8 -0
  113. package/dist/security/content-limits.d.ts +8 -0
  114. package/dist/security/content-limits.js +39 -4
  115. package/dist/util/concurrency.cjs +41 -0
  116. package/dist/util/concurrency.js +40 -0
  117. package/package.json +30 -3
  118. package/schema/document-v1.schema.json +5 -1
@@ -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,17 @@ 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 textDecorations = svg.textDecorations ?? "shapes";
40
+ if (textDecorations !== "shapes" && textDecorations !== "css") throw invalid("svg.textDecorations must be \"shapes\" or \"css\"");
41
+ const embedImages = svg.embedImages ?? false;
42
+ if (embedImages !== true && embedImages !== false && embedImages !== "require") throw invalid("svg.embedImages must be true, false or \"require\"");
43
+ const maxEmbeddedImageBytes = svg.maxEmbeddedImageBytes ?? 26214400;
44
+ if (!isPositive(maxEmbeddedImageBytes)) throw invalid("svg.maxEmbeddedImageBytes must be greater than zero");
45
+ const embedFonts = svg.embedFonts ?? {};
46
+ if (typeof embedFonts !== "object" || Array.isArray(embedFonts)) throw invalid("svg.embedFonts must map font families to font files");
35
47
  return {
36
48
  format: options.format,
37
49
  scale,
@@ -39,9 +51,17 @@ function normalizeExportOptions(options) {
39
51
  area,
40
52
  padding,
41
53
  background: options.background ?? "keep",
42
- signal: options.signal
54
+ signal: options.signal,
55
+ svg: {
56
+ textOnPath,
57
+ textDecorations,
58
+ embedImages,
59
+ maxEmbeddedImageBytes,
60
+ embedFonts
61
+ }
43
62
  };
44
63
  }
45
64
  //#endregion
65
+ exports.DEFAULT_MAX_EMBEDDED_IMAGE_BYTES = DEFAULT_MAX_EMBEDDED_IMAGE_BYTES;
46
66
  exports.isRasterFormat = isRasterFormat;
47
67
  exports.normalizeExportOptions = normalizeExportOptions;
@@ -8,6 +8,33 @@ 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
+ * How underlines, overlines and line-throughs of ordinary text are written.
23
+ * `shapes` (the default) draws them where the canvas does; `css` keeps
24
+ * Fabric's `text-decoration`, which readers place in their own way.
25
+ */
26
+ textDecorations?: "shapes" | "css";
27
+ /**
28
+ * Puts every image into the file as data, so the SVG opens anywhere
29
+ * without the original image URLs. `require` blocks the export when an
30
+ * image cannot be embedded; `true` exports anyway with a warning.
31
+ */
32
+ embedImages?: boolean | "require";
33
+ /** The largest image, in bytes, to embed. Default 25 MB. */
34
+ maxEmbeddedImageBytes?: number;
35
+ /** Font files to put into the file, by font family, for the families the canvas uses. */
36
+ embedFonts?: Record<string, FontSource>;
37
+ }
11
38
  export interface ExportOptions {
12
39
  format: ExportFormat;
13
40
  scale?: number;
@@ -16,5 +43,7 @@ export interface ExportOptions {
16
43
  padding?: number;
17
44
  background?: ExportBackground;
18
45
  signal?: AbortSignal;
46
+ /** Options for SVG exports. */
47
+ svg?: SvgExportOptions;
19
48
  }
20
49
  //#endregion
@@ -8,6 +8,33 @@ 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
+ * How underlines, overlines and line-throughs of ordinary text are written.
23
+ * `shapes` (the default) draws them where the canvas does; `css` keeps
24
+ * Fabric's `text-decoration`, which readers place in their own way.
25
+ */
26
+ textDecorations?: "shapes" | "css";
27
+ /**
28
+ * Puts every image into the file as data, so the SVG opens anywhere
29
+ * without the original image URLs. `require` blocks the export when an
30
+ * image cannot be embedded; `true` exports anyway with a warning.
31
+ */
32
+ embedImages?: boolean | "require";
33
+ /** The largest image, in bytes, to embed. Default 25 MB. */
34
+ maxEmbeddedImageBytes?: number;
35
+ /** Font files to put into the file, by font family, for the families the canvas uses. */
36
+ embedFonts?: Record<string, FontSource>;
37
+ }
11
38
  export interface ExportOptions {
12
39
  format: ExportFormat;
13
40
  scale?: number;
@@ -16,5 +43,7 @@ export interface ExportOptions {
16
43
  padding?: number;
17
44
  background?: ExportBackground;
18
45
  signal?: AbortSignal;
46
+ /** Options for SVG exports. */
47
+ svg?: SvgExportOptions;
19
48
  }
20
49
  //#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,17 @@ 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 textDecorations = svg.textDecorations ?? "shapes";
40
+ if (textDecorations !== "shapes" && textDecorations !== "css") throw invalid("svg.textDecorations must be \"shapes\" or \"css\"");
41
+ const embedImages = svg.embedImages ?? false;
42
+ if (embedImages !== true && embedImages !== false && embedImages !== "require") throw invalid("svg.embedImages must be true, false or \"require\"");
43
+ const maxEmbeddedImageBytes = svg.maxEmbeddedImageBytes ?? 26214400;
44
+ if (!isPositive(maxEmbeddedImageBytes)) throw invalid("svg.maxEmbeddedImageBytes must be greater than zero");
45
+ const embedFonts = svg.embedFonts ?? {};
46
+ if (typeof embedFonts !== "object" || Array.isArray(embedFonts)) throw invalid("svg.embedFonts must map font families to font files");
35
47
  return {
36
48
  format: options.format,
37
49
  scale,
@@ -39,8 +51,15 @@ function normalizeExportOptions(options) {
39
51
  area,
40
52
  padding,
41
53
  background: options.background ?? "keep",
42
- signal: options.signal
54
+ signal: options.signal,
55
+ svg: {
56
+ textOnPath,
57
+ textDecorations,
58
+ embedImages,
59
+ maxEmbeddedImageBytes,
60
+ embedFonts
61
+ }
43
62
  };
44
63
  }
45
64
  //#endregion
46
- export { isRasterFormat, normalizeExportOptions };
65
+ 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" | "TOO_LARGE";
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" | "TOO_LARGE";
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)) {
@@ -1,3 +1,9 @@
1
+ const require_object_ids = require("../fabric/object-ids.cjs");
2
+ const require_overrides = require("./svg/overrides.cjs");
3
+ const require_text_on_path = require("./svg/text-on-path.cjs");
4
+ const require_clip_masks = require("./svg/clip-masks.cjs");
5
+ const require_raster_object = require("./svg/raster-object.cjs");
6
+ const require_text_decorations = require("./svg/text-decorations.cjs");
1
7
  //#region src/export/render-export.ts
2
8
  const identity = [
3
9
  1,
@@ -34,12 +40,54 @@ function withExportView(canvas, background, format, work) {
34
40
  }
35
41
  function renderRaster(canvas, area, format, scale, quality) {
36
42
  const rendered = canvas.toCanvasElement(scale, area);
43
+ const release = () => {
44
+ rendered.width = 0;
45
+ rendered.height = 0;
46
+ };
37
47
  return new Promise((resolve, reject) => {
38
48
  rendered.toBlob((blob) => blob ? resolve(blob) : reject(/* @__PURE__ */ new Error(`The browser could not encode ${format}`)), mimeTypes[format], quality);
39
- });
49
+ }).finally(release);
40
50
  }
41
- function renderSvg(canvas, area, scale) {
42
- return canvas.toSVG({
51
+ /**
52
+ * Fabric 6 and 7 write `rotate="..."style="..."` with no space for the
53
+ * spaces in text on a path, which is not valid XML: browsers, Illustrator and
54
+ * Inkscape refuse to open the file. Put the missing space back.
55
+ */
56
+ function repairFabricSvg(svg) {
57
+ return svg.replace(/(rotate="[^"]*")(style=)/g, "$1 $2");
58
+ }
59
+ /**
60
+ * Writes the SVG with the engine's corrections on top of Fabric's output:
61
+ * curved text and decorations as the canvas draws them, inverted clip paths
62
+ * as masks, and objects with nested clip paths as pictures. Every other
63
+ * object is written by Fabric unchanged.
64
+ */
65
+ function renderSvg(canvas, area, scale, options, limits) {
66
+ const textOnPath = (options?.textOnPath ?? "vector") === "vector";
67
+ const decorations = (options?.textDecorations ?? "shapes") === "shapes";
68
+ const rasterizedObjectIds = [];
69
+ const pictures = /* @__PURE__ */ new Map();
70
+ for (const object of canvas.getObjects()) {
71
+ if (!require_clip_masks.hasNestedClipPath(object)) continue;
72
+ pictures.set(object, require_raster_object.rasterMarkup(canvas, object, Math.max(2, scale * 2), limits));
73
+ const id = require_object_ids.readObjectId(object);
74
+ if (id !== void 0) rasterizedObjectIds.push(id);
75
+ }
76
+ const baseWriter = (object) => {
77
+ if (textOnPath && require_text_on_path.isTextOnPath(object)) return (reviver) => require_text_on_path.textOnPathToSVG(object, reviver);
78
+ if (decorations && require_text_decorations.hasStraightDecorations(object)) return (reviver) => require_text_decorations.decoratedTextToSVG(object, reviver);
79
+ };
80
+ const writerFor = (object) => {
81
+ const picture = pictures.get(object);
82
+ if (picture !== void 0) return () => picture;
83
+ const base = baseWriter(object);
84
+ if (require_clip_masks.ownClipPathIsInverted(object)) {
85
+ const prototypeWriter = (reviver) => Object.getPrototypeOf(object).toSVG.call(object, reviver);
86
+ return (reviver) => require_clip_masks.writeWithInvertedMask(object, base ?? prototypeWriter, reviver);
87
+ }
88
+ return base;
89
+ };
90
+ const svg = require_overrides.withSvgOverrides(canvas, writerFor, () => canvas.toSVG({
43
91
  viewBox: {
44
92
  x: area.left,
45
93
  y: area.top,
@@ -48,10 +96,15 @@ function renderSvg(canvas, area, scale) {
48
96
  },
49
97
  width: String(area.width * scale),
50
98
  height: String(area.height * scale)
51
- });
99
+ })).result;
100
+ return {
101
+ svg: repairFabricSvg(svg),
102
+ rasterizedObjectIds
103
+ };
52
104
  }
53
105
  //#endregion
54
106
  exports.mimeTypes = mimeTypes;
55
107
  exports.renderRaster = renderRaster;
56
108
  exports.renderSvg = renderSvg;
109
+ exports.repairFabricSvg = repairFabricSvg;
57
110
  exports.withExportView = withExportView;