@svgedit/svgcanvas 7.2.6 → 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/CHANGES.md +6 -0
- 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 +51 -31
- 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 +100 -23
- 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 +438 -24
- package/core/sanitize.js +71 -34
- package/core/select.js +44 -20
- package/core/selected-elem.js +146 -31
- package/core/selection.js +16 -6
- package/core/svg-exec.js +103 -29
- 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 +316 -203
- package/dist/svgcanvas.js +31616 -53281
- 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/selected-elem.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { NS } from './namespaces.js'
|
|
10
10
|
import * as hstry from './history.js'
|
|
11
11
|
import * as pathModule from './path.js'
|
|
12
|
+
import { warn, error } from '../common/logger.js'
|
|
12
13
|
import {
|
|
13
14
|
getStrokedBBoxDefaultVisible,
|
|
14
15
|
setHref,
|
|
@@ -63,6 +64,7 @@ export const init = canvas => {
|
|
|
63
64
|
svgCanvas.updateCanvas = updateCanvas // Updates the editor canvas width/height/position after a zoom has occurred.
|
|
64
65
|
svgCanvas.cycleElement = cycleElement // Select the next/previous element within the current layer.
|
|
65
66
|
svgCanvas.deleteSelectedElements = deleteSelectedElements // Removes all selected elements from the DOM and adds the change to the history
|
|
67
|
+
svgCanvas.flipSelectedElements = flipSelectedElements // Flips selected elements horizontally or vertically
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/**
|
|
@@ -103,14 +105,17 @@ const moveToBottomSelectedElem = () => {
|
|
|
103
105
|
let t = selected
|
|
104
106
|
const oldParent = t.parentNode
|
|
105
107
|
const oldNextSibling = t.nextSibling
|
|
106
|
-
let
|
|
107
|
-
if (firstChild
|
|
108
|
-
firstChild = firstChild.
|
|
108
|
+
let firstChild = t.parentNode.firstElementChild
|
|
109
|
+
if (firstChild?.tagName === 'title') {
|
|
110
|
+
firstChild = firstChild.nextElementSibling
|
|
109
111
|
}
|
|
110
112
|
// This can probably be removed, as the defs should not ever apppear
|
|
111
113
|
// inside a layer group
|
|
112
|
-
if (firstChild
|
|
113
|
-
firstChild = firstChild.
|
|
114
|
+
if (firstChild?.tagName === 'defs') {
|
|
115
|
+
firstChild = firstChild.nextElementSibling
|
|
116
|
+
}
|
|
117
|
+
if (!firstChild) {
|
|
118
|
+
return
|
|
114
119
|
}
|
|
115
120
|
t = t.parentNode.insertBefore(t, firstChild)
|
|
116
121
|
// If the element actually moved position, add the command and fire the changed
|
|
@@ -178,7 +183,7 @@ const moveUpDownSelected = dir => {
|
|
|
178
183
|
// event handler.
|
|
179
184
|
if (oldNextSibling !== t.nextSibling) {
|
|
180
185
|
svgCanvas.addCommandToHistory(
|
|
181
|
-
new MoveElementCommand(t, oldNextSibling, oldParent,
|
|
186
|
+
new MoveElementCommand(t, oldNextSibling, oldParent, `Move ${dir}`)
|
|
182
187
|
)
|
|
183
188
|
svgCanvas.call('changed', [t])
|
|
184
189
|
}
|
|
@@ -207,6 +212,9 @@ const moveSelectedElements = (dx, dy, undoable = true) => {
|
|
|
207
212
|
const batchCmd = new BatchCommand('position')
|
|
208
213
|
selectedElements.forEach((selected, i) => {
|
|
209
214
|
if (selected) {
|
|
215
|
+
// Store the existing transform before modifying
|
|
216
|
+
const existingTransform = selected.getAttribute('transform') || ''
|
|
217
|
+
|
|
210
218
|
const xform = svgCanvas.getSvgRoot().createSVGTransform()
|
|
211
219
|
const tlist = getTransformList(selected)
|
|
212
220
|
|
|
@@ -226,6 +234,12 @@ const moveSelectedElements = (dx, dy, undoable = true) => {
|
|
|
226
234
|
const cmd = recalculateDimensions(selected)
|
|
227
235
|
if (cmd) {
|
|
228
236
|
batchCmd.addSubCommand(cmd)
|
|
237
|
+
} else if ((selected.getAttribute('transform') || '') !== existingTransform) {
|
|
238
|
+
// For groups and other elements where recalculateDimensions returns null,
|
|
239
|
+
// record the transform change directly
|
|
240
|
+
batchCmd.addSubCommand(
|
|
241
|
+
new ChangeElementCommand(selected, { transform: existingTransform })
|
|
242
|
+
)
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
svgCanvas
|
|
@@ -264,9 +278,11 @@ const cloneSelectedElements = (x, y) => {
|
|
|
264
278
|
const index = el => {
|
|
265
279
|
if (!el) return -1
|
|
266
280
|
let i = 0
|
|
281
|
+
let current = el
|
|
267
282
|
do {
|
|
268
283
|
i++
|
|
269
|
-
|
|
284
|
+
current = current.previousElementSibling
|
|
285
|
+
} while (current)
|
|
270
286
|
return i
|
|
271
287
|
}
|
|
272
288
|
|
|
@@ -621,13 +637,87 @@ const deleteSelectedElements = () => {
|
|
|
621
637
|
svgCanvas.clearSelection()
|
|
622
638
|
}
|
|
623
639
|
|
|
640
|
+
/**
|
|
641
|
+
* Flips selected elements horizontally or vertically by transforming actual coordinates.
|
|
642
|
+
* @function module:selected-elem.SvgCanvas#flipSelectedElements
|
|
643
|
+
* @param {number} scaleX - Scale factor for X axis (-1 for horizontal flip, 1 for no flip)
|
|
644
|
+
* @param {number} scaleY - Scale factor for Y axis (1 for no flip, -1 for vertical flip)
|
|
645
|
+
* @fires module:selected-elem.SvgCanvas#event:changed
|
|
646
|
+
* @returns {void}
|
|
647
|
+
*/
|
|
648
|
+
const flipSelectedElements = (scaleX, scaleY) => {
|
|
649
|
+
const selectedElements = svgCanvas.getSelectedElements()
|
|
650
|
+
const batchCmd = new BatchCommand('Flip Elements')
|
|
651
|
+
const svgRoot = svgCanvas.getSvgRoot()
|
|
652
|
+
|
|
653
|
+
selectedElements.forEach(selected => {
|
|
654
|
+
if (!selected) return
|
|
655
|
+
|
|
656
|
+
const bbox = getStrokedBBoxDefaultVisible([selected])
|
|
657
|
+
if (!bbox) return
|
|
658
|
+
|
|
659
|
+
const cx = bbox.x + bbox.width / 2
|
|
660
|
+
const cy = bbox.y + bbox.height / 2
|
|
661
|
+
const existingTransform = selected.getAttribute('transform') || ''
|
|
662
|
+
|
|
663
|
+
const flipMatrix = svgRoot
|
|
664
|
+
.createSVGMatrix()
|
|
665
|
+
.translate(cx, cy)
|
|
666
|
+
.scaleNonUniform(scaleX, scaleY)
|
|
667
|
+
.translate(-cx, -cy)
|
|
668
|
+
|
|
669
|
+
const tlist = getTransformList(selected)
|
|
670
|
+
const combinedMatrix = matrixMultiply(
|
|
671
|
+
transformListToTransform(tlist).matrix,
|
|
672
|
+
flipMatrix
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
const flipTransform = svgRoot.createSVGTransform()
|
|
676
|
+
flipTransform.setMatrix(combinedMatrix)
|
|
677
|
+
|
|
678
|
+
tlist.clear()
|
|
679
|
+
tlist.appendItem(flipTransform)
|
|
680
|
+
|
|
681
|
+
const prevStartTransform = svgCanvas.getStartTransform
|
|
682
|
+
? svgCanvas.getStartTransform()
|
|
683
|
+
: null
|
|
684
|
+
if (svgCanvas.setStartTransform) {
|
|
685
|
+
svgCanvas.setStartTransform(existingTransform)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const cmd = recalculateDimensions(selected)
|
|
689
|
+
|
|
690
|
+
if (svgCanvas.setStartTransform) {
|
|
691
|
+
svgCanvas.setStartTransform(prevStartTransform)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (cmd) {
|
|
695
|
+
batchCmd.addSubCommand(cmd)
|
|
696
|
+
} else if ((selected.getAttribute('transform') || '') !== existingTransform) {
|
|
697
|
+
batchCmd.addSubCommand(
|
|
698
|
+
new ChangeElementCommand(selected, { transform: existingTransform })
|
|
699
|
+
)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
svgCanvas
|
|
703
|
+
.gettingSelectorManager()
|
|
704
|
+
.requestSelector(selected)
|
|
705
|
+
.resize()
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
if (!batchCmd.isEmpty()) {
|
|
709
|
+
svgCanvas.addCommandToHistory(batchCmd)
|
|
710
|
+
svgCanvas.call('changed', selectedElements.filter(Boolean))
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
624
714
|
/**
|
|
625
715
|
* Remembers the current selected elements on the clipboard.
|
|
626
716
|
* @function module:selected-elem.SvgCanvas#copySelectedElements
|
|
627
717
|
* @returns {void}
|
|
628
718
|
*/
|
|
629
719
|
const copySelectedElements = () => {
|
|
630
|
-
const selectedElements = svgCanvas.getSelectedElements()
|
|
720
|
+
const selectedElements = svgCanvas.getSelectedElements().filter(Boolean)
|
|
631
721
|
const data = JSON.stringify(
|
|
632
722
|
selectedElements.map(x => svgCanvas.getJsonFromSvgElements(x))
|
|
633
723
|
)
|
|
@@ -637,7 +727,7 @@ const copySelectedElements = () => {
|
|
|
637
727
|
|
|
638
728
|
// Context menu might not exist (it is provided by editor.js).
|
|
639
729
|
const canvMenu = document.getElementById('se-cmenu_canvas')
|
|
640
|
-
canvMenu
|
|
730
|
+
canvMenu?.setAttribute('enablemenuitems', '#paste,#paste_in_place')
|
|
641
731
|
}
|
|
642
732
|
|
|
643
733
|
/**
|
|
@@ -791,10 +881,10 @@ const pushGroupProperty = (g, undoable) => {
|
|
|
791
881
|
// Change this in future for different filters
|
|
792
882
|
const suffix =
|
|
793
883
|
blurElem?.tagName === 'feGaussianBlur' ? 'blur' : 'filter'
|
|
794
|
-
gfilter.id = elem.id
|
|
884
|
+
gfilter.id = `${elem.id}_${suffix}`
|
|
795
885
|
svgCanvas.changeSelectedAttribute(
|
|
796
886
|
'filter',
|
|
797
|
-
|
|
887
|
+
`url(#${gfilter.id})`,
|
|
798
888
|
[elem]
|
|
799
889
|
)
|
|
800
890
|
}
|
|
@@ -901,20 +991,29 @@ const pushGroupProperty = (g, undoable) => {
|
|
|
901
991
|
changes = {}
|
|
902
992
|
changes.transform = oldxform || ''
|
|
903
993
|
|
|
994
|
+
// Simply prepend the group's transform to the child's transform list
|
|
995
|
+
// New transform = [group transform] [child transform]
|
|
996
|
+
// This preserves the correct application order
|
|
904
997
|
const newxform = svgCanvas.getSvgRoot().createSVGTransform()
|
|
998
|
+
newxform.setMatrix(m)
|
|
905
999
|
|
|
906
|
-
//
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1000
|
+
// Insert group's transform at the beginning of child's transform list
|
|
1001
|
+
if (chtlist.numberOfItems) {
|
|
1002
|
+
chtlist.insertItemBefore(newxform, 0)
|
|
1003
|
+
} else {
|
|
1004
|
+
chtlist.appendItem(newxform)
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Record the transform change for undo/redo
|
|
1008
|
+
if (undoable) {
|
|
1009
|
+
batchCmd.addSubCommand(new ChangeElementCommand(elem, changes))
|
|
1010
|
+
}
|
|
917
1011
|
}
|
|
1012
|
+
// NOTE: We intentionally do NOT call recalculateDimensions here because:
|
|
1013
|
+
// 1. It reorders transforms (moves rotate before translate), changing the visual result
|
|
1014
|
+
// 2. It recalculates rotation centers, causing elements to jump
|
|
1015
|
+
// 3. The prepended group transform is already in the correct position
|
|
1016
|
+
// Just leave the transforms as-is after prepending the group's transform
|
|
918
1017
|
}
|
|
919
1018
|
}
|
|
920
1019
|
|
|
@@ -972,6 +1071,10 @@ const convertToGroup = elem => {
|
|
|
972
1071
|
svgCanvas.call('selected', [elem])
|
|
973
1072
|
} else if (dataStorage.has($elem, 'symbol')) {
|
|
974
1073
|
elem = dataStorage.get($elem, 'symbol')
|
|
1074
|
+
if (!elem) {
|
|
1075
|
+
warn('Unable to convert <use>: missing symbol reference', null, 'selected-elem')
|
|
1076
|
+
return
|
|
1077
|
+
}
|
|
975
1078
|
|
|
976
1079
|
ts = $elem.getAttribute('transform') || ''
|
|
977
1080
|
const pos = {
|
|
@@ -990,14 +1093,15 @@ const convertToGroup = elem => {
|
|
|
990
1093
|
// Not ideal, but works
|
|
991
1094
|
ts += ' translate(' + (pos.x || 0) + ',' + (pos.y || 0) + ')'
|
|
992
1095
|
|
|
993
|
-
const
|
|
1096
|
+
const useParent = $elem.parentNode
|
|
1097
|
+
const useNextSibling = $elem.nextSibling
|
|
994
1098
|
|
|
995
1099
|
// Remove <use> element
|
|
996
1100
|
batchCmd.addSubCommand(
|
|
997
1101
|
new RemoveElementCommand(
|
|
998
1102
|
$elem,
|
|
999
|
-
|
|
1000
|
-
|
|
1103
|
+
useNextSibling,
|
|
1104
|
+
useParent
|
|
1001
1105
|
)
|
|
1002
1106
|
)
|
|
1003
1107
|
$elem.remove()
|
|
@@ -1049,7 +1153,9 @@ const convertToGroup = elem => {
|
|
|
1049
1153
|
// now give the g itself a new id
|
|
1050
1154
|
g.id = svgCanvas.getNextId()
|
|
1051
1155
|
|
|
1052
|
-
|
|
1156
|
+
if (useParent) {
|
|
1157
|
+
useParent.insertBefore(g, useNextSibling)
|
|
1158
|
+
}
|
|
1053
1159
|
|
|
1054
1160
|
if (parent) {
|
|
1055
1161
|
if (!hasMore) {
|
|
@@ -1077,7 +1183,7 @@ const convertToGroup = elem => {
|
|
|
1077
1183
|
try {
|
|
1078
1184
|
recalculateDimensions(n)
|
|
1079
1185
|
} catch (e) {
|
|
1080
|
-
|
|
1186
|
+
error('Error recalculating dimensions', e, 'selected-elem')
|
|
1081
1187
|
}
|
|
1082
1188
|
})
|
|
1083
1189
|
|
|
@@ -1098,7 +1204,7 @@ const convertToGroup = elem => {
|
|
|
1098
1204
|
|
|
1099
1205
|
svgCanvas.addCommandToHistory(batchCmd)
|
|
1100
1206
|
} else {
|
|
1101
|
-
|
|
1207
|
+
warn('Unexpected element to ungroup:', elem, 'selected-elem')
|
|
1102
1208
|
}
|
|
1103
1209
|
}
|
|
1104
1210
|
|
|
@@ -1122,7 +1228,16 @@ const ungroupSelectedElement = () => {
|
|
|
1122
1228
|
}
|
|
1123
1229
|
if (g.tagName === 'use') {
|
|
1124
1230
|
// Somehow doesn't have data set, so retrieve
|
|
1125
|
-
const
|
|
1231
|
+
const href = getHref(g)
|
|
1232
|
+
if (!href || !href.startsWith('#')) {
|
|
1233
|
+
warn('Unexpected <use> without local reference:', g, 'selected-elem')
|
|
1234
|
+
return
|
|
1235
|
+
}
|
|
1236
|
+
const symbol = getElement(href.slice(1))
|
|
1237
|
+
if (!symbol) {
|
|
1238
|
+
warn('Unexpected <use> without resolved reference:', g, 'selected-elem')
|
|
1239
|
+
return
|
|
1240
|
+
}
|
|
1126
1241
|
dataStorage.put(g, 'symbol', symbol)
|
|
1127
1242
|
dataStorage.put(g, 'ref', symbol)
|
|
1128
1243
|
convertToGroup(g)
|
|
@@ -1206,7 +1321,7 @@ const updateCanvas = (w, h) => {
|
|
|
1206
1321
|
height: svgCanvas.contentH * zoom,
|
|
1207
1322
|
x,
|
|
1208
1323
|
y,
|
|
1209
|
-
viewBox:
|
|
1324
|
+
viewBox: `0 0 ${svgCanvas.contentW} ${svgCanvas.contentH}`
|
|
1210
1325
|
})
|
|
1211
1326
|
|
|
1212
1327
|
assignAttributes(bg, {
|
|
@@ -1226,7 +1341,7 @@ const updateCanvas = (w, h) => {
|
|
|
1226
1341
|
|
|
1227
1342
|
svgCanvas.selectorManager.selectorParentGroup.setAttribute(
|
|
1228
1343
|
'transform',
|
|
1229
|
-
|
|
1344
|
+
`translate(${x},${y})`
|
|
1230
1345
|
)
|
|
1231
1346
|
|
|
1232
1347
|
/**
|
package/core/selection.js
CHANGED
|
@@ -409,8 +409,11 @@ const setRotationAngle = (val, preventUndo) => {
|
|
|
409
409
|
cy,
|
|
410
410
|
transformListToTransform(tlist).matrix
|
|
411
411
|
)
|
|
412
|
+
// Safety check: if center coordinates are invalid (NaN), fall back to untransformed bbox center
|
|
413
|
+
const centerX = Number.isFinite(center.x) ? center.x : cx
|
|
414
|
+
const centerY = Number.isFinite(center.y) ? center.y : cy
|
|
412
415
|
const Rnc = svgCanvas.getSvgRoot().createSVGTransform()
|
|
413
|
-
Rnc.setRotate(val,
|
|
416
|
+
Rnc.setRotate(val, centerX, centerY)
|
|
414
417
|
if (tlist.numberOfItems) {
|
|
415
418
|
tlist.insertItemBefore(Rnc, 0)
|
|
416
419
|
} else {
|
|
@@ -424,13 +427,20 @@ const setRotationAngle = (val, preventUndo) => {
|
|
|
424
427
|
// we need to undo it, then redo it so it can be undo-able! :)
|
|
425
428
|
// TODO: figure out how to make changes to transform list undo-able cross-browser?
|
|
426
429
|
let newTransform = elem.getAttribute('transform')
|
|
430
|
+
|
|
427
431
|
// new transform is something like: 'rotate(5 1.39625e-8 -11)'
|
|
428
432
|
// we round the x so it becomes 'rotate(5 0 -11)'
|
|
429
|
-
if
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
433
|
+
// Only do this manipulation if the first transform is actually a rotation
|
|
434
|
+
if (newTransform && newTransform.startsWith('rotate(')) {
|
|
435
|
+
const match = newTransform.match(/^rotate\(([\d.\-e]+)\s+([\d.\-e]+)\s+([\d.\-e]+)\)(.*)/)
|
|
436
|
+
if (match) {
|
|
437
|
+
const angle = Number.parseFloat(match[1])
|
|
438
|
+
const round = (num) => Math.round(Number(num) + Number.EPSILON)
|
|
439
|
+
const x = round(match[2])
|
|
440
|
+
const y = round(match[3])
|
|
441
|
+
const restOfTransform = match[4] || '' // Preserve any transforms after the rotate
|
|
442
|
+
newTransform = `rotate(${angle} ${x} ${y})${restOfTransform}`
|
|
443
|
+
}
|
|
434
444
|
}
|
|
435
445
|
|
|
436
446
|
if (oldTransform) {
|
package/core/svg-exec.js
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
import { jsPDF as JsPDF } from 'jspdf'
|
|
9
9
|
import 'svg2pdf.js'
|
|
10
10
|
import * as history from './history.js'
|
|
11
|
+
import { error } from '../common/logger.js'
|
|
11
12
|
import {
|
|
12
13
|
text2xml,
|
|
13
14
|
cleanupElement,
|
|
14
15
|
findDefs,
|
|
16
|
+
setHref,
|
|
15
17
|
getHref,
|
|
16
18
|
preventClickDefault,
|
|
17
19
|
toXml,
|
|
@@ -130,7 +132,7 @@ const svgToString = (elem, indent) => {
|
|
|
130
132
|
const nsMap = svgCanvas.getNsMap()
|
|
131
133
|
const out = []
|
|
132
134
|
const unit = curConfig.baseUnit
|
|
133
|
-
const unitRe = new RegExp(
|
|
135
|
+
const unitRe = new RegExp(`^-?[\\d\\.]+${unit}$`)
|
|
134
136
|
|
|
135
137
|
if (elem) {
|
|
136
138
|
cleanupElement(elem)
|
|
@@ -163,7 +165,10 @@ const svgToString = (elem, indent) => {
|
|
|
163
165
|
// }
|
|
164
166
|
if (curConfig.dynamicOutput) {
|
|
165
167
|
vb = elem.getAttribute('viewBox')
|
|
166
|
-
|
|
168
|
+
if (!vb) {
|
|
169
|
+
vb = [0, 0, res.w, res.h].join(' ')
|
|
170
|
+
}
|
|
171
|
+
out.push(` viewBox="${vb}" xmlns="${NS.SVG}"`)
|
|
167
172
|
} else {
|
|
168
173
|
if (unit !== 'px') {
|
|
169
174
|
res.w = convertUnit(res.w, unit) + unit
|
|
@@ -192,14 +197,14 @@ const svgToString = (elem, indent) => {
|
|
|
192
197
|
nsMap[uri] !== 'xml'
|
|
193
198
|
) {
|
|
194
199
|
nsuris[uri] = true
|
|
195
|
-
out.push(
|
|
200
|
+
out.push(` xmlns:${nsMap[uri]}="${uri}"`)
|
|
196
201
|
}
|
|
197
202
|
if (el.attributes.length > 0) {
|
|
198
203
|
for (const [, attr] of Object.entries(el.attributes)) {
|
|
199
204
|
const u = attr.namespaceURI
|
|
200
205
|
if (u && !nsuris[u] && nsMap[u] !== 'xmlns' && nsMap[u] !== 'xml') {
|
|
201
206
|
nsuris[u] = true
|
|
202
|
-
out.push(
|
|
207
|
+
out.push(` xmlns:${nsMap[u]}="${u}"`)
|
|
203
208
|
}
|
|
204
209
|
}
|
|
205
210
|
}
|
|
@@ -443,7 +448,8 @@ const setSvgString = (xmlString, preventUndo) => {
|
|
|
443
448
|
// const url = decodeURIComponent(m.groups.url);
|
|
444
449
|
const iimg = new Image()
|
|
445
450
|
iimg.addEventListener('load', () => {
|
|
446
|
-
|
|
451
|
+
// Set the href attribute to the data URL
|
|
452
|
+
setHref(image, val)
|
|
447
453
|
})
|
|
448
454
|
iimg.src = url
|
|
449
455
|
}
|
|
@@ -467,7 +473,7 @@ const setSvgString = (xmlString, preventUndo) => {
|
|
|
467
473
|
|
|
468
474
|
Object.entries(ids).forEach(([key, value]) => {
|
|
469
475
|
if (value > 1) {
|
|
470
|
-
const nodes = content.querySelectorAll(
|
|
476
|
+
const nodes = content.querySelectorAll(`[id="${key}"]`)
|
|
471
477
|
for (let i = 1; i < nodes.length; i++) {
|
|
472
478
|
nodes[i].setAttribute('id', svgCanvas.getNextId())
|
|
473
479
|
}
|
|
@@ -523,14 +529,20 @@ const setSvgString = (xmlString, preventUndo) => {
|
|
|
523
529
|
if (content.getAttribute('viewBox')) {
|
|
524
530
|
const viBox = content.getAttribute('viewBox')
|
|
525
531
|
const vb = viBox.split(/[ ,]+/)
|
|
526
|
-
|
|
527
|
-
|
|
532
|
+
const vbWidth = Number(vb[2])
|
|
533
|
+
const vbHeight = Number(vb[3])
|
|
534
|
+
if (Number.isFinite(vbWidth)) {
|
|
535
|
+
attrs.width = vbWidth
|
|
536
|
+
}
|
|
537
|
+
if (Number.isFinite(vbHeight)) {
|
|
538
|
+
attrs.height = vbHeight
|
|
539
|
+
}
|
|
528
540
|
// handle content that doesn't have a viewBox
|
|
529
541
|
} else {
|
|
530
542
|
;['width', 'height'].forEach(dim => {
|
|
531
543
|
// Set to 100 if not given
|
|
532
544
|
const val = content.getAttribute(dim) || '100%'
|
|
533
|
-
if (String(val).
|
|
545
|
+
if (String(val).slice(-1) === '%') {
|
|
534
546
|
// Use user units if percentage given
|
|
535
547
|
percs = true
|
|
536
548
|
} else {
|
|
@@ -556,16 +568,25 @@ const setSvgString = (xmlString, preventUndo) => {
|
|
|
556
568
|
// Percentage width/height, so let's base it on visible elements
|
|
557
569
|
if (percs) {
|
|
558
570
|
const bb = getStrokedBBoxDefaultVisible()
|
|
559
|
-
|
|
560
|
-
|
|
571
|
+
if (bb && typeof bb === 'object') {
|
|
572
|
+
attrs.width = bb.width + bb.x
|
|
573
|
+
attrs.height = bb.height + bb.y
|
|
574
|
+
} else {
|
|
575
|
+
if (attrs.width === null || attrs.width === undefined) {
|
|
576
|
+
attrs.width = 100
|
|
577
|
+
}
|
|
578
|
+
if (attrs.height === null || attrs.height === undefined) {
|
|
579
|
+
attrs.height = 100
|
|
580
|
+
}
|
|
581
|
+
}
|
|
561
582
|
}
|
|
562
583
|
|
|
563
584
|
// Just in case negative numbers are given or
|
|
564
585
|
// result from the percs calculation
|
|
565
|
-
if (attrs.width <= 0) {
|
|
586
|
+
if (!Number.isFinite(attrs.width) || attrs.width <= 0) {
|
|
566
587
|
attrs.width = 100
|
|
567
588
|
}
|
|
568
|
-
if (attrs.height <= 0) {
|
|
589
|
+
if (!Number.isFinite(attrs.height) || attrs.height <= 0) {
|
|
569
590
|
attrs.height = 100
|
|
570
591
|
}
|
|
571
592
|
|
|
@@ -594,7 +615,7 @@ const setSvgString = (xmlString, preventUndo) => {
|
|
|
594
615
|
if (!preventUndo) svgCanvas.addCommandToHistory(batchCmd)
|
|
595
616
|
svgCanvas.call('sourcechanged', [svgCanvas.getSvgContent()])
|
|
596
617
|
} catch (e) {
|
|
597
|
-
|
|
618
|
+
error('Error setting SVG string', e, 'svg-exec')
|
|
598
619
|
return false
|
|
599
620
|
}
|
|
600
621
|
|
|
@@ -664,16 +685,26 @@ const importSvgString = (xmlString, preserveDimension) => {
|
|
|
664
685
|
|
|
665
686
|
// TODO: properly handle preserveAspectRatio
|
|
666
687
|
const // canvasw = +svgContent.getAttribute('width'),
|
|
667
|
-
|
|
688
|
+
rawCanvash = Number(svgCanvas.getSvgContent().getAttribute('height'))
|
|
689
|
+
const canvash =
|
|
690
|
+
Number.isFinite(rawCanvash) && rawCanvash > 0
|
|
691
|
+
? rawCanvash
|
|
692
|
+
: (Number(svgCanvas.getCurConfig().dimensions?.[1]) || 100)
|
|
668
693
|
// imported content should be 1/3 of the canvas on its largest dimension
|
|
669
694
|
|
|
695
|
+
const vbWidth = vb[2]
|
|
696
|
+
const vbHeight = vb[3]
|
|
697
|
+
const importW = Number.isFinite(vbWidth) && vbWidth > 0 ? vbWidth : (innerw > 0 ? innerw : 100)
|
|
698
|
+
const importH = Number.isFinite(vbHeight) && vbHeight > 0 ? vbHeight : (innerh > 0 ? innerh : 100)
|
|
699
|
+
const safeImportW = Number.isFinite(importW) && importW > 0 ? importW : 100
|
|
700
|
+
const safeImportH = Number.isFinite(importH) && importH > 0 ? importH : 100
|
|
670
701
|
ts =
|
|
671
|
-
|
|
672
|
-
? 'scale(' + canvash / 3 /
|
|
673
|
-
: 'scale(' + canvash / 3 /
|
|
702
|
+
safeImportH > safeImportW
|
|
703
|
+
? 'scale(' + canvash / 3 / safeImportH + ')'
|
|
704
|
+
: 'scale(' + canvash / 3 / safeImportW + ')'
|
|
674
705
|
|
|
675
706
|
// Hack to make recalculateDimensions understand how to scale
|
|
676
|
-
ts =
|
|
707
|
+
ts = `translate(0) ${ts} translate(0)`
|
|
677
708
|
|
|
678
709
|
symbol = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'symbol')
|
|
679
710
|
const defs = findDefs()
|
|
@@ -736,7 +767,7 @@ const importSvgString = (xmlString, preserveDimension) => {
|
|
|
736
767
|
svgCanvas.addCommandToHistory(batchCmd)
|
|
737
768
|
svgCanvas.call('changed', [svgCanvas.getSvgContent()])
|
|
738
769
|
} catch (e) {
|
|
739
|
-
|
|
770
|
+
error('Error importing SVG string', e, 'svg-exec')
|
|
740
771
|
return null
|
|
741
772
|
}
|
|
742
773
|
|
|
@@ -858,13 +889,13 @@ const convertImagesToBase64 = async svgElement => {
|
|
|
858
889
|
const reader = new FileReader()
|
|
859
890
|
return new Promise(resolve => {
|
|
860
891
|
reader.onload = () => {
|
|
861
|
-
img
|
|
892
|
+
setHref(img, reader.result)
|
|
862
893
|
resolve()
|
|
863
894
|
}
|
|
864
895
|
reader.readAsDataURL(blob)
|
|
865
896
|
})
|
|
866
|
-
} catch (
|
|
867
|
-
|
|
897
|
+
} catch (err) {
|
|
898
|
+
error('Failed to fetch image', err, 'svg-exec')
|
|
868
899
|
}
|
|
869
900
|
}
|
|
870
901
|
})
|
|
@@ -903,10 +934,14 @@ const rasterExport = (
|
|
|
903
934
|
|
|
904
935
|
const canvas = document.createElement('canvas')
|
|
905
936
|
const ctx = canvas.getContext('2d')
|
|
937
|
+
if (!ctx) {
|
|
938
|
+
reject(new Error('Canvas 2D context not available'))
|
|
939
|
+
return
|
|
940
|
+
}
|
|
906
941
|
|
|
907
|
-
const
|
|
908
|
-
const
|
|
909
|
-
|
|
942
|
+
const res = svgCanvas.getResolution()
|
|
943
|
+
const width = res.w
|
|
944
|
+
const height = res.h
|
|
910
945
|
canvas.width = width
|
|
911
946
|
canvas.height = height
|
|
912
947
|
|
|
@@ -1011,7 +1046,7 @@ const exportPDF = (
|
|
|
1011
1046
|
}
|
|
1012
1047
|
|
|
1013
1048
|
img.onerror = err => {
|
|
1014
|
-
|
|
1049
|
+
error('Failed to load SVG into image element', err, 'svg-exec')
|
|
1015
1050
|
reject(err)
|
|
1016
1051
|
}
|
|
1017
1052
|
|
|
@@ -1110,7 +1145,7 @@ const uniquifyElemsMethod = g => {
|
|
|
1110
1145
|
let j = attrs.length
|
|
1111
1146
|
while (j--) {
|
|
1112
1147
|
const attr = attrs[j]
|
|
1113
|
-
attr.ownerElement.setAttribute(attr.name,
|
|
1148
|
+
attr.ownerElement.setAttribute(attr.name, `url(#${newid})`)
|
|
1114
1149
|
}
|
|
1115
1150
|
|
|
1116
1151
|
// remap all href attributes
|
|
@@ -1140,7 +1175,11 @@ const setUseDataMethod = parent => {
|
|
|
1140
1175
|
|
|
1141
1176
|
Array.prototype.forEach.call(elems, (el, _) => {
|
|
1142
1177
|
const dataStorage = svgCanvas.getDataStorage()
|
|
1143
|
-
const
|
|
1178
|
+
const href = svgCanvas.getHref(el)
|
|
1179
|
+
if (!href || !href.startsWith('#')) {
|
|
1180
|
+
return
|
|
1181
|
+
}
|
|
1182
|
+
const id = href.substr(1)
|
|
1144
1183
|
const refElem = svgCanvas.getElement(id)
|
|
1145
1184
|
if (!refElem) {
|
|
1146
1185
|
return
|
|
@@ -1299,6 +1338,41 @@ const convertGradientsMethod = elem => {
|
|
|
1299
1338
|
grad.setAttribute('x2', (gCoords.x2 - bb.x) / bb.width)
|
|
1300
1339
|
grad.setAttribute('y2', (gCoords.y2 - bb.y) / bb.height)
|
|
1301
1340
|
grad.removeAttribute('gradientUnits')
|
|
1341
|
+
} else if (grad.tagName === 'radialGradient') {
|
|
1342
|
+
const getNum = (value, fallback) => {
|
|
1343
|
+
const num = Number(value)
|
|
1344
|
+
return Number.isFinite(num) ? num : fallback
|
|
1345
|
+
}
|
|
1346
|
+
let cx = getNum(grad.getAttribute('cx'), 0.5)
|
|
1347
|
+
let cy = getNum(grad.getAttribute('cy'), 0.5)
|
|
1348
|
+
let r = getNum(grad.getAttribute('r'), 0.5)
|
|
1349
|
+
let fx = getNum(grad.getAttribute('fx'), cx)
|
|
1350
|
+
let fy = getNum(grad.getAttribute('fy'), cy)
|
|
1351
|
+
|
|
1352
|
+
// If has transform, convert
|
|
1353
|
+
const tlist = getTransformList(grad)
|
|
1354
|
+
if (tlist?.numberOfItems > 0) {
|
|
1355
|
+
const m = transformListToTransform(tlist).matrix
|
|
1356
|
+
const cpt = transformPoint(cx, cy, m)
|
|
1357
|
+
const fpt = transformPoint(fx, fy, m)
|
|
1358
|
+
const rpt = transformPoint(cx + r, cy, m)
|
|
1359
|
+
cx = cpt.x
|
|
1360
|
+
cy = cpt.y
|
|
1361
|
+
fx = fpt.x
|
|
1362
|
+
fy = fpt.y
|
|
1363
|
+
r = Math.hypot(rpt.x - cpt.x, rpt.y - cpt.y)
|
|
1364
|
+
grad.removeAttribute('gradientTransform')
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (!bb.width || !bb.height) {
|
|
1368
|
+
return
|
|
1369
|
+
}
|
|
1370
|
+
grad.setAttribute('cx', (cx - bb.x) / bb.width)
|
|
1371
|
+
grad.setAttribute('cy', (cy - bb.y) / bb.height)
|
|
1372
|
+
grad.setAttribute('fx', (fx - bb.x) / bb.width)
|
|
1373
|
+
grad.setAttribute('fy', (fy - bb.y) / bb.height)
|
|
1374
|
+
grad.setAttribute('r', r / Math.max(bb.width, bb.height))
|
|
1375
|
+
grad.removeAttribute('gradientUnits')
|
|
1302
1376
|
}
|
|
1303
1377
|
}
|
|
1304
1378
|
})
|
package/core/svgroot.js
CHANGED
|
@@ -14,7 +14,7 @@ import { text2xml } from './utilities.js'
|
|
|
14
14
|
* @param {ArgumentsArray} dimensions - dimensions of width and height
|
|
15
15
|
* @returns {svgRootElement}
|
|
16
16
|
*/
|
|
17
|
-
export const svgRootElement =
|
|
17
|
+
export const svgRootElement = (svgdoc, dimensions) => {
|
|
18
18
|
return svgdoc.importNode(
|
|
19
19
|
text2xml(
|
|
20
20
|
`<svg id="svgroot" xmlns="${NS.SVG}" xlinkns="${NS.XLINK}" width="${dimensions[0]}"
|