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.
- package/README.md +220 -11
- package/README.zh-CN.md +220 -11
- package/dist/assets/asset-manifest.d.cts +3 -0
- package/dist/assets/asset-manifest.d.ts +3 -0
- package/dist/assets/asset-pipeline.cjs +31 -17
- package/dist/assets/asset-pipeline.d.cts +5 -1
- package/dist/assets/asset-pipeline.d.ts +5 -1
- package/dist/assets/asset-pipeline.js +31 -17
- package/dist/assets/font-check.cjs +9 -1
- package/dist/assets/font-check.js +9 -1
- package/dist/assets/image-check.cjs +89 -6
- package/dist/assets/image-check.d.cts +17 -0
- package/dist/assets/image-check.d.ts +17 -0
- package/dist/assets/image-check.js +89 -7
- package/dist/commands/clipboard.cjs +141 -0
- package/dist/commands/clipboard.d.cts +47 -0
- package/dist/commands/clipboard.d.ts +47 -0
- package/dist/commands/clipboard.js +140 -0
- package/dist/commands/layers.cjs +97 -0
- package/dist/commands/layers.d.cts +39 -0
- package/dist/commands/layers.d.ts +39 -0
- package/dist/commands/layers.js +92 -0
- package/dist/document/document-format.d.cts +15 -0
- package/dist/document/document-format.d.ts +15 -0
- package/dist/document/validate-document.cjs +8 -0
- package/dist/document/validate-document.js +8 -0
- package/dist/engine/create-document-engine.cjs +196 -31
- package/dist/engine/create-document-engine.d.cts +44 -7
- package/dist/engine/create-document-engine.d.ts +44 -7
- package/dist/engine/create-document-engine.js +197 -32
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -1
- package/dist/export/batch-render.cjs +172 -0
- package/dist/export/batch-render.d.cts +43 -0
- package/dist/export/batch-render.d.ts +43 -0
- package/dist/export/batch-render.js +171 -0
- package/dist/export/export-options.cjs +21 -1
- package/dist/export/export-options.d.cts +29 -0
- package/dist/export/export-options.d.ts +29 -0
- package/dist/export/export-options.js +21 -2
- package/dist/export/preflight-export.cjs +11 -2
- package/dist/export/preflight-export.d.cts +1 -1
- package/dist/export/preflight-export.d.ts +1 -1
- package/dist/export/preflight-export.js +11 -2
- package/dist/export/render-export.cjs +57 -4
- package/dist/export/render-export.js +57 -5
- package/dist/export/svg/clip-masks.cjs +39 -0
- package/dist/export/svg/clip-masks.js +37 -0
- package/dist/export/svg/embed-assets.cjs +250 -0
- package/dist/export/svg/embed-assets.js +250 -0
- package/dist/export/svg/overrides.cjs +32 -0
- package/dist/export/svg/overrides.js +32 -0
- package/dist/export/svg/raster-object.cjs +82 -0
- package/dist/export/svg/raster-object.js +81 -0
- package/dist/export/svg/text-decorations.cjs +108 -0
- package/dist/export/svg/text-decorations.js +107 -0
- package/dist/export/svg/text-on-path.cjs +242 -0
- package/dist/export/svg/text-on-path.js +240 -0
- package/dist/fabric/fabric-adapter.cjs +49 -6
- package/dist/fabric/fabric-adapter.js +49 -6
- package/dist/fabric/object-registry.cjs +1 -1
- package/dist/fabric/object-registry.js +1 -1
- package/dist/fabric/page-state.cjs +69 -0
- package/dist/fabric/page-state.js +64 -0
- package/dist/history/apply-state.cjs +19 -2
- package/dist/history/apply-state.js +19 -2
- package/dist/history/create-history.cjs +53 -13
- package/dist/history/create-history.d.cts +8 -0
- package/dist/history/create-history.d.ts +8 -0
- package/dist/history/create-history.js +53 -13
- package/dist/history/snapshot.cjs +10 -1
- package/dist/history/snapshot.js +10 -2
- package/dist/import/svg-import.cjs +256 -0
- package/dist/import/svg-import.d.cts +50 -0
- package/dist/import/svg-import.d.ts +50 -0
- package/dist/import/svg-import.js +256 -0
- package/dist/index.cjs +12 -0
- package/dist/index.d.cts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +4 -1
- package/dist/migrations/migrate-document.cjs +2 -1
- package/dist/migrations/migrate-document.js +2 -1
- package/dist/pdf/export-pdf.cjs +381 -0
- package/dist/pdf/export-pdf.d.cts +73 -0
- package/dist/pdf/export-pdf.d.ts +73 -0
- package/dist/pdf/export-pdf.js +381 -0
- package/dist/pdf/fonts.cjs +115 -0
- package/dist/pdf/fonts.d.cts +12 -0
- package/dist/pdf/fonts.d.ts +12 -0
- package/dist/pdf/fonts.js +112 -0
- package/dist/pdf/page-layout.cjs +79 -0
- package/dist/pdf/page-layout.d.cts +51 -0
- package/dist/pdf/page-layout.d.ts +51 -0
- package/dist/pdf/page-layout.js +76 -0
- package/dist/pdf.cjs +7 -0
- package/dist/pdf.d.cts +4 -0
- package/dist/pdf.d.ts +4 -0
- package/dist/pdf.js +3 -0
- package/dist/react/use-layers.cjs +53 -0
- package/dist/react/use-layers.d.cts +10 -0
- package/dist/react/use-layers.d.ts +10 -0
- package/dist/react/use-layers.js +53 -0
- package/dist/react.cjs +2 -0
- package/dist/react.d.cts +3 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.js +2 -1
- package/dist/recovery/recovery-controller.cjs +148 -29
- package/dist/recovery/recovery-controller.d.cts +8 -0
- package/dist/recovery/recovery-controller.d.ts +8 -0
- package/dist/recovery/recovery-controller.js +148 -29
- package/dist/security/content-limits.cjs +43 -3
- package/dist/security/content-limits.d.cts +8 -0
- package/dist/security/content-limits.d.ts +8 -0
- package/dist/security/content-limits.js +39 -4
- package/dist/util/concurrency.cjs +41 -0
- package/dist/util/concurrency.js +40 -0
- package/package.json +30 -3
- 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,
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/history/snapshot.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
//#region src/history/snapshot.ts
|
|
2
|
-
|
|
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
|