@svgedit/svgcanvas 7.2.7 → 7.4.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/common/browser.js +104 -37
- package/common/logger.js +151 -0
- package/common/util.js +96 -155
- package/core/blur-event.js +106 -42
- package/core/clear.js +13 -3
- package/core/coords.js +214 -90
- package/core/copy-elem.js +27 -13
- package/core/dataStorage.js +84 -21
- package/core/draw.js +80 -40
- package/core/elem-get-set.js +161 -77
- package/core/event.js +143 -28
- package/core/history.js +50 -30
- package/core/historyrecording.js +4 -2
- package/core/json.js +54 -12
- package/core/layer.js +11 -17
- package/core/math.js +102 -23
- package/core/namespaces.js +5 -5
- package/core/paint.js +95 -24
- package/core/paste-elem.js +58 -19
- package/core/path-actions.js +812 -791
- package/core/path-method.js +236 -37
- package/core/path.js +45 -10
- package/core/recalculate.js +410 -15
- package/core/sanitize.js +46 -14
- package/core/select.js +44 -20
- package/core/selected-elem.js +146 -31
- package/core/selection.js +16 -6
- package/core/svg-exec.js +99 -27
- package/core/svgroot.js +1 -1
- package/core/text-actions.js +327 -306
- package/core/undo.js +20 -5
- package/core/units.js +8 -6
- package/core/utilities.js +282 -170
- package/dist/svgcanvas.js +31590 -53383
- package/dist/svgcanvas.js.map +1 -1
- package/package.json +55 -54
- package/publish.md +1 -6
- package/svgcanvas.d.ts +225 -0
- package/svgcanvas.js +9 -9
- package/vite.config.mjs +20 -0
- package/rollup.config.mjs +0 -38
package/core/recalculate.js
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { convertToNum } from './units.js'
|
|
8
|
-
import {
|
|
8
|
+
import { NS } from './namespaces.js'
|
|
9
|
+
import {
|
|
10
|
+
getRotationAngle,
|
|
11
|
+
getBBox,
|
|
12
|
+
getHref,
|
|
13
|
+
getRefElem,
|
|
14
|
+
findDefs
|
|
15
|
+
} from './utilities.js'
|
|
9
16
|
import { BatchCommand, ChangeElementCommand } from './history.js'
|
|
10
17
|
import { remapElement } from './coords.js'
|
|
11
18
|
import {
|
|
@@ -36,20 +43,93 @@ export const init = canvas => {
|
|
|
36
43
|
* @param {string} attr - The clip-path attribute value containing the clipPath's ID
|
|
37
44
|
* @param {number} tx - The translation's x value
|
|
38
45
|
* @param {number} ty - The translation's y value
|
|
39
|
-
* @
|
|
46
|
+
* @param {Element} elem - The element referencing the clipPath
|
|
47
|
+
* @returns {string|undefined} The clip-path attribute used after updates.
|
|
40
48
|
*/
|
|
41
|
-
export const updateClipPath = (attr, tx, ty) => {
|
|
49
|
+
export const updateClipPath = (attr, tx, ty, elem) => {
|
|
42
50
|
const clipPath = getRefElem(attr)
|
|
43
|
-
if (!clipPath) return
|
|
44
|
-
|
|
51
|
+
if (!clipPath) return undefined
|
|
52
|
+
if (elem && clipPath.id) {
|
|
53
|
+
const svgContent = svgCanvas.getSvgContent?.()
|
|
54
|
+
if (svgContent) {
|
|
55
|
+
const refSelector = `[clip-path="url(#${clipPath.id})"]`
|
|
56
|
+
const users = svgContent.querySelectorAll(refSelector)
|
|
57
|
+
if (users.length > 1) {
|
|
58
|
+
const newClipPath = clipPath.cloneNode(true)
|
|
59
|
+
newClipPath.id = svgCanvas.getNextId()
|
|
60
|
+
findDefs().append(newClipPath)
|
|
61
|
+
elem.setAttribute('clip-path', `url(#${newClipPath.id})`)
|
|
62
|
+
return updateClipPath(`url(#${newClipPath.id})`, tx, ty)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const path = clipPath.firstElementChild
|
|
67
|
+
if (!path) return attr
|
|
45
68
|
const cpXform = getTransformList(path)
|
|
69
|
+
if (!cpXform) {
|
|
70
|
+
const tag = (path.tagName || '').toLowerCase()
|
|
71
|
+
if (tag === 'rect') {
|
|
72
|
+
const x = convertToNum('x', path.getAttribute('x') || 0) + tx
|
|
73
|
+
const y = convertToNum('y', path.getAttribute('y') || 0) + ty
|
|
74
|
+
path.setAttribute('x', x)
|
|
75
|
+
path.setAttribute('y', y)
|
|
76
|
+
} else if (tag === 'circle' || tag === 'ellipse') {
|
|
77
|
+
const cx = convertToNum('cx', path.getAttribute('cx') || 0) + tx
|
|
78
|
+
const cy = convertToNum('cy', path.getAttribute('cy') || 0) + ty
|
|
79
|
+
path.setAttribute('cx', cx)
|
|
80
|
+
path.setAttribute('cy', cy)
|
|
81
|
+
} else if (tag === 'line') {
|
|
82
|
+
path.setAttribute('x1', convertToNum('x1', path.getAttribute('x1') || 0) + tx)
|
|
83
|
+
path.setAttribute('y1', convertToNum('y1', path.getAttribute('y1') || 0) + ty)
|
|
84
|
+
path.setAttribute('x2', convertToNum('x2', path.getAttribute('x2') || 0) + tx)
|
|
85
|
+
path.setAttribute('y2', convertToNum('y2', path.getAttribute('y2') || 0) + ty)
|
|
86
|
+
} else if (tag === 'polyline' || tag === 'polygon') {
|
|
87
|
+
const points = (path.getAttribute('points') || '').trim()
|
|
88
|
+
if (points) {
|
|
89
|
+
const updated = points.split(/\s+/).map((pair) => {
|
|
90
|
+
const [x, y] = pair.split(',')
|
|
91
|
+
const nx = Number(x) + tx
|
|
92
|
+
const ny = Number(y) + ty
|
|
93
|
+
return `${nx},${ny}`
|
|
94
|
+
})
|
|
95
|
+
path.setAttribute('points', updated.join(' '))
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
path.setAttribute('transform', `translate(${tx},${ty})`)
|
|
99
|
+
}
|
|
100
|
+
return attr
|
|
101
|
+
}
|
|
102
|
+
if (cpXform.numberOfItems) {
|
|
103
|
+
const translate = svgCanvas.getSvgRoot().createSVGMatrix()
|
|
104
|
+
translate.e = tx
|
|
105
|
+
translate.f = ty
|
|
106
|
+
const combined = matrixMultiply(transformListToTransform(cpXform).matrix, translate)
|
|
107
|
+
const merged = svgCanvas.getSvgRoot().createSVGTransform()
|
|
108
|
+
merged.setMatrix(combined)
|
|
109
|
+
cpXform.clear()
|
|
110
|
+
cpXform.appendItem(merged)
|
|
111
|
+
return attr
|
|
112
|
+
}
|
|
113
|
+
const tag = (path.tagName || '').toLowerCase()
|
|
114
|
+
if ((tag === 'polyline' || tag === 'polygon') && !path.points?.numberOfItems) {
|
|
115
|
+
const points = (path.getAttribute('points') || '').trim()
|
|
116
|
+
if (points) {
|
|
117
|
+
const updated = points.split(/\s+/).map((pair) => {
|
|
118
|
+
const [x, y] = pair.split(',')
|
|
119
|
+
const nx = Number(x) + tx
|
|
120
|
+
const ny = Number(y) + ty
|
|
121
|
+
return `${nx},${ny}`
|
|
122
|
+
})
|
|
123
|
+
path.setAttribute('points', updated.join(' '))
|
|
124
|
+
}
|
|
125
|
+
return
|
|
126
|
+
}
|
|
46
127
|
const newTranslate = svgCanvas.getSvgRoot().createSVGTransform()
|
|
47
128
|
newTranslate.setTranslate(tx, ty)
|
|
48
129
|
|
|
49
130
|
cpXform.appendItem(newTranslate)
|
|
50
|
-
|
|
51
|
-
// Update clipPath's dimensions
|
|
52
131
|
recalculateDimensions(path)
|
|
132
|
+
return attr
|
|
53
133
|
}
|
|
54
134
|
|
|
55
135
|
/**
|
|
@@ -60,6 +140,20 @@ export const updateClipPath = (attr, tx, ty) => {
|
|
|
60
140
|
*/
|
|
61
141
|
export const recalculateDimensions = selected => {
|
|
62
142
|
if (!selected) return null
|
|
143
|
+
|
|
144
|
+
// Don't recalculate dimensions for groups - this would push their transforms down to children
|
|
145
|
+
// Groups should maintain their transform attribute on the group element itself
|
|
146
|
+
if (selected.tagName === 'g' || selected.tagName === 'a') {
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
(selected.getAttribute?.('clip-path')) &&
|
|
152
|
+
selected.querySelector?.('[clip-path]')
|
|
153
|
+
) {
|
|
154
|
+
// Keep transforms when clip-paths are present to avoid mutating defs.
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
63
157
|
const svgroot = svgCanvas.getSvgRoot()
|
|
64
158
|
const dataStorage = svgCanvas.getDataStorage()
|
|
65
159
|
const tlist = getTransformList(selected)
|
|
@@ -105,6 +199,11 @@ export const recalculateDimensions = selected => {
|
|
|
105
199
|
return null
|
|
106
200
|
}
|
|
107
201
|
|
|
202
|
+
// Avoid remapping transforms on <use> to preserve referenced positioning/rotation
|
|
203
|
+
if (selected.tagName === 'use') {
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
|
|
108
207
|
// Set up undo command
|
|
109
208
|
const batchCmd = new BatchCommand('Transform')
|
|
110
209
|
|
|
@@ -206,14 +305,310 @@ export const recalculateDimensions = selected => {
|
|
|
206
305
|
|
|
207
306
|
// Handle group elements ('g' or 'a')
|
|
208
307
|
if ((selected.tagName === 'g' && !gsvg) || selected.tagName === 'a') {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
308
|
+
const box = getBBox(selected)
|
|
309
|
+
|
|
310
|
+
oldcenter = { x: box.x + box.width / 2, y: box.y + box.height / 2 }
|
|
311
|
+
newcenter = transformPoint(
|
|
312
|
+
box.x + box.width / 2,
|
|
313
|
+
box.y + box.height / 2,
|
|
314
|
+
transformListToTransform(tlist).matrix
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
const gangle = getRotationAngle(selected)
|
|
318
|
+
if (gangle) {
|
|
319
|
+
const a = gangle * Math.PI / 180
|
|
320
|
+
const s = Math.abs(a) > (1.0e-10) ? Math.sin(a) / (1 - Math.cos(a)) : 2 / a
|
|
321
|
+
for (let i = 0; i < tlist.numberOfItems; ++i) {
|
|
322
|
+
const xform = tlist.getItem(i)
|
|
323
|
+
if (xform.type === SVGTransform.SVG_TRANSFORM_ROTATE) {
|
|
324
|
+
const rm = xform.matrix
|
|
325
|
+
oldcenter.y = (s * rm.e + rm.f) / 2
|
|
326
|
+
oldcenter.x = (rm.e - s * rm.f) / 2
|
|
327
|
+
tlist.removeItem(i)
|
|
328
|
+
break
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const N = tlist.numberOfItems
|
|
334
|
+
let tx = 0
|
|
335
|
+
let ty = 0
|
|
336
|
+
let operation = 0
|
|
337
|
+
|
|
338
|
+
let firstM
|
|
339
|
+
if (N) {
|
|
340
|
+
firstM = tlist.getItem(0).matrix
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let oldStartTransform
|
|
344
|
+
if (
|
|
345
|
+
N >= 3 &&
|
|
346
|
+
tlist.getItem(N - 2).type === SVGTransform.SVG_TRANSFORM_SCALE &&
|
|
347
|
+
tlist.getItem(N - 3).type === SVGTransform.SVG_TRANSFORM_TRANSLATE &&
|
|
348
|
+
tlist.getItem(N - 1).type === SVGTransform.SVG_TRANSFORM_TRANSLATE
|
|
349
|
+
) {
|
|
350
|
+
operation = 3 // scale
|
|
351
|
+
|
|
352
|
+
const tm = tlist.getItem(N - 3).matrix
|
|
353
|
+
const sm = tlist.getItem(N - 2).matrix
|
|
354
|
+
const tmn = tlist.getItem(N - 1).matrix
|
|
355
|
+
|
|
356
|
+
const children = selected.childNodes
|
|
357
|
+
let c = children.length
|
|
358
|
+
while (c--) {
|
|
359
|
+
const child = children.item(c)
|
|
360
|
+
if (child.nodeType !== 1) continue
|
|
361
|
+
|
|
362
|
+
const childTlist = getTransformList(child)
|
|
363
|
+
if (!childTlist) continue
|
|
364
|
+
|
|
365
|
+
const m = transformListToTransform(childTlist).matrix
|
|
366
|
+
|
|
367
|
+
const angle = getRotationAngle(child)
|
|
368
|
+
oldStartTransform = svgCanvas.getStartTransform()
|
|
369
|
+
svgCanvas.setStartTransform(child.getAttribute('transform'))
|
|
370
|
+
|
|
371
|
+
if (angle || hasMatrixTransform(childTlist)) {
|
|
372
|
+
const e2t = svgroot.createSVGTransform()
|
|
373
|
+
e2t.setMatrix(matrixMultiply(tm, sm, tmn, m))
|
|
374
|
+
childTlist.clear()
|
|
375
|
+
childTlist.appendItem(e2t)
|
|
376
|
+
} else {
|
|
377
|
+
const t2n = matrixMultiply(m.inverse(), tmn, m)
|
|
378
|
+
const t2 = svgroot.createSVGMatrix()
|
|
379
|
+
t2.e = -t2n.e
|
|
380
|
+
t2.f = -t2n.f
|
|
381
|
+
|
|
382
|
+
const s2 = matrixMultiply(
|
|
383
|
+
t2.inverse(),
|
|
384
|
+
m.inverse(),
|
|
385
|
+
tm,
|
|
386
|
+
sm,
|
|
387
|
+
tmn,
|
|
388
|
+
m,
|
|
389
|
+
t2n.inverse()
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
const translateOrigin = svgroot.createSVGTransform()
|
|
393
|
+
const scale = svgroot.createSVGTransform()
|
|
394
|
+
const translateBack = svgroot.createSVGTransform()
|
|
395
|
+
translateOrigin.setTranslate(t2n.e, t2n.f)
|
|
396
|
+
scale.setScale(s2.a, s2.d)
|
|
397
|
+
translateBack.setTranslate(t2.e, t2.f)
|
|
398
|
+
childTlist.appendItem(translateBack)
|
|
399
|
+
childTlist.appendItem(scale)
|
|
400
|
+
childTlist.appendItem(translateOrigin)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const recalculatedDimensions = recalculateDimensions(child)
|
|
404
|
+
if (recalculatedDimensions) {
|
|
405
|
+
batchCmd.addSubCommand(recalculatedDimensions)
|
|
406
|
+
}
|
|
407
|
+
svgCanvas.setStartTransform(oldStartTransform)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
tlist.removeItem(N - 1)
|
|
411
|
+
tlist.removeItem(N - 2)
|
|
412
|
+
tlist.removeItem(N - 3)
|
|
413
|
+
} else if (N >= 3 && tlist.getItem(N - 1).type === SVGTransform.SVG_TRANSFORM_MATRIX) {
|
|
414
|
+
operation = 3 // scale (matrix imposition)
|
|
415
|
+
const m = transformListToTransform(tlist).matrix
|
|
416
|
+
const e2t = svgroot.createSVGTransform()
|
|
417
|
+
e2t.setMatrix(m)
|
|
418
|
+
tlist.clear()
|
|
419
|
+
tlist.appendItem(e2t)
|
|
420
|
+
} else if (
|
|
421
|
+
(N === 1 ||
|
|
422
|
+
(N > 1 && tlist.getItem(1).type !== SVGTransform.SVG_TRANSFORM_SCALE)) &&
|
|
423
|
+
tlist.getItem(0).type === SVGTransform.SVG_TRANSFORM_TRANSLATE
|
|
424
|
+
) {
|
|
425
|
+
operation = 2 // translate
|
|
426
|
+
const tM = transformListToTransform(tlist).matrix
|
|
427
|
+
tlist.removeItem(0)
|
|
428
|
+
const mInv = transformListToTransform(tlist).matrix.inverse()
|
|
429
|
+
const m2 = matrixMultiply(mInv, tM)
|
|
430
|
+
|
|
431
|
+
tx = m2.e
|
|
432
|
+
ty = m2.f
|
|
433
|
+
|
|
434
|
+
if (tx !== 0 || ty !== 0) {
|
|
435
|
+
const selectedClipPath = selected.getAttribute?.('clip-path')
|
|
436
|
+
if (selectedClipPath) {
|
|
437
|
+
updateClipPath(selectedClipPath, tx, ty, selected)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const children = selected.childNodes
|
|
441
|
+
let c = children.length
|
|
442
|
+
|
|
443
|
+
const clipPathsDone = []
|
|
444
|
+
while (c--) {
|
|
445
|
+
const child = children.item(c)
|
|
446
|
+
if (child.nodeType !== 1) continue
|
|
447
|
+
|
|
448
|
+
const clipPathAttr = child.getAttribute('clip-path')
|
|
449
|
+
if (clipPathAttr && !clipPathsDone.includes(clipPathAttr)) {
|
|
450
|
+
const updatedAttr = updateClipPath(clipPathAttr, tx, ty, child)
|
|
451
|
+
clipPathsDone.push(updatedAttr || clipPathAttr)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const childTlist = getTransformList(child)
|
|
455
|
+
if (!childTlist) continue
|
|
456
|
+
|
|
457
|
+
oldStartTransform = svgCanvas.getStartTransform()
|
|
458
|
+
svgCanvas.setStartTransform(child.getAttribute('transform'))
|
|
459
|
+
|
|
460
|
+
const newxlate = svgroot.createSVGTransform()
|
|
461
|
+
newxlate.setTranslate(tx, ty)
|
|
462
|
+
if (childTlist.numberOfItems) {
|
|
463
|
+
childTlist.insertItemBefore(newxlate, 0)
|
|
464
|
+
} else {
|
|
465
|
+
childTlist.appendItem(newxlate)
|
|
466
|
+
}
|
|
467
|
+
const recalculatedDimensions = recalculateDimensions(child)
|
|
468
|
+
if (recalculatedDimensions) {
|
|
469
|
+
batchCmd.addSubCommand(recalculatedDimensions)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const uses = selected.getElementsByTagNameNS(NS.SVG, 'use')
|
|
473
|
+
const href = `#${child.id}`
|
|
474
|
+
let u = uses.length
|
|
475
|
+
while (u--) {
|
|
476
|
+
const useElem = uses.item(u)
|
|
477
|
+
if (href === getHref(useElem)) {
|
|
478
|
+
const usexlate = svgroot.createSVGTransform()
|
|
479
|
+
usexlate.setTranslate(-tx, -ty)
|
|
480
|
+
const useTlist = getTransformList(useElem)
|
|
481
|
+
useTlist?.insertItemBefore(usexlate, 0)
|
|
482
|
+
const useRecalc = recalculateDimensions(useElem)
|
|
483
|
+
if (useRecalc) {
|
|
484
|
+
batchCmd.addSubCommand(useRecalc)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
svgCanvas.setStartTransform(oldStartTransform)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} else if (
|
|
493
|
+
N === 1 &&
|
|
494
|
+
tlist.getItem(0).type === SVGTransform.SVG_TRANSFORM_MATRIX &&
|
|
495
|
+
!gangle
|
|
496
|
+
) {
|
|
497
|
+
operation = 1
|
|
498
|
+
const m = tlist.getItem(0).matrix
|
|
499
|
+
const children = selected.childNodes
|
|
500
|
+
let c = children.length
|
|
501
|
+
while (c--) {
|
|
502
|
+
const child = children.item(c)
|
|
503
|
+
if (child.nodeType !== 1) continue
|
|
504
|
+
|
|
505
|
+
const childTlist = getTransformList(child)
|
|
506
|
+
if (!childTlist) continue
|
|
507
|
+
|
|
508
|
+
oldStartTransform = svgCanvas.getStartTransform()
|
|
509
|
+
svgCanvas.setStartTransform(child.getAttribute('transform'))
|
|
510
|
+
|
|
511
|
+
const em = matrixMultiply(m, transformListToTransform(childTlist).matrix)
|
|
512
|
+
const e2m = svgroot.createSVGTransform()
|
|
513
|
+
e2m.setMatrix(em)
|
|
514
|
+
childTlist.clear()
|
|
515
|
+
childTlist.appendItem(e2m)
|
|
516
|
+
|
|
517
|
+
const recalculatedDimensions = recalculateDimensions(child)
|
|
518
|
+
if (recalculatedDimensions) {
|
|
519
|
+
batchCmd.addSubCommand(recalculatedDimensions)
|
|
520
|
+
}
|
|
521
|
+
svgCanvas.setStartTransform(oldStartTransform)
|
|
522
|
+
|
|
523
|
+
const sw = child.getAttribute('stroke-width')
|
|
524
|
+
if (child.getAttribute('stroke') !== 'none' && !Number.isNaN(Number(sw))) {
|
|
525
|
+
const avg = (Math.abs(em.a) + Math.abs(em.d)) / 2
|
|
526
|
+
child.setAttribute('stroke-width', Number(sw) * avg)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
tlist.clear()
|
|
530
|
+
} else {
|
|
531
|
+
if (gangle) {
|
|
532
|
+
const newRot = svgroot.createSVGTransform()
|
|
533
|
+
newRot.setRotate(gangle, newcenter.x, newcenter.y)
|
|
534
|
+
if (tlist.numberOfItems) {
|
|
535
|
+
tlist.insertItemBefore(newRot, 0)
|
|
536
|
+
} else {
|
|
537
|
+
tlist.appendItem(newRot)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (tlist.numberOfItems === 0) {
|
|
541
|
+
selected.removeAttribute('transform')
|
|
542
|
+
}
|
|
543
|
+
return null
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (operation === 2) {
|
|
547
|
+
if (gangle) {
|
|
548
|
+
newcenter = {
|
|
549
|
+
x: oldcenter.x + firstM.e,
|
|
550
|
+
y: oldcenter.y + firstM.f
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const newRot = svgroot.createSVGTransform()
|
|
554
|
+
newRot.setRotate(gangle, newcenter.x, newcenter.y)
|
|
555
|
+
if (tlist.numberOfItems) {
|
|
556
|
+
tlist.insertItemBefore(newRot, 0)
|
|
557
|
+
} else {
|
|
558
|
+
tlist.appendItem(newRot)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
} else if (operation === 3) {
|
|
562
|
+
const m = transformListToTransform(tlist).matrix
|
|
563
|
+
const roldt = svgroot.createSVGTransform()
|
|
564
|
+
roldt.setRotate(gangle, oldcenter.x, oldcenter.y)
|
|
565
|
+
const rold = roldt.matrix
|
|
566
|
+
const rnew = svgroot.createSVGTransform()
|
|
567
|
+
rnew.setRotate(gangle, newcenter.x, newcenter.y)
|
|
568
|
+
const rnewInv = rnew.matrix.inverse()
|
|
569
|
+
const mInv = m.inverse()
|
|
570
|
+
const extrat = matrixMultiply(mInv, rnewInv, rold, m)
|
|
571
|
+
|
|
572
|
+
tx = extrat.e
|
|
573
|
+
ty = extrat.f
|
|
574
|
+
|
|
575
|
+
if (tx !== 0 || ty !== 0) {
|
|
576
|
+
const children = selected.childNodes
|
|
577
|
+
let c = children.length
|
|
578
|
+
while (c--) {
|
|
579
|
+
const child = children.item(c)
|
|
580
|
+
if (child.nodeType !== 1) continue
|
|
581
|
+
|
|
582
|
+
const childTlist = getTransformList(child)
|
|
583
|
+
if (!childTlist) continue
|
|
584
|
+
|
|
585
|
+
oldStartTransform = svgCanvas.getStartTransform()
|
|
586
|
+
svgCanvas.setStartTransform(child.getAttribute('transform'))
|
|
587
|
+
|
|
588
|
+
const newxlate = svgroot.createSVGTransform()
|
|
589
|
+
newxlate.setTranslate(tx, ty)
|
|
590
|
+
if (childTlist.numberOfItems) {
|
|
591
|
+
childTlist.insertItemBefore(newxlate, 0)
|
|
592
|
+
} else {
|
|
593
|
+
childTlist.appendItem(newxlate)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const recalculatedDimensions = recalculateDimensions(child)
|
|
597
|
+
if (recalculatedDimensions) {
|
|
598
|
+
batchCmd.addSubCommand(recalculatedDimensions)
|
|
599
|
+
}
|
|
600
|
+
svgCanvas.setStartTransform(oldStartTransform)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (gangle) {
|
|
605
|
+
if (tlist.numberOfItems) {
|
|
606
|
+
tlist.insertItemBefore(rnew, 0)
|
|
607
|
+
} else {
|
|
608
|
+
tlist.appendItem(rnew)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
217
612
|
} else {
|
|
218
613
|
// Non-group elements
|
|
219
614
|
|
package/core/sanitize.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { getReverseNS, NS } from './namespaces.js'
|
|
10
|
-
import { getHref, setHref, getUrlFromAttr } from './utilities.js'
|
|
10
|
+
import { getHref, getRefElem, setHref, getUrlFromAttr } from './utilities.js'
|
|
11
|
+
import { warn } from '../common/logger.js'
|
|
11
12
|
|
|
12
13
|
const REVERSE_NS = getReverseNS()
|
|
13
14
|
|
|
@@ -39,7 +40,11 @@ const svgWhiteList_ = {
|
|
|
39
40
|
filter: ['color-interpolation-filters', 'filterRes', 'filterUnits', 'height', 'href', 'primitiveUnits', 'requiredFeatures', 'width', 'x', 'xlink:href', 'y'],
|
|
40
41
|
foreignObject: ['font-size', 'height', 'opacity', 'requiredFeatures', 'width', 'x', 'y'],
|
|
41
42
|
g: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'font-family', 'font-size', 'font-style', 'font-weight', 'text-anchor'],
|
|
42
|
-
image: [
|
|
43
|
+
image: [
|
|
44
|
+
'clip-path', 'clip-rule', 'filter', 'height', 'mask', 'opacity',
|
|
45
|
+
'preserveAspectRatio', 'requiredFeatures', 'systemLanguage', 'viewBox',
|
|
46
|
+
'width', 'x', 'href', 'xlink:href', 'xlink:title', 'y'
|
|
47
|
+
],
|
|
43
48
|
line: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'x1', 'x2', 'y1', 'y2'],
|
|
44
49
|
linearGradient: ['gradientTransform', 'gradientUnits', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'x1', 'x2', 'href', 'xlink:href', 'y1', 'y2'],
|
|
45
50
|
marker: ['markerHeight', 'markerUnits', 'markerWidth', 'orient', 'preserveAspectRatio', 'refX', 'refY', 'se_type', 'systemLanguage', 'viewBox'],
|
|
@@ -126,22 +131,24 @@ const svgWhiteList_ = {
|
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
// add generic attributes to all elements of the whitelist
|
|
129
|
-
|
|
134
|
+
for (const [element, attrs] of Object.entries(svgWhiteList_)) {
|
|
135
|
+
svgWhiteList_[element] = [...attrs, ...svgGenericWhiteList]
|
|
136
|
+
}
|
|
130
137
|
|
|
131
138
|
// Produce a Namespace-aware version of svgWhitelist
|
|
132
139
|
const svgWhiteListNS_ = {}
|
|
133
|
-
|
|
140
|
+
for (const [elt, atts] of Object.entries(svgWhiteList_)) {
|
|
134
141
|
const attNS = {}
|
|
135
|
-
|
|
142
|
+
for (const att of atts) {
|
|
136
143
|
if (att.includes(':')) {
|
|
137
|
-
const
|
|
138
|
-
attNS[
|
|
144
|
+
const [prefix, localName] = att.split(':')
|
|
145
|
+
attNS[localName] = NS[prefix.toUpperCase()]
|
|
139
146
|
} else {
|
|
140
147
|
attNS[att] = att === 'xmlns' ? NS.XMLNS : null
|
|
141
148
|
}
|
|
142
|
-
}
|
|
149
|
+
}
|
|
143
150
|
svgWhiteListNS_[elt] = attNS
|
|
144
|
-
}
|
|
151
|
+
}
|
|
145
152
|
|
|
146
153
|
/**
|
|
147
154
|
* Sanitizes the input node and its children.
|
|
@@ -201,7 +208,7 @@ export const sanitizeSvg = (node) => {
|
|
|
201
208
|
const seAttrNS = (attrName.startsWith('se:')) ? NS.SE : ((attrName.startsWith('oi:')) ? NS.OI : null)
|
|
202
209
|
seAttrs.push([attrName, attr.value, seAttrNS])
|
|
203
210
|
} else {
|
|
204
|
-
|
|
211
|
+
warn(`attribute ${attrName} in element ${node.nodeName} not in whitelist is removed: ${node.outerHTML}`, null, 'sanitize')
|
|
205
212
|
node.removeAttributeNS(attrNsURI, attrLocalName)
|
|
206
213
|
}
|
|
207
214
|
}
|
|
@@ -224,6 +231,13 @@ export const sanitizeSvg = (node) => {
|
|
|
224
231
|
}
|
|
225
232
|
}
|
|
226
233
|
|
|
234
|
+
// If legacy xlink:href is present but href is missing, mirror it to href for modern browsers
|
|
235
|
+
const xlinkHref = node.getAttributeNS(NS.XLINK, 'href')
|
|
236
|
+
if (xlinkHref) {
|
|
237
|
+
node.setAttribute('href', xlinkHref)
|
|
238
|
+
node.removeAttributeNS(NS.XLINK, 'href')
|
|
239
|
+
}
|
|
240
|
+
|
|
227
241
|
Object.values(seAttrs).forEach(([att, val, ns]) => {
|
|
228
242
|
node.setAttributeNS(ns, att, val)
|
|
229
243
|
})
|
|
@@ -236,17 +250,35 @@ export const sanitizeSvg = (node) => {
|
|
|
236
250
|
'radialGradient', 'textPath', 'use'].includes(node.nodeName) && href[0] !== '#') {
|
|
237
251
|
// remove the attribute (but keep the element)
|
|
238
252
|
setHref(node, '')
|
|
239
|
-
|
|
253
|
+
warn(`attribute href in element ${node.nodeName} pointing to a non-local reference (${href}) is removed: ${node.outerHTML}`, null, 'sanitize')
|
|
240
254
|
node.removeAttributeNS(NS.XLINK, 'href')
|
|
241
255
|
node.removeAttribute('href')
|
|
242
256
|
}
|
|
243
257
|
|
|
244
258
|
// Safari crashes on a <use> without a xlink:href, so we just remove the node here
|
|
245
259
|
if (node.nodeName === 'use' && !getHref(node)) {
|
|
246
|
-
|
|
260
|
+
warn(`element ${node.nodeName} without a xlink:href or href is removed: ${node.outerHTML}`, null, 'sanitize')
|
|
247
261
|
node.remove()
|
|
248
262
|
return
|
|
249
263
|
}
|
|
264
|
+
// For <use> elements with missing width/height, derive defaults from referenced viewBox/size for proper sizing/selection
|
|
265
|
+
if (node.nodeName === 'use') {
|
|
266
|
+
const ref = getRefElem(getHref(node))
|
|
267
|
+
if (ref) {
|
|
268
|
+
const refViewBox = ref.getAttribute('viewBox')
|
|
269
|
+
const viewBoxParts = refViewBox ? refViewBox.split(/[\s,]+/).map(Number) : null
|
|
270
|
+
const refWidth = Number(ref.getAttribute('width'))
|
|
271
|
+
const refHeight = Number(ref.getAttribute('height'))
|
|
272
|
+
if (!node.hasAttribute('width')) {
|
|
273
|
+
const width = viewBoxParts?.[2] || refWidth
|
|
274
|
+
if (width) node.setAttribute('width', width)
|
|
275
|
+
}
|
|
276
|
+
if (!node.hasAttribute('height')) {
|
|
277
|
+
const height = viewBoxParts?.[3] || refHeight
|
|
278
|
+
if (height) node.setAttribute('height', height)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
250
282
|
// if the element has attributes pointing to a non-local reference,
|
|
251
283
|
// need to remove the attribute
|
|
252
284
|
['clip-path', 'fill', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke'].forEach((attr) => {
|
|
@@ -256,7 +288,7 @@ export const sanitizeSvg = (node) => {
|
|
|
256
288
|
// simply check for first character being a '#'
|
|
257
289
|
if (val && val[0] !== '#') {
|
|
258
290
|
node.setAttribute(attr, '')
|
|
259
|
-
|
|
291
|
+
warn(`attribute ${attr} in element ${node.nodeName} pointing to a non-local reference (${val}) is removed: ${node.outerHTML}`, null, 'sanitize')
|
|
260
292
|
node.removeAttribute(attr)
|
|
261
293
|
}
|
|
262
294
|
}
|
|
@@ -269,7 +301,7 @@ export const sanitizeSvg = (node) => {
|
|
|
269
301
|
} else {
|
|
270
302
|
// remove all children from this node and insert them before this node
|
|
271
303
|
// TODO: in the case of animation elements this will hardly ever be correct
|
|
272
|
-
|
|
304
|
+
warn(`element ${node.nodeName} not supported is removed: ${node.outerHTML}`, null, 'sanitize')
|
|
273
305
|
const children = []
|
|
274
306
|
while (node.hasChildNodes()) {
|
|
275
307
|
children.push(parent.insertBefore(node.firstChild, node))
|