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
@@ -1,6 +1,6 @@
1
+ const require_snapshot = require("./snapshot.cjs");
1
2
  const require_apply_state = require("./apply-state.cjs");
2
3
  const require_history_stack = require("./history-stack.cjs");
3
- const require_snapshot = require("./snapshot.cjs");
4
4
  //#region src/history/create-history.ts
5
5
  const verbsByAction = {
6
6
  drag: "Move",
@@ -24,18 +24,23 @@ function describePendingChanges(changes) {
24
24
  return changes.length === 1 ? `${verb} ${changes[0].noun}` : `${verb} ${changes.length} objects`;
25
25
  }
26
26
  function createHistory(options) {
27
- const { canvas, serializeObjects, onChange, onContentChange } = options;
27
+ const { canvas, serializeState, onChange, onContentChange, onLiveChange } = options;
28
+ const takeSnapshot = () => {
29
+ const { objects, page } = serializeState();
30
+ return require_snapshot.createSnapshot(objects, page);
31
+ };
28
32
  const stack = require_history_stack.createHistoryStack({
29
33
  steps: Math.max(1, options.limit ?? 100),
30
34
  bytes: Math.max(0, options.maxBytes ?? 67108864)
31
35
  });
32
- let snapshot = require_snapshot.createSnapshot(serializeObjects());
36
+ let snapshot = takeSnapshot();
33
37
  let pendingChanges = [];
34
38
  let flushScheduled = false;
35
39
  let transactionDepth = 0;
36
40
  let pausedDepth = 0;
37
41
  let applying = false;
38
42
  let queue = Promise.resolve();
43
+ let epoch = 0;
39
44
  function state() {
40
45
  const undoLabels = stack.undoLabels();
41
46
  const redoLabels = stack.redoLabels();
@@ -48,7 +53,7 @@ function createHistory(options) {
48
53
  }
49
54
  function commit(label) {
50
55
  pendingChanges = [];
51
- const nextSnapshot = require_snapshot.createSnapshot(serializeObjects());
56
+ const nextSnapshot = takeSnapshot();
52
57
  const difference = require_snapshot.diffSnapshots(snapshot, nextSnapshot);
53
58
  snapshot = nextSnapshot;
54
59
  if (difference === null) return false;
@@ -86,26 +91,56 @@ function createHistory(options) {
86
91
  notice(verbsByAction[action] ?? "Transform", event.target);
87
92
  };
88
93
  const handleTextEdited = ({ target }) => notice("Edit", target);
94
+ const handleTextChanged = () => {
95
+ if (!applying && pausedDepth === 0) onLiveChange?.();
96
+ };
89
97
  const listeners = [
90
98
  ["object:added", handleAdded],
91
99
  ["object:removed", handleRemoved],
92
100
  ["object:modified", handleModified],
93
- ["text:editing:exited", handleTextEdited]
101
+ ["text:editing:exited", handleTextEdited],
102
+ ["text:changed", handleTextChanged]
94
103
  ];
95
104
  const eventTarget = canvas;
96
105
  listeners.forEach(([name, handler]) => eventTarget.on(name, handler));
97
- function transaction(label, work) {
106
+ /**
107
+ * Puts the canvas back to the last recorded snapshot. The snapshot is the
108
+ * state before the transaction, so the difference to undo is exactly what
109
+ * the transaction changed: one serialization, then only the changed
110
+ * objects are rebuilt.
111
+ */
112
+ function rollBack() {
113
+ const difference = require_snapshot.diffSnapshots(snapshot, takeSnapshot());
114
+ pendingChanges = [];
115
+ if (difference === null) return Promise.resolve();
116
+ applying = true;
117
+ return runInOrder(async () => {
118
+ try {
119
+ await require_apply_state.applyStateChange(canvas, difference.before);
120
+ } finally {
121
+ applying = false;
122
+ }
123
+ });
124
+ }
125
+ function transaction(label, work, transactionOptions = {}) {
98
126
  if (transactionDepth === 0) commitPendingChanges();
127
+ const outermost = transactionDepth === 0;
99
128
  transactionDepth += 1;
100
129
  const finish = () => {
101
130
  transactionDepth -= 1;
102
131
  if (transactionDepth === 0) commit(label);
103
132
  };
133
+ const fail = () => {
134
+ transactionDepth -= 1;
135
+ if (transactionDepth > 0) return void 0;
136
+ if (outermost && transactionOptions.rollback) return rollBack();
137
+ commit(label);
138
+ };
104
139
  let result;
105
140
  try {
106
141
  result = work();
107
142
  } catch (error) {
108
- finish();
143
+ fail()?.catch(() => void 0);
109
144
  throw error;
110
145
  }
111
146
  if (!isPromiseLike(result)) {
@@ -115,8 +150,8 @@ function createHistory(options) {
115
150
  return Promise.resolve(result).then((value) => {
116
151
  finish();
117
152
  return value;
118
- }, (error) => {
119
- finish();
153
+ }, async (error) => {
154
+ await fail();
120
155
  throw error;
121
156
  });
122
157
  }
@@ -148,16 +183,19 @@ function createHistory(options) {
148
183
  commitPendingChanges();
149
184
  const step = takeStep();
150
185
  if (step === void 0) return false;
186
+ const startedIn = epoch;
151
187
  applying = true;
188
+ let applied;
152
189
  try {
153
- await require_apply_state.applyStateChange(canvas, pickChange(step));
190
+ applied = await require_apply_state.applyStateChange(canvas, pickChange(step), () => epoch !== startedIn);
154
191
  } catch (error) {
155
- putBack(step);
192
+ if (epoch === startedIn) putBack(step);
156
193
  throw error;
157
194
  } finally {
158
195
  applying = false;
159
196
  }
160
- snapshot = require_snapshot.createSnapshot(serializeObjects());
197
+ if (!applied) return false;
198
+ snapshot = takeSnapshot();
161
199
  moveTo(step);
162
200
  onContentChange();
163
201
  onChange(state());
@@ -170,6 +208,7 @@ function createHistory(options) {
170
208
  return commit(label);
171
209
  },
172
210
  transaction,
211
+ settled: () => queue.then(() => void 0),
173
212
  undo: () => travel(() => stack.takeUndo(), (step) => step.before, (step) => stack.returnUndo(step), (step) => stack.returnRedo(step)),
174
213
  redo: () => travel(() => stack.takeRedo(), (step) => step.after, (step) => stack.returnRedo(step), (step) => stack.returnUndo(step)),
175
214
  state,
@@ -182,7 +221,8 @@ function createHistory(options) {
182
221
  reset() {
183
222
  pendingChanges = [];
184
223
  stack.clear();
185
- snapshot = require_snapshot.createSnapshot(serializeObjects());
224
+ epoch += 1;
225
+ snapshot = takeSnapshot();
186
226
  onChange(state());
187
227
  },
188
228
  destroy() {
@@ -10,4 +10,12 @@ export interface HistoryState {
10
10
  undoLabel: string | undefined;
11
11
  redoLabel: string | undefined;
12
12
  }
13
+ export interface TransactionOptions {
14
+ /**
15
+ * When `work` throws or rejects, put the canvas back as it was before the
16
+ * transaction, record no undo step and leave the document's unsaved state
17
+ * alone. Applies to the outermost transaction.
18
+ */
19
+ rollback?: boolean;
20
+ }
13
21
  //#endregion
@@ -10,4 +10,12 @@ export interface HistoryState {
10
10
  undoLabel: string | undefined;
11
11
  redoLabel: string | undefined;
12
12
  }
13
+ export interface TransactionOptions {
14
+ /**
15
+ * When `work` throws or rejects, put the canvas back as it was before the
16
+ * transaction, record no undo step and leave the document's unsaved state
17
+ * alone. Applies to the outermost transaction.
18
+ */
19
+ rollback?: boolean;
20
+ }
13
21
  //#endregion
@@ -1,6 +1,6 @@
1
+ import { createSnapshot, diffSnapshots } from "./snapshot.js";
1
2
  import { applyStateChange } from "./apply-state.js";
2
3
  import { createHistoryStack } from "./history-stack.js";
3
- import { createSnapshot, diffSnapshots } from "./snapshot.js";
4
4
  //#region src/history/create-history.ts
5
5
  const verbsByAction = {
6
6
  drag: "Move",
@@ -24,18 +24,23 @@ function describePendingChanges(changes) {
24
24
  return changes.length === 1 ? `${verb} ${changes[0].noun}` : `${verb} ${changes.length} objects`;
25
25
  }
26
26
  function createHistory(options) {
27
- const { canvas, serializeObjects, onChange, onContentChange } = options;
27
+ const { canvas, serializeState, onChange, onContentChange, onLiveChange } = options;
28
+ const takeSnapshot = () => {
29
+ const { objects, page } = serializeState();
30
+ return createSnapshot(objects, page);
31
+ };
28
32
  const stack = createHistoryStack({
29
33
  steps: Math.max(1, options.limit ?? 100),
30
34
  bytes: Math.max(0, options.maxBytes ?? 67108864)
31
35
  });
32
- let snapshot = createSnapshot(serializeObjects());
36
+ let snapshot = takeSnapshot();
33
37
  let pendingChanges = [];
34
38
  let flushScheduled = false;
35
39
  let transactionDepth = 0;
36
40
  let pausedDepth = 0;
37
41
  let applying = false;
38
42
  let queue = Promise.resolve();
43
+ let epoch = 0;
39
44
  function state() {
40
45
  const undoLabels = stack.undoLabels();
41
46
  const redoLabels = stack.redoLabels();
@@ -48,7 +53,7 @@ function createHistory(options) {
48
53
  }
49
54
  function commit(label) {
50
55
  pendingChanges = [];
51
- const nextSnapshot = createSnapshot(serializeObjects());
56
+ const nextSnapshot = takeSnapshot();
52
57
  const difference = diffSnapshots(snapshot, nextSnapshot);
53
58
  snapshot = nextSnapshot;
54
59
  if (difference === null) return false;
@@ -86,26 +91,56 @@ function createHistory(options) {
86
91
  notice(verbsByAction[action] ?? "Transform", event.target);
87
92
  };
88
93
  const handleTextEdited = ({ target }) => notice("Edit", target);
94
+ const handleTextChanged = () => {
95
+ if (!applying && pausedDepth === 0) onLiveChange?.();
96
+ };
89
97
  const listeners = [
90
98
  ["object:added", handleAdded],
91
99
  ["object:removed", handleRemoved],
92
100
  ["object:modified", handleModified],
93
- ["text:editing:exited", handleTextEdited]
101
+ ["text:editing:exited", handleTextEdited],
102
+ ["text:changed", handleTextChanged]
94
103
  ];
95
104
  const eventTarget = canvas;
96
105
  listeners.forEach(([name, handler]) => eventTarget.on(name, handler));
97
- function transaction(label, work) {
106
+ /**
107
+ * Puts the canvas back to the last recorded snapshot. The snapshot is the
108
+ * state before the transaction, so the difference to undo is exactly what
109
+ * the transaction changed: one serialization, then only the changed
110
+ * objects are rebuilt.
111
+ */
112
+ function rollBack() {
113
+ const difference = diffSnapshots(snapshot, takeSnapshot());
114
+ pendingChanges = [];
115
+ if (difference === null) return Promise.resolve();
116
+ applying = true;
117
+ return runInOrder(async () => {
118
+ try {
119
+ await applyStateChange(canvas, difference.before);
120
+ } finally {
121
+ applying = false;
122
+ }
123
+ });
124
+ }
125
+ function transaction(label, work, transactionOptions = {}) {
98
126
  if (transactionDepth === 0) commitPendingChanges();
127
+ const outermost = transactionDepth === 0;
99
128
  transactionDepth += 1;
100
129
  const finish = () => {
101
130
  transactionDepth -= 1;
102
131
  if (transactionDepth === 0) commit(label);
103
132
  };
133
+ const fail = () => {
134
+ transactionDepth -= 1;
135
+ if (transactionDepth > 0) return void 0;
136
+ if (outermost && transactionOptions.rollback) return rollBack();
137
+ commit(label);
138
+ };
104
139
  let result;
105
140
  try {
106
141
  result = work();
107
142
  } catch (error) {
108
- finish();
143
+ fail()?.catch(() => void 0);
109
144
  throw error;
110
145
  }
111
146
  if (!isPromiseLike(result)) {
@@ -115,8 +150,8 @@ function createHistory(options) {
115
150
  return Promise.resolve(result).then((value) => {
116
151
  finish();
117
152
  return value;
118
- }, (error) => {
119
- finish();
153
+ }, async (error) => {
154
+ await fail();
120
155
  throw error;
121
156
  });
122
157
  }
@@ -148,16 +183,19 @@ function createHistory(options) {
148
183
  commitPendingChanges();
149
184
  const step = takeStep();
150
185
  if (step === void 0) return false;
186
+ const startedIn = epoch;
151
187
  applying = true;
188
+ let applied;
152
189
  try {
153
- await applyStateChange(canvas, pickChange(step));
190
+ applied = await applyStateChange(canvas, pickChange(step), () => epoch !== startedIn);
154
191
  } catch (error) {
155
- putBack(step);
192
+ if (epoch === startedIn) putBack(step);
156
193
  throw error;
157
194
  } finally {
158
195
  applying = false;
159
196
  }
160
- snapshot = createSnapshot(serializeObjects());
197
+ if (!applied) return false;
198
+ snapshot = takeSnapshot();
161
199
  moveTo(step);
162
200
  onContentChange();
163
201
  onChange(state());
@@ -170,6 +208,7 @@ function createHistory(options) {
170
208
  return commit(label);
171
209
  },
172
210
  transaction,
211
+ settled: () => queue.then(() => void 0),
173
212
  undo: () => travel(() => stack.takeUndo(), (step) => step.before, (step) => stack.returnUndo(step), (step) => stack.returnRedo(step)),
174
213
  redo: () => travel(() => stack.takeRedo(), (step) => step.after, (step) => stack.returnRedo(step), (step) => stack.returnUndo(step)),
175
214
  state,
@@ -182,7 +221,8 @@ function createHistory(options) {
182
221
  reset() {
183
222
  pendingChanges = [];
184
223
  stack.clear();
185
- snapshot = createSnapshot(serializeObjects());
224
+ epoch += 1;
225
+ snapshot = takeSnapshot();
186
226
  onChange(state());
187
227
  },
188
228
  destroy() {
@@ -1,7 +1,15 @@
1
1
  //#region src/history/snapshot.ts
2
- function createSnapshot(objects) {
2
+ /**
3
+ * The page itself (size, background, overlay, mask) is one more entry in the
4
+ * same map under this reserved key. It never appears in `order`, so the
5
+ * diff stays proportional to what changed and object ordering is unaffected.
6
+ * Object ids come from `createId`, which never produces this value.
7
+ */
8
+ const PAGE_KEY = "__page__";
9
+ function createSnapshot(objects, page) {
3
10
  const objectsById = /* @__PURE__ */ new Map();
4
11
  const order = [];
12
+ if (page !== void 0) objectsById.set(PAGE_KEY, JSON.stringify(page));
5
13
  for (const object of objects) {
6
14
  const id = object.id ?? "";
7
15
  objectsById.set(id, JSON.stringify(object));
@@ -43,5 +51,6 @@ function diffSnapshots(previous, next) {
43
51
  };
44
52
  }
45
53
  //#endregion
54
+ exports.PAGE_KEY = PAGE_KEY;
46
55
  exports.createSnapshot = createSnapshot;
47
56
  exports.diffSnapshots = diffSnapshots;
@@ -1,7 +1,15 @@
1
1
  //#region src/history/snapshot.ts
2
- function createSnapshot(objects) {
2
+ /**
3
+ * The page itself (size, background, overlay, mask) is one more entry in the
4
+ * same map under this reserved key. It never appears in `order`, so the
5
+ * diff stays proportional to what changed and object ordering is unaffected.
6
+ * Object ids come from `createId`, which never produces this value.
7
+ */
8
+ const PAGE_KEY = "__page__";
9
+ function createSnapshot(objects, page) {
3
10
  const objectsById = /* @__PURE__ */ new Map();
4
11
  const order = [];
12
+ if (page !== void 0) objectsById.set(PAGE_KEY, JSON.stringify(page));
5
13
  for (const object of objects) {
6
14
  const id = object.id ?? "";
7
15
  objectsById.set(id, JSON.stringify(object));
@@ -43,4 +51,4 @@ function diffSnapshots(previous, next) {
43
51
  };
44
52
  }
45
53
  //#endregion
46
- export { createSnapshot, diffSnapshots };
54
+ export { PAGE_KEY, createSnapshot, diffSnapshots };
@@ -0,0 +1,256 @@
1
+ const require_errors = require("../engine/errors.cjs");
2
+ const require_content_limits = require("../security/content-limits.cjs");
3
+ let fabric = require("fabric");
4
+ //#region src/import/svg-import.ts
5
+ const XLINK = "http://www.w3.org/1999/xlink";
6
+ const REMOVED_ELEMENTS = [
7
+ "script",
8
+ "foreignObject",
9
+ "iframe",
10
+ "object",
11
+ "embed",
12
+ "audio",
13
+ "video",
14
+ "animate",
15
+ "set",
16
+ "animateTransform",
17
+ "animateMotion"
18
+ ];
19
+ function refuse(message, cause) {
20
+ throw new require_errors.DocumentEngineError("SVG_IMPORT_FAILED", message, cause === void 0 ? {} : { cause });
21
+ }
22
+ function unsafe(message) {
23
+ throw new require_errors.DocumentEngineError("UNSAFE_DOCUMENT", `The SVG was refused: ${message}`, { issues: [{
24
+ code: "UNSAFE_DOCUMENT",
25
+ path: "",
26
+ message
27
+ }] });
28
+ }
29
+ function parse(svg) {
30
+ if (typeof DOMParser === "undefined") refuse("Importing SVG needs a browser DOM");
31
+ const document = new DOMParser().parseFromString(svg.trim(), "image/svg+xml");
32
+ const error = document.querySelector("parsererror");
33
+ if (error) refuse(`The SVG is not valid XML: ${(error.textContent ?? "").trim().slice(0, 200)}`);
34
+ if (document.documentElement.localName !== "svg") refuse("The file is not an SVG: its root element is not <svg>");
35
+ return document;
36
+ }
37
+ function depthOf(element) {
38
+ let deepest = 0;
39
+ const pending = [[element, 1]];
40
+ while (pending.length > 0) {
41
+ const [current, depth] = pending.pop();
42
+ deepest = Math.max(deepest, depth);
43
+ for (const child of Array.from(current.children)) pending.push([child, depth + 1]);
44
+ }
45
+ return deepest;
46
+ }
47
+ /**
48
+ * Removes what an SVG can use to run code or reach other servers. Fabric
49
+ * never runs scripts, but the same file may be shown elsewhere later.
50
+ */
51
+ function sanitize(document, limits, warnings) {
52
+ const root = document.documentElement;
53
+ const elements = root.getElementsByTagName("*").length;
54
+ const maxObjects = limits.maxObjects ?? 5e4;
55
+ if (elements > maxObjects) unsafe(`it has ${elements} elements, the limit is ${maxObjects}`);
56
+ const maxDepth = limits.maxDepth ?? 100;
57
+ const depth = depthOf(root);
58
+ if (depth > maxDepth) unsafe(`it is nested ${depth} levels deep, the limit is ${maxDepth}`);
59
+ let removed = 0;
60
+ for (const name of REMOVED_ELEMENTS) for (const element of Array.from(root.getElementsByTagName(name))) {
61
+ element.remove();
62
+ removed += 1;
63
+ }
64
+ const isAllowed = limits.isAllowedUrl ?? require_content_limits.isSafeImageUrl;
65
+ for (const element of [root, ...Array.from(root.getElementsByTagName("*"))]) {
66
+ for (const attribute of Array.from(element.attributes)) if (/^on/i.test(attribute.name)) {
67
+ element.removeAttribute(attribute.name);
68
+ removed += 1;
69
+ }
70
+ const link = element.getAttribute("href") ?? element.getAttributeNS(XLINK, "href");
71
+ if (link === null) continue;
72
+ const target = link.trim();
73
+ if (element.localName === "image") {
74
+ if (!isAllowed(target) || /^javascript:/i.test(target)) {
75
+ warnings.push({
76
+ code: "SVG_IMAGE_BLOCKED",
77
+ message: `An image with the address ${target.slice(0, 80)} was left out because the address is not allowed`
78
+ });
79
+ element.remove();
80
+ }
81
+ } else if (!target.startsWith("#")) {
82
+ element.removeAttribute("href");
83
+ element.removeAttributeNS(XLINK, "href");
84
+ removed += 1;
85
+ }
86
+ }
87
+ for (const style of Array.from(root.getElementsByTagName("style"))) {
88
+ const css = style.textContent ?? "";
89
+ const cleaned = css.replace(/@import[^;]*;?/gi, "").replace(/url\(\s*['"]?(?!#)[^)]*\)/gi, "none");
90
+ if (cleaned !== css) {
91
+ style.textContent = cleaned;
92
+ removed += 1;
93
+ }
94
+ }
95
+ if (removed > 0) warnings.push({
96
+ code: "SVG_CONTENT_REMOVED",
97
+ message: `${removed} scripts, event handlers, embedded pages or links to other files were removed from the SVG`
98
+ });
99
+ }
100
+ function intersectsViewport(object, width, height) {
101
+ object.setCoords();
102
+ const box = object.getBoundingRect();
103
+ return box.left < width && box.top < height && box.left + box.width > 0 && box.top + box.height > 0;
104
+ }
105
+ function contentBounds(objects) {
106
+ let left = Infinity;
107
+ let top = Infinity;
108
+ let right = -Infinity;
109
+ let bottom = -Infinity;
110
+ for (const object of objects) {
111
+ object.setCoords();
112
+ const box = object.getBoundingRect();
113
+ left = Math.min(left, box.left);
114
+ top = Math.min(top, box.top);
115
+ right = Math.max(right, box.left + box.width);
116
+ bottom = Math.max(bottom, box.top + box.height);
117
+ }
118
+ return Number.isFinite(left) ? {
119
+ left,
120
+ top,
121
+ width: right - left,
122
+ height: bottom - top
123
+ } : {
124
+ left: 0,
125
+ top: 0,
126
+ width: 0,
127
+ height: 0
128
+ };
129
+ }
130
+ /** Parses and places an SVG. Adding the result to a canvas is up to the caller. */
131
+ async function readSvg(svg, options = {}, limits = {}) {
132
+ if (typeof svg !== "string" || svg.trim().length === 0) refuse("The SVG is empty");
133
+ const warnings = [];
134
+ const document = parse(svg);
135
+ sanitize(document, limits, warnings);
136
+ let parsed;
137
+ try {
138
+ parsed = await (0, fabric.parseSVGDocument)(document, void 0, {
139
+ crossOrigin: options.crossOrigin === void 0 ? "anonymous" : options.crossOrigin,
140
+ signal: options.signal
141
+ });
142
+ } catch (error) {
143
+ if (require_errors.isDocumentEngineError(error)) throw error;
144
+ refuse(`Fabric could not read the SVG: ${error instanceof Error ? error.message : String(error)}`, error);
145
+ }
146
+ if (options.signal?.aborted) throw new require_errors.DocumentEngineError("LOAD_ABORTED", "The SVG import was cancelled");
147
+ let objects = parsed.objects.filter((object) => object !== null && object !== void 0);
148
+ const declared = {
149
+ width: parsed.options.width ?? 0,
150
+ height: parsed.options.height ?? 0
151
+ };
152
+ const useContent = options.viewport === "content" || declared.width <= 0 || declared.height <= 0;
153
+ const bounds = useContent ? contentBounds(objects) : {
154
+ left: 0,
155
+ top: 0,
156
+ ...declared
157
+ };
158
+ const offscreen = options.offscreen ?? "keep";
159
+ if (offscreen === "drop" && !useContent) {
160
+ const kept = objects.filter((object) => intersectsViewport(object, bounds.width, bounds.height));
161
+ if (kept.length < objects.length) warnings.push({
162
+ code: "SVG_OFFSCREEN_DROPPED",
163
+ message: `${objects.length - kept.length} elements outside the SVG's viewport were left out`
164
+ });
165
+ objects = kept;
166
+ }
167
+ const viewport = {
168
+ width: bounds.width,
169
+ height: bounds.height
170
+ };
171
+ if (objects.length === 0) return {
172
+ objects: [],
173
+ viewport,
174
+ warnings
175
+ };
176
+ const group = new fabric.Group([], {
177
+ layoutManager: new fabric.LayoutManager(new fabric.FixedLayout()),
178
+ width: bounds.width,
179
+ height: bounds.height,
180
+ left: bounds.left + bounds.width / 2,
181
+ top: bounds.top + bounds.height / 2,
182
+ originX: "center",
183
+ originY: "center"
184
+ });
185
+ group.add(...objects);
186
+ if (offscreen === "clip") group.clipPath = new fabric.Rect({
187
+ width: bounds.width,
188
+ height: bounds.height,
189
+ originX: "center",
190
+ originY: "center",
191
+ left: 0,
192
+ top: 0
193
+ });
194
+ let scaleX = 1;
195
+ let scaleY = 1;
196
+ let shiftX = 0;
197
+ let shiftY = 0;
198
+ const fit = options.fit;
199
+ if (fit && bounds.width > 0 && bounds.height > 0) {
200
+ const sx = fit.width / bounds.width;
201
+ const sy = fit.height / bounds.height;
202
+ const mode = fit.mode ?? "contain";
203
+ if (mode === "fill") {
204
+ scaleX = sx;
205
+ scaleY = sy;
206
+ } else {
207
+ scaleX = scaleY = mode === "cover" ? Math.max(sx, sy) : Math.min(sx, sy);
208
+ shiftX = (fit.width - bounds.width * scaleX) / 2;
209
+ shiftY = (fit.height - bounds.height * scaleY) / 2;
210
+ }
211
+ }
212
+ group.set({
213
+ scaleX,
214
+ scaleY,
215
+ originX: "left",
216
+ originY: "top",
217
+ left: (options.left ?? 0) + shiftX,
218
+ top: (options.top ?? 0) + shiftY
219
+ });
220
+ group.setCoords();
221
+ if ((options.as ?? "group") === "group") return {
222
+ objects: [group],
223
+ viewport,
224
+ warnings
225
+ };
226
+ const clip = group.clipPath;
227
+ group.clipPath = void 0;
228
+ const loose = group.removeAll();
229
+ if (clip) {
230
+ const frame = group.calcTransformMatrix();
231
+ for (const object of loose) {
232
+ const absoluteClip = new fabric.Rect({
233
+ width: bounds.width,
234
+ height: bounds.height,
235
+ originX: "center",
236
+ originY: "center",
237
+ absolutePositioned: true
238
+ });
239
+ absoluteClip.set({
240
+ left: frame[4],
241
+ top: frame[5],
242
+ scaleX,
243
+ scaleY
244
+ });
245
+ object.clipPath = absoluteClip;
246
+ }
247
+ }
248
+ loose.forEach((object) => object.setCoords());
249
+ return {
250
+ objects: loose,
251
+ viewport,
252
+ warnings
253
+ };
254
+ }
255
+ //#endregion
256
+ exports.readSvg = readSvg;
@@ -0,0 +1,50 @@
1
+ import { FabricObject } from "fabric";
2
+ //#region src/import/svg-import.d.ts
3
+ /**
4
+ * Opens SVG files as Fabric objects without moving them (fabric.js #10916).
5
+ *
6
+ * Fabric's parser already puts every element where the SVG's viewBox says.
7
+ * The usual next step, `util.groupSVGElements`, then sizes the group to the
8
+ * content, so an element outside the viewBox, or a hidden one, moves and
9
+ * resizes everything. This keeps the SVG's own viewport as the frame instead.
10
+ */
11
+ export interface SvgImportOptions {
12
+ /**
13
+ * `preserve` (the default) keeps the SVG's viewport, so the artwork lands
14
+ * where it sits in the file. `content` uses the bounds of what is drawn,
15
+ * like `util.groupSVGElements`.
16
+ */
17
+ viewport?: "preserve" | "content";
18
+ /** What to do with elements entirely outside the viewport: keep them, drop them, or clip them. Default `keep`. */
19
+ offscreen?: "keep" | "drop" | "clip";
20
+ /** One group (the default), or separate objects. */
21
+ as?: "group" | "objects";
22
+ /** Where the viewport's top-left corner lands on the canvas. Default 0, 0. */
23
+ left?: number;
24
+ top?: number;
25
+ /** Scales the viewport into this box. `contain` keeps the whole SVG visible, `cover` fills the box, `fill` stretches. */
26
+ fit?: {
27
+ width: number;
28
+ height: number;
29
+ mode?: "contain" | "cover" | "fill";
30
+ };
31
+ /** Passed to images in the SVG. Default `anonymous`, so the canvas can still be exported. */
32
+ crossOrigin?: "anonymous" | "use-credentials" | null;
33
+ signal?: AbortSignal;
34
+ }
35
+ export type SvgImportWarningCode = "SVG_CONTENT_REMOVED" | "SVG_IMAGE_BLOCKED" | "SVG_OFFSCREEN_DROPPED";
36
+ export interface SvgImportWarning {
37
+ code: SvgImportWarningCode;
38
+ message: string;
39
+ }
40
+ export interface SvgImportResult {
41
+ /** The objects added to the canvas: one group, or each object. */
42
+ objects: FabricObject[];
43
+ /** The size of the SVG's viewport in canvas units, before `fit`. */
44
+ viewport: {
45
+ width: number;
46
+ height: number;
47
+ };
48
+ warnings: SvgImportWarning[];
49
+ }
50
+ //#endregion