@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.
@@ -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 { firstChild } = t.parentNode
107
- if (firstChild.tagName === 'title') {
108
- firstChild = firstChild.nextSibling
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.tagName === 'defs') {
113
- firstChild = firstChild.nextSibling
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, 'Move ' + dir)
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
- } while (el === el.previousElementSibling)
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.setAttribute('enablemenuitems', '#paste,#paste_in_place')
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 + '_' + suffix
884
+ gfilter.id = `${elem.id}_${suffix}`
795
885
  svgCanvas.changeSelectedAttribute(
796
886
  'filter',
797
- 'url(#' + gfilter.id + ')',
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
- // [ gm ] [ chm ] = [ chm ] [ gm' ]
907
- // [ gm' ] = [ chmInv ] [ gm ] [ chm ]
908
- const chm = transformListToTransform(chtlist).matrix
909
- const chmInv = chm.inverse()
910
- const gm = matrixMultiply(chmInv, m, chm)
911
- newxform.setMatrix(gm)
912
- chtlist.appendItem(newxform)
913
- }
914
- const cmd = recalculateDimensions(elem)
915
- if (cmd) {
916
- batchCmd.addSubCommand(cmd)
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 prev = $elem.previousElementSibling
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
- $elem.nextElementSibling,
1000
- $elem.parentNode
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
- prev.after(g)
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
- console.error(e)
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
- console.warn('Unexpected element to ungroup:', elem)
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 symbol = getElement(getHref(g).substr(1))
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: '0 0 ' + svgCanvas.contentW + ' ' + svgCanvas.contentH
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
- 'translate(' + x + ',' + y + ')'
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, center.x, center.y)
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 (newTransform) {
430
- const newTransformArray = newTransform.split(/[ ,]+/)
431
- const round = (num) => Math.round(Number(num) + Number.EPSILON)
432
- const x = round(newTransformArray[1])
433
- newTransform = `${newTransformArray[0]} ${x} ${newTransformArray[2]}`
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('^-?[\\d\\.]+' + unit + '$')
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
- out.push(' viewBox="' + vb + '" xmlns="' + NS.SVG + '"')
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(' xmlns:' + nsMap[uri] + '="' + uri + '"')
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(' xmlns:' + nsMap[u] + '="' + u + '"')
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
- image.setAttributeNS(NS.XLINK, 'xlink:href', url)
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('[id="' + key + '"]')
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
- attrs.width = vb[2]
527
- attrs.height = vb[3]
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).substr(-1) === '%') {
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
- attrs.width = bb.width + bb.x
560
- attrs.height = bb.height + bb.y
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
- console.error(e)
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
- canvash = Number(svgCanvas.getSvgContent().getAttribute('height'))
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
- innerh > innerw
672
- ? 'scale(' + canvash / 3 / vb[3] + ')'
673
- : 'scale(' + canvash / 3 / vb[2] + ')'
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 = 'translate(0) ' + ts + ' translate(0)'
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
- console.error(e)
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.setAttribute('xlink:href', reader.result)
892
+ setHref(img, reader.result)
862
893
  resolve()
863
894
  }
864
895
  reader.readAsDataURL(blob)
865
896
  })
866
- } catch (error) {
867
- console.error('Failed to fetch image:', error)
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 width = svgElement.clientWidth || svgElement.getAttribute('width')
908
- const height =
909
- svgElement.clientHeight || svgElement.getAttribute('height')
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
- console.error('Failed to load SVG into image element:', err)
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, 'url(#' + newid + ')')
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 id = svgCanvas.getHref(el).substr(1)
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 = function (svgdoc, dimensions) {
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]}"