headless-svg-to-excalidraw 0.0.1
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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +656 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Excalidraw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# svg-to-excalidraw
|
|
2
|
+
|
|
3
|
+
Library to convert SVG to Excalidraw’s file format.
|
|
4
|
+
|
|
5
|
+
## :floppy_disk: Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add svg-to-excalidraw
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## :beginner: Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import svgToEx from "svg-to-excalidraw";
|
|
15
|
+
|
|
16
|
+
const heartSVG = `
|
|
17
|
+
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
18
|
+
<path d="M 10,30
|
|
19
|
+
A 20,20 0,0,1 50,30
|
|
20
|
+
A 20,20 0,0,1 90,30
|
|
21
|
+
Q 90,60 50,90
|
|
22
|
+
Q 10,60 10,30 z"/>
|
|
23
|
+
</svg>
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const { hasErrors, errors, content } = svgToEx.convert(heartSVG);
|
|
27
|
+
|
|
28
|
+
// SVG parsing errors are propagated through.
|
|
29
|
+
if (hasErrors) {
|
|
30
|
+
console.error(errors);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
navigator.clipboard.writeText(content);
|
|
35
|
+
|
|
36
|
+
// the heart excalidraw json is now copied to your clipboard.
|
|
37
|
+
// Just Paste it into your Excalidraw session!
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## :game_die: Running tests
|
|
41
|
+
|
|
42
|
+
TODO.
|
|
43
|
+
|
|
44
|
+
### :building_construction: Local Development
|
|
45
|
+
|
|
46
|
+
#### Building the Project
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
yarn build
|
|
50
|
+
|
|
51
|
+
# Build and watch whenever a file is updated
|
|
52
|
+
yarn build:watch
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## :busts_in_silhouette: Contributing
|
|
56
|
+
|
|
57
|
+
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/svg-to-excalidraw/issues) first to discuss what you would like to change.
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { mat4, vec3 } from "gl-matrix";
|
|
2
|
+
import { Random } from "roughjs/bin/math";
|
|
3
|
+
import { nanoid } from "nanoid";
|
|
4
|
+
import chroma from "chroma-js";
|
|
5
|
+
import { DOMParser, parseHTML } from "linkedom";
|
|
6
|
+
import { pointsOnPath } from "points-on-path";
|
|
7
|
+
|
|
8
|
+
//#region src/elements/ExcalidrawScene.ts
|
|
9
|
+
var ExcalidrawScene = class {
|
|
10
|
+
type = "excalidraw";
|
|
11
|
+
version = 2;
|
|
12
|
+
source = "https://excalidraw.com";
|
|
13
|
+
elements = [];
|
|
14
|
+
constructor(elements = []) {
|
|
15
|
+
this.elements = elements;
|
|
16
|
+
}
|
|
17
|
+
toExJSON() {
|
|
18
|
+
return {
|
|
19
|
+
...this,
|
|
20
|
+
elements: this.elements.map((el) => ({ ...el }))
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var ExcalidrawScene_default = ExcalidrawScene;
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/utils.ts
|
|
28
|
+
const random = new Random(Date.now());
|
|
29
|
+
const randomInteger = () => Math.floor(random.next() * 2 ** 31);
|
|
30
|
+
const randomId = () => nanoid();
|
|
31
|
+
function dimensionsFromPoints(points) {
|
|
32
|
+
const xCoords = points.map(([x]) => x);
|
|
33
|
+
const yCoords = points.map(([, y]) => y);
|
|
34
|
+
const minX = Math.min(...xCoords);
|
|
35
|
+
const minY = Math.min(...yCoords);
|
|
36
|
+
const maxX = Math.max(...xCoords);
|
|
37
|
+
const maxY = Math.max(...yCoords);
|
|
38
|
+
return [maxX - minX, maxY - minY];
|
|
39
|
+
}
|
|
40
|
+
function getWindingOrder(points) {
|
|
41
|
+
return points.reduce((acc, [x1, y1], idx, arr) => {
|
|
42
|
+
const p2 = arr[idx + 1];
|
|
43
|
+
const x2 = p2 ? p2[0] : 0;
|
|
44
|
+
const y2 = p2 ? p2[1] : 0;
|
|
45
|
+
return (x2 - x1) * (y2 + y1) + acc;
|
|
46
|
+
}, 0) > 0 ? "clockwise" : "counterclockwise";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/attributes.ts
|
|
51
|
+
function hexWithAlpha(color, alpha) {
|
|
52
|
+
return chroma(color).alpha(alpha).css();
|
|
53
|
+
}
|
|
54
|
+
function has(el, attr) {
|
|
55
|
+
return el.hasAttribute(attr);
|
|
56
|
+
}
|
|
57
|
+
function get(el, attr, backup) {
|
|
58
|
+
return el.getAttribute(attr) || backup || "";
|
|
59
|
+
}
|
|
60
|
+
function getNum(el, attr, backup) {
|
|
61
|
+
const numVal = Number(get(el, attr));
|
|
62
|
+
return Number.isNaN(numVal) ? backup || 0 : numVal;
|
|
63
|
+
}
|
|
64
|
+
const attrHandlers = {
|
|
65
|
+
stroke: ({ el, exVals }) => {
|
|
66
|
+
const strokeColor = get(el, "stroke");
|
|
67
|
+
exVals.strokeColor = has(el, "stroke-opacity") ? hexWithAlpha(strokeColor, getNum(el, "stroke-opacity")) : strokeColor;
|
|
68
|
+
},
|
|
69
|
+
"stroke-opacity": ({ el, exVals }) => {
|
|
70
|
+
exVals.strokeColor = hexWithAlpha(get(el, "stroke", "#000000"), getNum(el, "stroke-opacity"));
|
|
71
|
+
},
|
|
72
|
+
"stroke-width": ({ el, exVals }) => {
|
|
73
|
+
exVals.strokeWidth = getNum(el, "stroke-width");
|
|
74
|
+
},
|
|
75
|
+
fill: ({ el, exVals }) => {
|
|
76
|
+
const fill = get(el, `fill`);
|
|
77
|
+
exVals.backgroundColor = fill === "none" ? "#00000000" : fill;
|
|
78
|
+
},
|
|
79
|
+
"fill-opacity": ({ el, exVals }) => {
|
|
80
|
+
exVals.backgroundColor = hexWithAlpha(get(el, "fill", "#000000"), getNum(el, "fill-opacity"));
|
|
81
|
+
},
|
|
82
|
+
opacity: ({ el, exVals }) => {
|
|
83
|
+
exVals.opacity = getNum(el, "opacity", 100);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
function presAttrsToElementValues(el) {
|
|
87
|
+
return [...el.attributes].reduce((exVals, attr) => {
|
|
88
|
+
const name = attr.name;
|
|
89
|
+
if (Object.keys(attrHandlers).includes(name)) attrHandlers[name]({
|
|
90
|
+
el,
|
|
91
|
+
exVals
|
|
92
|
+
});
|
|
93
|
+
return exVals;
|
|
94
|
+
}, {});
|
|
95
|
+
}
|
|
96
|
+
function filterAttrsToElementValues(el) {
|
|
97
|
+
const filterVals = {};
|
|
98
|
+
if (has(el, "x")) filterVals.x = getNum(el, "x");
|
|
99
|
+
if (has(el, "y")) filterVals.y = getNum(el, "y");
|
|
100
|
+
if (has(el, "width")) filterVals.width = getNum(el, "width");
|
|
101
|
+
if (has(el, "height")) filterVals.height = getNum(el, "height");
|
|
102
|
+
return filterVals;
|
|
103
|
+
}
|
|
104
|
+
function pointsAttrToPoints(el) {
|
|
105
|
+
let points = [];
|
|
106
|
+
if (has(el, "points")) points = get(el, "points").split(" ").map((p) => p.split(",").map(parseFloat));
|
|
107
|
+
return points;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/elements/Group.ts
|
|
112
|
+
function getGroupAttrs(groups) {
|
|
113
|
+
return groups.reduce((acc, { element }) => {
|
|
114
|
+
const elVals = presAttrsToElementValues(element);
|
|
115
|
+
return {
|
|
116
|
+
...acc,
|
|
117
|
+
...elVals
|
|
118
|
+
};
|
|
119
|
+
}, {});
|
|
120
|
+
}
|
|
121
|
+
var Group = class {
|
|
122
|
+
id = randomId();
|
|
123
|
+
element;
|
|
124
|
+
constructor(element) {
|
|
125
|
+
this.element = element;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
var Group_default = Group;
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/dom.ts
|
|
132
|
+
/**
|
|
133
|
+
* DOM API provider using linkedom
|
|
134
|
+
* Works in browser, Node.js, and serverless/edge environments
|
|
135
|
+
*/
|
|
136
|
+
const dom = parseHTML("<!DOCTYPE html>");
|
|
137
|
+
const DOMParser$1 = new DOMParser();
|
|
138
|
+
const document = dom.document;
|
|
139
|
+
const NodeFilter = {
|
|
140
|
+
SHOW_ALL: 4294967295,
|
|
141
|
+
SHOW_ELEMENT: 1,
|
|
142
|
+
SHOW_ATTRIBUTE: 2,
|
|
143
|
+
SHOW_TEXT: 4,
|
|
144
|
+
SHOW_CDATA_SECTION: 8,
|
|
145
|
+
SHOW_ENTITY_REFERENCE: 16,
|
|
146
|
+
SHOW_ENTITY: 32,
|
|
147
|
+
SHOW_PROCESSING_INSTRUCTION: 64,
|
|
148
|
+
SHOW_COMMENT: 128,
|
|
149
|
+
SHOW_DOCUMENT: 256,
|
|
150
|
+
SHOW_DOCUMENT_TYPE: 512,
|
|
151
|
+
SHOW_DOCUMENT_FRAGMENT: 1024,
|
|
152
|
+
SHOW_NOTATION: 2048,
|
|
153
|
+
FILTER_ACCEPT: 1,
|
|
154
|
+
FILTER_REJECT: 2,
|
|
155
|
+
FILTER_SKIP: 3
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/elements/ExcalidrawElement.ts
|
|
160
|
+
function createExElement() {
|
|
161
|
+
return {
|
|
162
|
+
id: randomId(),
|
|
163
|
+
x: 0,
|
|
164
|
+
y: 0,
|
|
165
|
+
strokeColor: "#000000",
|
|
166
|
+
backgroundColor: "#000000",
|
|
167
|
+
fillStyle: "solid",
|
|
168
|
+
strokeWidth: 1,
|
|
169
|
+
strokeStyle: "solid",
|
|
170
|
+
strokeSharpness: "sharp",
|
|
171
|
+
roughness: 0,
|
|
172
|
+
opacity: 100,
|
|
173
|
+
width: 0,
|
|
174
|
+
height: 0,
|
|
175
|
+
angle: 0,
|
|
176
|
+
seed: randomInteger(),
|
|
177
|
+
version: 0,
|
|
178
|
+
versionNonce: 0,
|
|
179
|
+
isDeleted: false,
|
|
180
|
+
groupIds: [],
|
|
181
|
+
boundElementIds: null
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function createExRect() {
|
|
185
|
+
return {
|
|
186
|
+
...createExElement(),
|
|
187
|
+
type: "rectangle"
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function createExLine() {
|
|
191
|
+
return {
|
|
192
|
+
...createExElement(),
|
|
193
|
+
type: "line",
|
|
194
|
+
points: []
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function createExEllipse() {
|
|
198
|
+
return {
|
|
199
|
+
...createExElement(),
|
|
200
|
+
type: "ellipse"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function createExDraw() {
|
|
204
|
+
return {
|
|
205
|
+
...createExElement(),
|
|
206
|
+
type: "draw",
|
|
207
|
+
points: []
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/transform.ts
|
|
213
|
+
const transformFunctionsArr = Object.keys({
|
|
214
|
+
matrix: "matrix",
|
|
215
|
+
matrix3d: "matrix3d",
|
|
216
|
+
perspective: "perspective",
|
|
217
|
+
rotate: "rotate",
|
|
218
|
+
rotate3d: "rotate3d",
|
|
219
|
+
rotateX: "rotateX",
|
|
220
|
+
rotateY: "rotateY",
|
|
221
|
+
rotateZ: "rotateZ",
|
|
222
|
+
scale: "scale",
|
|
223
|
+
scale3d: "scale3d",
|
|
224
|
+
scaleX: "scaleX",
|
|
225
|
+
scaleY: "scaleY",
|
|
226
|
+
scaleZ: "scaleZ",
|
|
227
|
+
skew: "skew",
|
|
228
|
+
skewX: "skewX",
|
|
229
|
+
skewY: "skewY",
|
|
230
|
+
translate: "translate",
|
|
231
|
+
translate3d: "translate3d",
|
|
232
|
+
translateX: "translateX",
|
|
233
|
+
translateY: "translateY",
|
|
234
|
+
translateZ: "translateZ"
|
|
235
|
+
});
|
|
236
|
+
const defaultUnits = {
|
|
237
|
+
matrix: "",
|
|
238
|
+
matrix3d: "",
|
|
239
|
+
perspective: "perspective",
|
|
240
|
+
rotate: "deg",
|
|
241
|
+
rotate3d: "deg",
|
|
242
|
+
rotateX: "deg",
|
|
243
|
+
rotateY: "deg",
|
|
244
|
+
rotateZ: "deg",
|
|
245
|
+
scale: "",
|
|
246
|
+
scale3d: "",
|
|
247
|
+
scaleX: "",
|
|
248
|
+
scaleY: "",
|
|
249
|
+
scaleZ: "",
|
|
250
|
+
skew: "skew",
|
|
251
|
+
skewX: "deg",
|
|
252
|
+
skewY: "deg",
|
|
253
|
+
translate: "px",
|
|
254
|
+
translate3d: "px",
|
|
255
|
+
translateX: "px",
|
|
256
|
+
translateY: "px",
|
|
257
|
+
translateZ: "px"
|
|
258
|
+
};
|
|
259
|
+
const svgTransformToCSSTransform = (svgTransformStr) => {
|
|
260
|
+
const tFuncs = svgTransformStr.match(/(\w+)\(([^)]*)\)/g);
|
|
261
|
+
if (!tFuncs) return "";
|
|
262
|
+
return tFuncs.map((tFuncStr) => {
|
|
263
|
+
const type = tFuncStr.split("(")[0];
|
|
264
|
+
if (!type) throw new Error("Unable to find transform name");
|
|
265
|
+
if (!transformFunctionsArr.includes(type)) throw new Error(`transform function name "${type}" is not valid`);
|
|
266
|
+
const tFuncParts = tFuncStr.match(/([-+]?[0-9]*\.?[0-9]+)([a-z])*/g);
|
|
267
|
+
if (!tFuncParts) return {
|
|
268
|
+
type,
|
|
269
|
+
values: []
|
|
270
|
+
};
|
|
271
|
+
let values = tFuncParts.map((a) => {
|
|
272
|
+
const [value, unit] = a.matchAll(/([-+]?[0-9]*\.?[0-9]+)|([a-z])*/g);
|
|
273
|
+
return {
|
|
274
|
+
unit: unit[0] || defaultUnits[type],
|
|
275
|
+
value: value[0]
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
if (values && type === "rotate" && values?.length > 1) values = [values[0]];
|
|
279
|
+
return {
|
|
280
|
+
type,
|
|
281
|
+
values
|
|
282
|
+
};
|
|
283
|
+
}).map(({ type, values }) => {
|
|
284
|
+
return `${type}(${values.map(({ unit, value }) => `${value}${unit}`).join(", ")})`;
|
|
285
|
+
}).join(" ");
|
|
286
|
+
};
|
|
287
|
+
function getElementMatrix(el) {
|
|
288
|
+
if (el.hasAttribute("transform")) {
|
|
289
|
+
const elMat = new DOMMatrix(svgTransformToCSSTransform(el.getAttribute("transform") || ""));
|
|
290
|
+
return mat4.multiply(mat4.create(), mat4.create(), elMat.toFloat32Array());
|
|
291
|
+
}
|
|
292
|
+
return mat4.create();
|
|
293
|
+
}
|
|
294
|
+
function getTransformMatrix(el, groups) {
|
|
295
|
+
return groups.map(({ element }) => getElementMatrix(element)).concat([getElementMatrix(el)]).reduce((acc, mat) => mat4.multiply(acc, acc, mat), mat4.create());
|
|
296
|
+
}
|
|
297
|
+
function transformPoints(points, transform) {
|
|
298
|
+
return points.map(([x, y]) => {
|
|
299
|
+
const [newX, newY] = vec3.transformMat4(vec3.create(), vec3.fromValues(x, y, 1), transform);
|
|
300
|
+
return [newX, newY];
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
//#endregion
|
|
305
|
+
//#region src/walker.ts
|
|
306
|
+
const SUPPORTED_TAGS = [
|
|
307
|
+
"svg",
|
|
308
|
+
"path",
|
|
309
|
+
"g",
|
|
310
|
+
"use",
|
|
311
|
+
"circle",
|
|
312
|
+
"ellipse",
|
|
313
|
+
"rect",
|
|
314
|
+
"polyline",
|
|
315
|
+
"polygon"
|
|
316
|
+
];
|
|
317
|
+
const nodeValidator = (node) => {
|
|
318
|
+
if (node.nodeType !== 1) return NodeFilter.FILTER_REJECT;
|
|
319
|
+
const element = node;
|
|
320
|
+
if (SUPPORTED_TAGS.includes(element.tagName.toLowerCase())) return NodeFilter.FILTER_ACCEPT;
|
|
321
|
+
return NodeFilter.FILTER_REJECT;
|
|
322
|
+
};
|
|
323
|
+
function createTreeWalker(dom) {
|
|
324
|
+
return (dom.ownerDocument || dom).createTreeWalker(dom, NodeFilter.SHOW_ELEMENT, { acceptNode: nodeValidator });
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Helper to simulate TreeWalker.nextSibling() for linkedom compatibility
|
|
328
|
+
* Linkedom's TreeWalker only implements nextNode(), not nextSibling()
|
|
329
|
+
*/
|
|
330
|
+
function getNextSibling(tw, currentNode) {
|
|
331
|
+
const startNode = currentNode;
|
|
332
|
+
let node = tw.nextNode();
|
|
333
|
+
while (node) {
|
|
334
|
+
let parent = node.parentNode;
|
|
335
|
+
let isDescendant = false;
|
|
336
|
+
while (parent) {
|
|
337
|
+
if (parent === startNode) {
|
|
338
|
+
isDescendant = true;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
parent = parent.parentNode;
|
|
342
|
+
}
|
|
343
|
+
if (!isDescendant) return node;
|
|
344
|
+
node = tw.nextNode();
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const presAttrs = (el, groups) => {
|
|
349
|
+
return {
|
|
350
|
+
...getGroupAttrs(groups),
|
|
351
|
+
...presAttrsToElementValues(el),
|
|
352
|
+
...filterAttrsToElementValues(el)
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
const skippedUseAttrs = ["id"];
|
|
356
|
+
const allwaysPassedUseAttrs = [
|
|
357
|
+
"x",
|
|
358
|
+
"y",
|
|
359
|
+
"width",
|
|
360
|
+
"height",
|
|
361
|
+
"href",
|
|
362
|
+
"xlink:href"
|
|
363
|
+
];
|
|
364
|
+
const getDefElWithCorrectAttrs = (defEl, useEl) => {
|
|
365
|
+
return [...useEl.attributes].reduce((el, attr) => {
|
|
366
|
+
if (skippedUseAttrs.includes(attr.value)) return el;
|
|
367
|
+
if (!defEl.hasAttribute(attr.name) || allwaysPassedUseAttrs.includes(attr.name)) el.setAttribute(attr.name, useEl.getAttribute(attr.name) || "");
|
|
368
|
+
return el;
|
|
369
|
+
}, defEl.cloneNode());
|
|
370
|
+
};
|
|
371
|
+
const walkers = {
|
|
372
|
+
svg: (args) => {
|
|
373
|
+
walk(args, args.tw.nextNode());
|
|
374
|
+
},
|
|
375
|
+
g: (args) => {
|
|
376
|
+
const groupNode = args.tw.currentNode;
|
|
377
|
+
const nextArgs = {
|
|
378
|
+
...args,
|
|
379
|
+
tw: createTreeWalker(groupNode),
|
|
380
|
+
groups: [...args.groups, new Group_default(groupNode)]
|
|
381
|
+
};
|
|
382
|
+
walk(nextArgs, nextArgs.tw.nextNode());
|
|
383
|
+
walk(args, getNextSibling(args.tw, groupNode));
|
|
384
|
+
},
|
|
385
|
+
use: (args) => {
|
|
386
|
+
const { root, tw, scene } = args;
|
|
387
|
+
const useEl = tw.currentNode;
|
|
388
|
+
const id = useEl.getAttribute("href") || useEl.getAttribute("xlink:href");
|
|
389
|
+
if (!id) throw new Error("unable to get id of use element");
|
|
390
|
+
const defEl = root.querySelector(id);
|
|
391
|
+
if (!defEl) throw new Error(`unable to find def element with id: ${id}`);
|
|
392
|
+
const tempScene = new ExcalidrawScene_default();
|
|
393
|
+
const finalEl = getDefElWithCorrectAttrs(defEl, useEl);
|
|
394
|
+
walk({
|
|
395
|
+
...args,
|
|
396
|
+
scene: tempScene,
|
|
397
|
+
tw: createTreeWalker(finalEl)
|
|
398
|
+
}, finalEl);
|
|
399
|
+
const exEl = tempScene.elements.pop();
|
|
400
|
+
if (!exEl) throw new Error("Unable to create ex element");
|
|
401
|
+
scene.elements.push(exEl);
|
|
402
|
+
walk(args, args.tw.nextNode());
|
|
403
|
+
},
|
|
404
|
+
circle: (args) => {
|
|
405
|
+
const { tw, scene, groups } = args;
|
|
406
|
+
const el = tw.currentNode;
|
|
407
|
+
const r = getNum(el, "r", 0);
|
|
408
|
+
const d = r * 2;
|
|
409
|
+
const x = getNum(el, "x", 0) + getNum(el, "cx", 0) - r;
|
|
410
|
+
const y = getNum(el, "y", 0) + getNum(el, "cy", 0) - r;
|
|
411
|
+
const mat = getTransformMatrix(el, groups);
|
|
412
|
+
const m = mat4.fromValues(d, 0, 0, 0, 0, d, 0, 0, 0, 0, 1, 0, x, y, 0, 1);
|
|
413
|
+
const result = mat4.multiply(mat4.create(), mat, m);
|
|
414
|
+
const circle = {
|
|
415
|
+
...createExEllipse(),
|
|
416
|
+
...presAttrs(el, groups),
|
|
417
|
+
x: result[12],
|
|
418
|
+
y: result[13],
|
|
419
|
+
width: result[0],
|
|
420
|
+
height: result[5],
|
|
421
|
+
groupIds: groups.map((g) => g.id)
|
|
422
|
+
};
|
|
423
|
+
scene.elements.push(circle);
|
|
424
|
+
walk(args, tw.nextNode());
|
|
425
|
+
},
|
|
426
|
+
ellipse: (args) => {
|
|
427
|
+
const { tw, scene, groups } = args;
|
|
428
|
+
const el = tw.currentNode;
|
|
429
|
+
const rx = getNum(el, "rx", 0);
|
|
430
|
+
const ry = getNum(el, "ry", 0);
|
|
431
|
+
const cx = getNum(el, "cx", 0);
|
|
432
|
+
const cy = getNum(el, "cy", 0);
|
|
433
|
+
const x = getNum(el, "x", 0) + cx - rx;
|
|
434
|
+
const y = getNum(el, "y", 0) + cy - ry;
|
|
435
|
+
const w = rx * 2;
|
|
436
|
+
const h = ry * 2;
|
|
437
|
+
const mat = getTransformMatrix(el, groups);
|
|
438
|
+
const m = mat4.fromValues(w, 0, 0, 0, 0, h, 0, 0, 0, 0, 1, 0, x, y, 0, 1);
|
|
439
|
+
const result = mat4.multiply(mat4.create(), mat, m);
|
|
440
|
+
const ellipse = {
|
|
441
|
+
...createExEllipse(),
|
|
442
|
+
...presAttrs(el, groups),
|
|
443
|
+
x: result[12],
|
|
444
|
+
y: result[13],
|
|
445
|
+
width: result[0],
|
|
446
|
+
height: result[5],
|
|
447
|
+
groupIds: groups.map((g) => g.id)
|
|
448
|
+
};
|
|
449
|
+
scene.elements.push(ellipse);
|
|
450
|
+
walk(args, tw.nextNode());
|
|
451
|
+
},
|
|
452
|
+
line: (args) => {
|
|
453
|
+
walk(args, args.tw.nextNode());
|
|
454
|
+
},
|
|
455
|
+
polygon: (args) => {
|
|
456
|
+
const { tw, scene, groups } = args;
|
|
457
|
+
const el = tw.currentNode;
|
|
458
|
+
const transformedPoints = transformPoints(pointsAttrToPoints(el), getTransformMatrix(el, groups));
|
|
459
|
+
const x = transformedPoints[0][0];
|
|
460
|
+
const y = transformedPoints[0][1];
|
|
461
|
+
const relativePoints = transformedPoints.map(([_x, _y]) => [_x - x, _y - y]);
|
|
462
|
+
const [width, height] = dimensionsFromPoints(relativePoints);
|
|
463
|
+
const line = {
|
|
464
|
+
...createExLine(),
|
|
465
|
+
...getGroupAttrs(groups),
|
|
466
|
+
...presAttrsToElementValues(el),
|
|
467
|
+
points: relativePoints.concat([[0, 0]]),
|
|
468
|
+
x,
|
|
469
|
+
y,
|
|
470
|
+
width,
|
|
471
|
+
height
|
|
472
|
+
};
|
|
473
|
+
scene.elements.push(line);
|
|
474
|
+
walk(args, args.tw.nextNode());
|
|
475
|
+
},
|
|
476
|
+
polyline: (args) => {
|
|
477
|
+
const { tw, scene, groups } = args;
|
|
478
|
+
const el = tw.currentNode;
|
|
479
|
+
const mat = getTransformMatrix(el, groups);
|
|
480
|
+
const transformedPoints = transformPoints(pointsAttrToPoints(el), mat);
|
|
481
|
+
const x = transformedPoints[0][0];
|
|
482
|
+
const y = transformedPoints[0][1];
|
|
483
|
+
const relativePoints = transformedPoints.map(([_x, _y]) => [_x - x, _y - y]);
|
|
484
|
+
const [width, height] = dimensionsFromPoints(relativePoints);
|
|
485
|
+
const hasFill = has(el, "fill");
|
|
486
|
+
const fill = get(el, "fill");
|
|
487
|
+
const shouldFill = !hasFill || hasFill && fill !== "none";
|
|
488
|
+
const line = {
|
|
489
|
+
...createExLine(),
|
|
490
|
+
...getGroupAttrs(groups),
|
|
491
|
+
...presAttrsToElementValues(el),
|
|
492
|
+
points: relativePoints.concat(shouldFill ? [[0, 0]] : []),
|
|
493
|
+
x,
|
|
494
|
+
y,
|
|
495
|
+
width,
|
|
496
|
+
height
|
|
497
|
+
};
|
|
498
|
+
scene.elements.push(line);
|
|
499
|
+
walk(args, args.tw.nextNode());
|
|
500
|
+
},
|
|
501
|
+
rect: (args) => {
|
|
502
|
+
const { tw, scene, groups } = args;
|
|
503
|
+
const el = tw.currentNode;
|
|
504
|
+
const x = getNum(el, "x", 0);
|
|
505
|
+
const y = getNum(el, "y", 0);
|
|
506
|
+
const w = getNum(el, "width", 0);
|
|
507
|
+
const h = getNum(el, "height", 0);
|
|
508
|
+
const mat = getTransformMatrix(el, groups);
|
|
509
|
+
const m = mat4.fromValues(w, 0, 0, 0, 0, h, 0, 0, 0, 0, 1, 0, x, y, 0, 1);
|
|
510
|
+
const result = mat4.multiply(mat4.create(), mat, m);
|
|
511
|
+
const isRound = el.hasAttribute("rx") || el.hasAttribute("ry");
|
|
512
|
+
const rect = {
|
|
513
|
+
...createExRect(),
|
|
514
|
+
...presAttrs(el, groups),
|
|
515
|
+
x: result[12],
|
|
516
|
+
y: result[13],
|
|
517
|
+
width: result[0],
|
|
518
|
+
height: result[5],
|
|
519
|
+
strokeSharpness: isRound ? "round" : "sharp"
|
|
520
|
+
};
|
|
521
|
+
scene.elements.push(rect);
|
|
522
|
+
walk(args, args.tw.nextNode());
|
|
523
|
+
},
|
|
524
|
+
path: (args) => {
|
|
525
|
+
const { tw, scene, groups } = args;
|
|
526
|
+
const el = tw.currentNode;
|
|
527
|
+
const mat = getTransformMatrix(el, groups);
|
|
528
|
+
const points = pointsOnPath(get(el, "d"));
|
|
529
|
+
const fillColor = get(el, "fill", "black");
|
|
530
|
+
const fillRule = get(el, "fill-rule", "nonzero");
|
|
531
|
+
let elements = [];
|
|
532
|
+
let localGroup = randomId();
|
|
533
|
+
switch (fillRule) {
|
|
534
|
+
case "nonzero":
|
|
535
|
+
let initialWindingOrder = "clockwise";
|
|
536
|
+
elements = points.map((pointArr, idx) => {
|
|
537
|
+
const tPoints = transformPoints(pointArr, mat4.clone(mat));
|
|
538
|
+
const x = tPoints[0][0];
|
|
539
|
+
const y = tPoints[0][1];
|
|
540
|
+
const [width, height] = dimensionsFromPoints(tPoints);
|
|
541
|
+
const relativePoints = tPoints.map(([_x, _y]) => [_x - x, _y - y]);
|
|
542
|
+
const windingOrder = getWindingOrder(relativePoints);
|
|
543
|
+
if (idx === 0) {
|
|
544
|
+
initialWindingOrder = windingOrder;
|
|
545
|
+
localGroup = randomId();
|
|
546
|
+
}
|
|
547
|
+
let backgroundColor = fillColor;
|
|
548
|
+
if (initialWindingOrder !== windingOrder) backgroundColor = "#FFFFFF";
|
|
549
|
+
return {
|
|
550
|
+
...createExDraw(),
|
|
551
|
+
strokeWidth: 0,
|
|
552
|
+
strokeColor: "#00000000",
|
|
553
|
+
...presAttrs(el, groups),
|
|
554
|
+
points: relativePoints,
|
|
555
|
+
backgroundColor,
|
|
556
|
+
width,
|
|
557
|
+
height,
|
|
558
|
+
x: x + getNum(el, "x", 0),
|
|
559
|
+
y: y + getNum(el, "y", 0),
|
|
560
|
+
groupIds: [localGroup]
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
break;
|
|
564
|
+
case "evenodd":
|
|
565
|
+
elements = points.map((pointArr, idx) => {
|
|
566
|
+
const tPoints = transformPoints(pointArr, mat4.clone(mat));
|
|
567
|
+
const x = tPoints[0][0];
|
|
568
|
+
const y = tPoints[0][1];
|
|
569
|
+
const [width, height] = dimensionsFromPoints(tPoints);
|
|
570
|
+
const relativePoints = tPoints.map(([_x, _y]) => [_x - x, _y - y]);
|
|
571
|
+
if (idx === 0) localGroup = randomId();
|
|
572
|
+
return {
|
|
573
|
+
...createExDraw(),
|
|
574
|
+
...presAttrs(el, groups),
|
|
575
|
+
points: relativePoints,
|
|
576
|
+
width,
|
|
577
|
+
height,
|
|
578
|
+
x: x + getNum(el, "x", 0),
|
|
579
|
+
y: y + getNum(el, "y", 0)
|
|
580
|
+
};
|
|
581
|
+
});
|
|
582
|
+
break;
|
|
583
|
+
default:
|
|
584
|
+
}
|
|
585
|
+
scene.elements = scene.elements.concat(elements);
|
|
586
|
+
walk(args, tw.nextNode());
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
function walk(args, nextNode) {
|
|
590
|
+
if (!nextNode) return;
|
|
591
|
+
const nodeName = nextNode.nodeName.toLowerCase();
|
|
592
|
+
if (walkers[nodeName]) walkers[nodeName](args);
|
|
593
|
+
else {
|
|
594
|
+
if (args.skippedElements && nextNode.nodeType === 1) {
|
|
595
|
+
const element = nextNode;
|
|
596
|
+
args.skippedElements.add(element.tagName.toLowerCase());
|
|
597
|
+
}
|
|
598
|
+
walk(args, args.tw.nextNode());
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
//#endregion
|
|
603
|
+
//#region src/parser.ts
|
|
604
|
+
const convert = (svgString) => {
|
|
605
|
+
const warnings = [];
|
|
606
|
+
if (!svgString || svgString.trim().length === 0) {
|
|
607
|
+
const error = "SVG string is empty or invalid";
|
|
608
|
+
console.error(error);
|
|
609
|
+
return {
|
|
610
|
+
hasErrors: true,
|
|
611
|
+
errors: [error],
|
|
612
|
+
content: null,
|
|
613
|
+
warnings
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
const svgDOM = DOMParser$1.parseFromString(svgString, "image/svg+xml");
|
|
617
|
+
const errorsElements = svgDOM.querySelectorAll("parsererror");
|
|
618
|
+
const hasErrors = errorsElements.length > 0;
|
|
619
|
+
let content = null;
|
|
620
|
+
if (hasErrors) console.error("There were errors while parsing the given SVG: ", [...errorsElements].map((el) => el.innerHTML));
|
|
621
|
+
else {
|
|
622
|
+
const tw = createTreeWalker(svgDOM);
|
|
623
|
+
const scene = new ExcalidrawScene_default();
|
|
624
|
+
const groups = [];
|
|
625
|
+
const skippedElements = /* @__PURE__ */ new Set();
|
|
626
|
+
walk({
|
|
627
|
+
tw,
|
|
628
|
+
scene,
|
|
629
|
+
groups,
|
|
630
|
+
root: svgDOM,
|
|
631
|
+
skippedElements
|
|
632
|
+
}, tw.nextNode());
|
|
633
|
+
content = scene.toExJSON();
|
|
634
|
+
const elementCount = content?.elements?.length || 0;
|
|
635
|
+
if (elementCount === 0) {
|
|
636
|
+
const warning = "Conversion produced 0 elements - the SVG may contain only unsupported elements or be empty";
|
|
637
|
+
console.warn(warning);
|
|
638
|
+
warnings.push(warning);
|
|
639
|
+
}
|
|
640
|
+
if (skippedElements.size > 0) {
|
|
641
|
+
const warning = `Skipped unsupported elements: ${Array.from(skippedElements).join(", ")}`;
|
|
642
|
+
console.warn(warning);
|
|
643
|
+
warnings.push(warning);
|
|
644
|
+
}
|
|
645
|
+
if (elementCount > 0) console.log(`✓ Successfully converted ${elementCount} SVG elements to Excalidraw format`);
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
hasErrors,
|
|
649
|
+
errors: hasErrors ? errorsElements : null,
|
|
650
|
+
content,
|
|
651
|
+
warnings
|
|
652
|
+
};
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
//#endregion
|
|
656
|
+
export { convert };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "headless-svg-to-excalidraw",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Convert SVG to Excalidraw's file format, no browser required.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.mts",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"package.json"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsdown",
|
|
23
|
+
"clean": "rm -rf dist",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/chroma-js": "^2.1.3",
|
|
29
|
+
"tsdown": "^0.20.1",
|
|
30
|
+
"typescript": "^5.9.3"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"typescript": "^4.5.5"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"chroma-js": "^2.1.2",
|
|
37
|
+
"gl-matrix": "^3.3.0",
|
|
38
|
+
"linkedom": "^0.18.12",
|
|
39
|
+
"nanoid": "5.0.9",
|
|
40
|
+
"path-data-parser": "^0.1.0",
|
|
41
|
+
"points-on-curve": "^0.2.0",
|
|
42
|
+
"points-on-path": "^0.2.1",
|
|
43
|
+
"roughjs": "^4.4.1"
|
|
44
|
+
}
|
|
45
|
+
}
|