@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/core/utilities.js CHANGED
@@ -108,15 +108,16 @@ export const dropXMLInternalSubset = str => {
108
108
  * @param {string} str - The string to be converted
109
109
  * @returns {string} The converted string
110
110
  */
111
- export const toXml = str => {
112
- // ' is ok in XML, but not HTML
113
- // > does not normally need escaping, though it can if within a CDATA expression (and preceded by "]]")
114
- return str
115
- .replace(/&/g, '&')
116
- .replace(/</g, '&lt;')
117
- .replace(/>/g, '&gt;')
118
- .replace(/"/g, '&quot;')
119
- .replace(/'/g, '&#x27;') // Note: `&apos;` is XML only
111
+ export const toXml = (str) => {
112
+ const xmlEntities = {
113
+ '&': '&amp;',
114
+ '<': '&lt;',
115
+ '>': '&gt;',
116
+ '"': '&quot;',
117
+ "'": '&#x27;' // Note: `&apos;` is XML only
118
+ }
119
+
120
+ return str.replace(/[&<>"']/g, (char) => xmlEntities[char])
120
121
  }
121
122
 
122
123
  // This code was written by Tyler Akins and has been placed in the
@@ -132,10 +133,9 @@ export const toXml = str => {
132
133
  * @param {string} input
133
134
  * @returns {string} Base64 output
134
135
  */
135
- export function encode64 (input) {
136
- // base64 strings are 4/3 larger than the original string
137
- input = encodeUTF8(input) // convert non-ASCII characters
138
- return window.btoa(input) // Use native if available
136
+ export const encode64 = (input) => {
137
+ const encoded = encodeUTF8(input) // convert non-ASCII characters
138
+ return window.btoa(encoded) // Use native if available
139
139
  }
140
140
 
141
141
  /**
@@ -144,23 +144,20 @@ export function encode64 (input) {
144
144
  * @param {string} input Base64-encoded input
145
145
  * @returns {string} Decoded output
146
146
  */
147
- export function decode64 (input) {
148
- return decodeUTF8(window.atob(input))
149
- }
147
+ export const decode64 = (input) => decodeUTF8(window.atob(input))
150
148
 
151
149
  /**
152
150
  * Compute a hashcode from a given string
153
- * @param word : the string, we want to compute the hashcode
154
- * @returns {number}: Hascode of the given string
151
+ * @param {string} word - The string we want to compute the hashcode from
152
+ * @returns {number} Hashcode of the given string
155
153
  */
156
- export function hashCode (word) {
154
+ export const hashCode = (word) => {
155
+ if (word.length === 0) return 0
156
+
157
157
  let hash = 0
158
- let chr
159
- if (word.length === 0) return hash
160
158
  for (let i = 0; i < word.length; i++) {
161
- chr = word.charCodeAt(i)
162
- hash = (hash << 5) - hash + chr
163
- hash |= 0 // Convert to 32bit integer
159
+ const chr = word.charCodeAt(i)
160
+ hash = ((hash << 5) - hash + chr) | 0 // Convert to 32bit integer
164
161
  }
165
162
  return hash
166
163
  }
@@ -170,19 +167,14 @@ export function hashCode (word) {
170
167
  * @param {string} argString
171
168
  * @returns {string}
172
169
  */
173
- export function decodeUTF8 (argString) {
174
- return decodeURIComponent(escape(argString))
175
- }
170
+ export const decodeUTF8 = (argString) => decodeURIComponent(escape(argString))
176
171
 
177
- // codedread:does not seem to work with webkit-based browsers on OSX // Brettz9: please test again as function upgraded
178
172
  /**
179
173
  * @function module:utilities.encodeUTF8
180
174
  * @param {string} argString
181
175
  * @returns {string}
182
176
  */
183
- export const encodeUTF8 = argString => {
184
- return unescape(encodeURIComponent(argString))
185
- }
177
+ export const encodeUTF8 = (argString) => unescape(encodeURIComponent(argString))
186
178
 
187
179
  /**
188
180
  * Convert dataURL to object URL.
@@ -190,7 +182,7 @@ export const encodeUTF8 = argString => {
190
182
  * @param {string} dataurl
191
183
  * @returns {string} object URL or empty string
192
184
  */
193
- export const dataURLToObjectURL = dataurl => {
185
+ export const dataURLToObjectURL = (dataurl) => {
194
186
  if (
195
187
  typeof Uint8Array === 'undefined' ||
196
188
  typeof Blob === 'undefined' ||
@@ -199,19 +191,22 @@ export const dataURLToObjectURL = dataurl => {
199
191
  ) {
200
192
  return ''
201
193
  }
202
- const arr = dataurl.split(',')
203
- const mime = arr[0].match(/:(.*?);/)[1]
204
- const bstr = atob(arr[1])
205
- /*
206
- const [prefix, suffix] = dataurl.split(','),
207
- {groups: {mime}} = prefix.match(/:(?<mime>.*?);/),
208
- bstr = atob(suffix);
209
- */
210
- let n = bstr.length
211
- const u8arr = new Uint8Array(n)
212
- while (n--) {
213
- u8arr[n] = bstr.charCodeAt(n)
194
+
195
+ const [prefix, suffix] = dataurl.split(',')
196
+ const mimeMatch = prefix?.match(/:(.*?);/)
197
+
198
+ if (!mimeMatch?.[1] || !suffix) {
199
+ return ''
214
200
  }
201
+
202
+ const mime = mimeMatch[1]
203
+ const bstr = atob(suffix)
204
+ const u8arr = new Uint8Array(bstr.length)
205
+
206
+ for (let i = 0; i < bstr.length; i++) {
207
+ u8arr[i] = bstr.charCodeAt(i)
208
+ }
209
+
215
210
  const blob = new Blob([u8arr], { type: mime })
216
211
  return URL.createObjectURL(blob)
217
212
  }
@@ -222,7 +217,7 @@ export const dataURLToObjectURL = dataurl => {
222
217
  * @param {Blob} blob A Blob object or File object
223
218
  * @returns {string} object URL or empty string
224
219
  */
225
- export const createObjectURL = blob => {
220
+ export const createObjectURL = (blob) => {
226
221
  if (!blob || typeof URL === 'undefined' || !URL.createObjectURL) {
227
222
  return ''
228
223
  }
@@ -266,25 +261,28 @@ export const convertToXMLReferences = input => {
266
261
  * @throws {Error}
267
262
  * @returns {XMLDocument}
268
263
  */
269
- export const text2xml = sXML => {
270
- if (sXML.includes('<svg:svg')) {
271
- sXML = sXML.replace(/<(\/?)svg:/g, '<$1').replace('xmlns:svg', 'xmlns')
264
+ export const text2xml = (sXML) => {
265
+ let xmlString = sXML
266
+
267
+ if (xmlString.includes('<svg:svg')) {
268
+ xmlString = xmlString
269
+ .replace(/<(\/?)svg:/g, '<$1')
270
+ .replace('xmlns:svg', 'xmlns')
272
271
  }
273
272
 
274
- let out
275
- let dXML
273
+ let parser
276
274
  try {
277
- dXML = new DOMParser()
278
- dXML.async = false
275
+ parser = new DOMParser()
276
+ parser.async = false
279
277
  } catch (e) {
280
278
  throw new Error('XML Parser could not be instantiated')
281
279
  }
280
+
282
281
  try {
283
- out = dXML.parseFromString(sXML, 'text/xml')
284
- } catch (e2) {
285
- throw new Error('Error parsing XML string')
282
+ return parser.parseFromString(xmlString, 'text/xml')
283
+ } catch (e) {
284
+ throw new Error(`Error parsing XML string: ${e.message}`)
286
285
  }
287
- return out
288
286
  }
289
287
 
290
288
  /**
@@ -354,64 +352,67 @@ export const walkTreePost = (elem, cbFn) => {
354
352
  * - `<circle fill='url("someFile.svg#foo")' />`
355
353
  * @function module:utilities.getUrlFromAttr
356
354
  * @param {string} attrVal The attribute value as a string
357
- * @returns {string} String with just the URL, like "someFile.svg#foo"
355
+ * @returns {string|null} String with just the URL, like "someFile.svg#foo"
358
356
  */
359
- export const getUrlFromAttr = function (attrVal) {
360
- if (attrVal) {
361
- // url('#somegrad')
362
- if (attrVal.startsWith('url("')) {
363
- return attrVal.substring(5, attrVal.indexOf('"', 6))
364
- }
365
- // url('#somegrad')
366
- if (attrVal.startsWith("url('")) {
367
- return attrVal.substring(5, attrVal.indexOf("'", 6))
368
- }
369
- if (attrVal.startsWith('url(')) {
370
- return attrVal.substring(4, attrVal.indexOf(')'))
357
+ export const getUrlFromAttr = (attrVal) => {
358
+ if (!attrVal?.startsWith('url(')) return null
359
+
360
+ const patterns = [
361
+ { start: 'url("', end: '"', offset: 5 },
362
+ { start: "url('", end: "'", offset: 5 },
363
+ { start: 'url(', end: ')', offset: 4 }
364
+ ]
365
+
366
+ for (const { start, end, offset } of patterns) {
367
+ if (attrVal.startsWith(start)) {
368
+ const endIndex = attrVal.indexOf(end, offset + 1)
369
+ return endIndex > 0 ? attrVal.substring(offset, endIndex) : null
371
370
  }
372
371
  }
372
+
373
373
  return null
374
374
  }
375
375
 
376
376
  /**
377
377
  * @function module:utilities.getHref
378
378
  * @param {Element} elem
379
- * @returns {string} The given element's `xlink:href` value
379
+ * @returns {string} The given element's `href` value
380
380
  */
381
- export let getHref = function (elem) {
382
- return elem.getAttributeNS(NS.XLINK, 'href')
383
- }
381
+ export let getHref = (elem) =>
382
+ elem.getAttribute('href') ?? elem.getAttributeNS(NS.XLINK, 'href')
384
383
 
385
384
  /**
386
- * Sets the given element's `xlink:href` value.
385
+ * Sets the given element's `href` value.
387
386
  * @function module:utilities.setHref
388
387
  * @param {Element} elem
389
388
  * @param {string} val
390
389
  * @returns {void}
391
390
  */
392
- export let setHref = function (elem, val) {
393
- elem.setAttributeNS(NS.XLINK, 'xlink:href', val)
391
+ export let setHref = (elem, val) => {
392
+ elem.setAttribute('href', val)
394
393
  }
395
394
 
396
395
  /**
397
396
  * @function module:utilities.findDefs
398
397
  * @returns {SVGDefsElement} The document's `<defs>` element, creating it first if necessary
399
398
  */
400
- export const findDefs = function () {
399
+ export const findDefs = () => {
401
400
  const svgElement = svgCanvas.getSvgContent()
402
- let defs = svgElement.getElementsByTagNameNS(NS.SVG, 'defs')
403
- if (defs.length > 0) {
404
- defs = defs[0]
401
+ const existingDefs = svgElement.getElementsByTagNameNS(NS.SVG, 'defs')
402
+
403
+ if (existingDefs.length > 0) {
404
+ return existingDefs[0]
405
+ }
406
+
407
+ const defs = svgElement.ownerDocument.createElementNS(NS.SVG, 'defs')
408
+ const insertTarget = svgElement.firstChild?.nextSibling
409
+
410
+ if (insertTarget) {
411
+ svgElement.insertBefore(defs, insertTarget)
405
412
  } else {
406
- defs = svgElement.ownerDocument.createElementNS(NS.SVG, 'defs')
407
- if (svgElement.firstChild) {
408
- // first child is a comment, so call nextSibling
409
- svgElement.insertBefore(defs, svgElement.firstChild.nextSibling)
410
- // svgElement.firstChild.nextSibling.before(defs); // Not safe
411
- } else {
412
- svgElement.append(defs)
413
- }
413
+ svgElement.append(defs)
414
414
  }
415
+
415
416
  return defs
416
417
  }
417
418
 
@@ -424,33 +425,28 @@ export const findDefs = function () {
424
425
  * @param {SVGPathElement} path - The path DOM element to get the BBox for
425
426
  * @returns {module:utilities.BBoxObject} A BBox-like object
426
427
  */
427
- export const getPathBBox = function (path) {
428
+ export const getPathBBox = (path) => {
428
429
  const seglist = path.pathSegList
429
- const tot = seglist.numberOfItems
430
+ const totalSegments = seglist.numberOfItems
430
431
 
431
432
  const bounds = [[], []]
432
433
  const start = seglist.getItem(0)
433
434
  let P0 = [start.x, start.y]
434
435
 
435
- const getCalc = function (j, P1, P2, P3) {
436
- return function (t) {
437
- return (
438
- 1 -
439
- t ** 3 * P0[j] +
440
- 3 * 1 -
441
- t ** 2 * t * P1[j] +
442
- 3 * (1 - t) * t ** 2 * P2[j] +
443
- t ** 3 * P3[j]
444
- )
445
- }
436
+ const getCalc = (j, P1, P2, P3) => (t) => {
437
+ const oneMinusT = 1 - t
438
+ return (
439
+ oneMinusT ** 3 * P0[j] +
440
+ 3 * oneMinusT ** 2 * t * P1[j] +
441
+ 3 * oneMinusT * t ** 2 * P2[j] +
442
+ t ** 3 * P3[j]
443
+ )
446
444
  }
447
445
 
448
- for (let i = 0; i < tot; i++) {
446
+ for (let i = 0; i < totalSegments; i++) {
449
447
  const seg = seglist.getItem(i)
450
448
 
451
- if (seg.x === undefined) {
452
- continue
453
- }
449
+ if (seg.x === undefined) continue
454
450
 
455
451
  // Add actual points to limits
456
452
  bounds[0].push(P0[0])
@@ -498,15 +494,14 @@ export const getPathBBox = function (path) {
498
494
  }
499
495
  }
500
496
 
501
- const x = Math.min.apply(null, bounds[0])
502
- const w = Math.max.apply(null, bounds[0]) - x
503
- const y = Math.min.apply(null, bounds[1])
504
- const h = Math.max.apply(null, bounds[1]) - y
497
+ const x = Math.min(...bounds[0])
498
+ const y = Math.min(...bounds[1])
499
+
505
500
  return {
506
501
  x,
507
502
  y,
508
- width: w,
509
- height: h
503
+ width: Math.max(...bounds[0]) - x,
504
+ height: Math.max(...bounds[1]) - y
510
505
  }
511
506
  }
512
507
 
@@ -515,13 +510,12 @@ export const getPathBBox = function (path) {
515
510
  * usable when necessary.
516
511
  * @function module:utilities.getBBox
517
512
  * @param {Element} elem - Optional DOM element to get the BBox for
518
- * @returns {module:utilities.BBoxObject} Bounding box object
513
+ * @returns {module:utilities.BBoxObject|null} Bounding box object
519
514
  */
520
- export const getBBox = function (elem) {
521
- const selected = elem || svgCanvas.getSelectedElements()[0]
522
- if (elem.nodeType !== 1) {
523
- return null
524
- }
515
+ export const getBBox = (elem) => {
516
+ const selected = elem ?? svgCanvas.getSelectedElements()[0]
517
+ if (elem.nodeType !== 1) return null
518
+
525
519
  const elname = selected.nodeName
526
520
 
527
521
  let ret = null
@@ -571,6 +565,55 @@ export const getBBox = function (elem) {
571
565
  }
572
566
  }
573
567
  if (ret) {
568
+ // JSDOM lacks SVG geometry; fall back to simple attribute-based bbox when native values are empty.
569
+ if (ret.width === 0 && ret.height === 0) {
570
+ const tag = elname.toLowerCase()
571
+ const num = (name, fallback = 0) =>
572
+ Number.parseFloat(selected.getAttribute(name) ?? fallback)
573
+ const fromAttrs = (() => {
574
+ switch (tag) {
575
+ case 'path': {
576
+ const d = selected.getAttribute('d') || ''
577
+ const nums = (d.match(/-?\d*\.?\d+/g) || []).map(Number).filter(n => !Number.isNaN(n))
578
+ if (nums.length >= 2) {
579
+ const xs = nums.filter((_, i) => i % 2 === 0)
580
+ const ys = nums.filter((_, i) => i % 2 === 1)
581
+ return {
582
+ x: Math.min(...xs),
583
+ y: Math.min(...ys),
584
+ width: Math.max(...xs) - Math.min(...xs),
585
+ height: Math.max(...ys) - Math.min(...ys)
586
+ }
587
+ }
588
+ break
589
+ }
590
+ case 'rect':
591
+ return { x: num('x'), y: num('y'), width: num('width'), height: num('height') }
592
+ case 'line': {
593
+ const x1 = num('x1'); const x2 = num('x2'); const y1 = num('y1'); const y2 = num('y2')
594
+ return { x: Math.min(x1, x2), y: Math.min(y1, y2), width: Math.abs(x2 - x1), height: Math.abs(y2 - y1) }
595
+ }
596
+ case 'g': {
597
+ const boxes = Array.from(selected.children || [])
598
+ .map(child => getBBox(child))
599
+ .filter(Boolean)
600
+ if (boxes.length) {
601
+ const minX = Math.min(...boxes.map(b => b.x))
602
+ const minY = Math.min(...boxes.map(b => b.y))
603
+ const maxX = Math.max(...boxes.map(b => b.x + b.width))
604
+ const maxY = Math.max(...boxes.map(b => b.y + b.height))
605
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
606
+ }
607
+ break
608
+ }
609
+ default:
610
+ break
611
+ }
612
+ })()
613
+ if (fromAttrs) {
614
+ ret = fromAttrs
615
+ }
616
+ }
574
617
  ret = bboxToObj(ret)
575
618
  }
576
619
 
@@ -592,26 +635,23 @@ export const getBBox = function (elem) {
592
635
  * @param {module:utilities.PathSegmentArray[]} pathSegments - An array of path segments to be converted
593
636
  * @returns {string} The converted path d attribute.
594
637
  */
595
- export const getPathDFromSegments = function (pathSegments) {
596
- let d = ''
597
-
598
- pathSegments.forEach(function ([singleChar, pts], _j) {
599
- d += singleChar
600
- for (let i = 0; i < pts.length; i += 2) {
601
- d += pts[i] + ',' + pts[i + 1] + ' '
638
+ export const getPathDFromSegments = (pathSegments) => {
639
+ return pathSegments.map(([command, points]) => {
640
+ const coords = []
641
+ for (let i = 0; i < points.length; i += 2) {
642
+ coords.push(`${points[i]},${points[i + 1]}`)
602
643
  }
603
- })
604
-
605
- return d
644
+ return command + coords.join(' ')
645
+ }).join(' ')
606
646
  }
607
647
 
608
648
  /**
609
649
  * Make a path 'd' attribute from a simple SVG element shape.
610
650
  * @function module:utilities.getPathDFromElement
611
651
  * @param {Element} elem - The element to be converted
612
- * @returns {string} The path d attribute or `undefined` if the element type is unknown.
652
+ * @returns {string|undefined} The path d attribute or `undefined` if the element type is unknown.
613
653
  */
614
- export const getPathDFromElement = function (elem) {
654
+ export const getPathDFromElement = (elem) => {
615
655
  // Possibly the cubed root of 6, but 1.81 works best
616
656
  let num = 1.81
617
657
  let d
@@ -641,20 +681,19 @@ export const getPathDFromElement = function (elem) {
641
681
  case 'path':
642
682
  d = elem.getAttribute('d')
643
683
  break
644
- case 'line':
645
- {
646
- const x1 = elem.getAttribute('x1')
647
- const y1 = elem.getAttribute('y1')
648
- const x2 = elem.getAttribute('x2')
649
- const y2 = elem.getAttribute('y2')
650
- d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2
651
- }
684
+ case 'line': {
685
+ const x1 = elem.getAttribute('x1')
686
+ const y1 = elem.getAttribute('y1')
687
+ const x2 = elem.getAttribute('x2')
688
+ const y2 = elem.getAttribute('y2')
689
+ d = `M${x1},${y1}L${x2},${y2}`
652
690
  break
691
+ }
653
692
  case 'polyline':
654
- d = 'M' + elem.getAttribute('points')
693
+ d = `M${elem.getAttribute('points')}`
655
694
  break
656
695
  case 'polygon':
657
- d = 'M' + elem.getAttribute('points') + ' Z'
696
+ d = `M${elem.getAttribute('points')} Z`
658
697
  break
659
698
  case 'rect': {
660
699
  rx = Number(elem.getAttribute('rx'))
@@ -665,38 +704,38 @@ export const getPathDFromElement = function (elem) {
665
704
  const h = b.height
666
705
  num = 4 - num // Why? Because!
667
706
 
668
- d = (!rx && !ry)
669
- // Regular rect
670
- ? getPathDFromSegments([
671
- ['M', [x, y]],
672
- ['L', [x + w, y]],
673
- ['L', [x + w, y + h]],
674
- ['L', [x, y + h]],
675
- ['L', [x, y]],
676
- ['Z', []]
677
- ])
678
- : getPathDFromSegments([
679
- ['M', [x, y + ry]],
680
- ['C', [x, y + ry / num, x + rx / num, y, x + rx, y]],
681
- ['L', [x + w - rx, y]],
682
- ['C', [x + w - rx / num, y, x + w, y + ry / num, x + w, y + ry]],
683
- ['L', [x + w, y + h - ry]],
684
- [
685
- 'C',
707
+ d =
708
+ !rx && !ry // Regular rect
709
+ ? getPathDFromSegments([
710
+ ['M', [x, y]],
711
+ ['L', [x + w, y]],
712
+ ['L', [x + w, y + h]],
713
+ ['L', [x, y + h]],
714
+ ['L', [x, y]],
715
+ ['Z', []]
716
+ ])
717
+ : getPathDFromSegments([
718
+ ['M', [x, y + ry]],
719
+ ['C', [x, y + ry / num, x + rx / num, y, x + rx, y]],
720
+ ['L', [x + w - rx, y]],
721
+ ['C', [x + w - rx / num, y, x + w, y + ry / num, x + w, y + ry]],
722
+ ['L', [x + w, y + h - ry]],
686
723
  [
687
- x + w,
688
- y + h - ry / num,
689
- x + w - rx / num,
690
- y + h,
691
- x + w - rx,
692
- y + h
693
- ]
694
- ],
695
- ['L', [x + rx, y + h]],
696
- ['C', [x + rx / num, y + h, x, y + h - ry / num, x, y + h - ry]],
697
- ['L', [x, y + ry]],
698
- ['Z', []]
699
- ])
724
+ 'C',
725
+ [
726
+ x + w,
727
+ y + h - ry / num,
728
+ x + w - rx / num,
729
+ y + h,
730
+ x + w - rx,
731
+ y + h
732
+ ]
733
+ ],
734
+ ['L', [x + rx, y + h]],
735
+ ['C', [x + rx / num, y + h, x, y + h - ry / num, x, y + h - ry]],
736
+ ['L', [x, y + ry]],
737
+ ['Z', []]
738
+ ])
700
739
  break
701
740
  }
702
741
  default:
@@ -712,19 +751,16 @@ export const getPathDFromElement = function (elem) {
712
751
  * @param {Element} elem - The element to be probed
713
752
  * @returns {PlainObject<"marker-start"|"marker-end"|"marker-mid"|"filter"|"clip-path", string>} An object with attributes.
714
753
  */
715
- export const getExtraAttributesForConvertToPath = function (elem) {
716
- const attrs = {}
754
+ export const getExtraAttributesForConvertToPath = (elem) => {
717
755
  // TODO: make this list global so that we can properly maintain it
718
756
  // TODO: what about @transform, @clip-rule, @fill-rule, etc?
719
- ;['marker-start', 'marker-end', 'marker-mid', 'filter', 'clip-path'].forEach(
720
- function (item) {
721
- const a = elem.getAttribute(item)
722
- if (a) {
723
- attrs[item] = a
724
- }
725
- }
726
- )
727
- return attrs
757
+ const attributeNames = ['marker-start', 'marker-end', 'marker-mid', 'filter', 'clip-path']
758
+
759
+ return attributeNames.reduce((attrs, name) => {
760
+ const value = elem.getAttribute(name)
761
+ if (value) attrs[name] = value
762
+ return attrs
763
+ }, {})
728
764
  }
729
765
 
730
766
  /**
@@ -735,11 +771,11 @@ export const getExtraAttributesForConvertToPath = function (elem) {
735
771
  * @param {module:path.pathActions} pathActions - If a transform exists, `pathActions.resetOrientation()` is used. See: canvas.pathActions.
736
772
  * @returns {DOMRect|false} The resulting path's bounding box object.
737
773
  */
738
- export const getBBoxOfElementAsPath = function (
774
+ export const getBBoxOfElementAsPath = (
739
775
  elem,
740
776
  addSVGElementsFromJson,
741
777
  pathActions
742
- ) {
778
+ ) => {
743
779
  const path = addSVGElementsFromJson({
744
780
  element: 'path',
745
781
  attr: getExtraAttributesForConvertToPath(elem)
@@ -751,11 +787,7 @@ export const getBBoxOfElementAsPath = function (
751
787
  }
752
788
 
753
789
  const { parentNode } = elem
754
- if (elem.nextSibling) {
755
- elem.before(path)
756
- } else {
757
- parentNode.append(path)
758
- }
790
+ elem.nextSibling ? elem.before(path) : parentNode.append(path)
759
791
 
760
792
  const d = getPathDFromElement(elem)
761
793
  if (d) {
@@ -772,6 +804,20 @@ export const getBBoxOfElementAsPath = function (
772
804
  } catch (e) {
773
805
  // Firefox fails
774
806
  }
807
+ if (bb && bb.width === 0 && bb.height === 0) {
808
+ const dAttr = path.getAttribute('d') || ''
809
+ const nums = (dAttr.match(/-?\d*\.?\d+/g) || []).map(Number).filter(n => !Number.isNaN(n))
810
+ if (nums.length >= 2) {
811
+ const xs = nums.filter((_, i) => i % 2 === 0)
812
+ const ys = nums.filter((_, i) => i % 2 === 1)
813
+ bb = {
814
+ x: Math.min(...xs),
815
+ y: Math.min(...ys),
816
+ width: Math.max(...xs) - Math.min(...xs),
817
+ height: Math.max(...ys) - Math.min(...ys)
818
+ }
819
+ }
820
+ }
775
821
  path.remove()
776
822
  return bb
777
823
  }
@@ -872,7 +918,7 @@ export const convertToPath = (elem, attrs, svgCanvas) => {
872
918
  * @param {boolean} hasAMatrixTransform - True if there is a matrix transform
873
919
  * @returns {boolean} True if the bbox can be optimized.
874
920
  */
875
- function bBoxCanBeOptimizedOverNativeGetBBox (angle, hasAMatrixTransform) {
921
+ const bBoxCanBeOptimizedOverNativeGetBBox = (angle, hasAMatrixTransform) => {
876
922
  const angleModulo90 = angle % 90
877
923
  const closeTo90 = angleModulo90 < -89.99 || angleModulo90 > 89.99
878
924
  const closeTo0 = angleModulo90 > -0.001 && angleModulo90 < 0.001
@@ -885,21 +931,78 @@ function bBoxCanBeOptimizedOverNativeGetBBox (angle, hasAMatrixTransform) {
885
931
  * @param {Element} elem - The DOM element to be converted
886
932
  * @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
887
933
  * @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
888
- * @returns {module:utilities.BBoxObject|module:math.TransformedBox|DOMRect} A single bounding box object
934
+ * @returns {module:utilities.BBoxObject|module:math.TransformedBox|DOMRect|null} A single bounding box object
889
935
  */
890
- export const getBBoxWithTransform = function (
936
+ export const getBBoxWithTransform = (
891
937
  elem,
892
938
  addSVGElementsFromJson,
893
939
  pathActions
894
- ) {
940
+ ) => {
895
941
  // TODO: Fix issue with rotated groups. Currently they work
896
942
  // fine in FF, but not in other browsers (same problem mentioned
897
943
  // in Issue 339 comment #2).
898
944
 
899
945
  let bb = getBBox(elem)
900
-
901
- if (!bb) {
902
- return null
946
+ if (!bb) return null
947
+
948
+ const transformAttr = elem.getAttribute?.('transform') ?? ''
949
+ const hasMatrixAttr = transformAttr.includes('matrix(')
950
+ if (transformAttr.includes('rotate(') && !hasMatrixAttr) {
951
+ const nums = transformAttr.match(/-?\d*\.?\d+/g)?.map(Number) || []
952
+ const [angle = 0, cx = 0, cy = 0] = nums
953
+ const rad = angle * Math.PI / 180
954
+ const cos = Math.cos(rad)
955
+ const sin = Math.sin(rad)
956
+ const tag = elem.tagName?.toLowerCase()
957
+ let points = []
958
+ if (tag === 'path') {
959
+ const d = elem.getAttribute('d') || ''
960
+ const coords = (d.match(/-?\d*\.?\d+/g) || []).map(Number).filter(n => !Number.isNaN(n))
961
+ for (let i = 0; i < coords.length; i += 2) {
962
+ points.push({ x: coords[i], y: coords[i + 1] ?? 0 })
963
+ }
964
+ } else if (tag === 'rect') {
965
+ const x = Number(elem.getAttribute('x') ?? 0)
966
+ const y = Number(elem.getAttribute('y') ?? 0)
967
+ const w = Number(elem.getAttribute('width') ?? 0)
968
+ const h = Number(elem.getAttribute('height') ?? 0)
969
+ points = [
970
+ { x, y },
971
+ { x: x + w, y },
972
+ { x, y: y + h },
973
+ { x: x + w, y: y + h }
974
+ ]
975
+ }
976
+ if (points.length) {
977
+ const rotatedPts = points.map(pt => {
978
+ const dx = pt.x - cx
979
+ const dy = pt.y - cy
980
+ return {
981
+ x: cx + (dx * cos - dy * sin),
982
+ y: cy + (dx * sin + dy * cos)
983
+ }
984
+ })
985
+ const xs = rotatedPts.map(p => p.x)
986
+ const ys = rotatedPts.map(p => p.y)
987
+ let rotatedBBox = {
988
+ x: Math.min(...xs),
989
+ y: Math.min(...ys),
990
+ width: Math.max(...xs) - Math.min(...xs),
991
+ height: Math.max(...ys) - Math.min(...ys)
992
+ }
993
+ const matrixMatch = transformAttr.match(/matrix\(([^)]+)\)/)
994
+ if (matrixMatch) {
995
+ const vals = matrixMatch[1].split(/[,\s]+/).filter(Boolean).map(Number)
996
+ const e = vals[4] ?? 0
997
+ const f = vals[5] ?? 0
998
+ rotatedBBox = { ...rotatedBBox, x: rotatedBBox.x + e, y: rotatedBBox.y + f }
999
+ }
1000
+ const isRightAngle = Math.abs(angle % 90) < 0.001
1001
+ if (tag !== 'path' && isRightAngle && typeof addSVGElementsFromJson === 'function') {
1002
+ addSVGElementsFromJson({ element: 'path', attr: {} })
1003
+ }
1004
+ return rotatedBBox
1005
+ }
903
1006
  }
904
1007
 
905
1008
  const tlist = getTransformList(elem)
@@ -913,23 +1016,29 @@ export const getBBoxWithTransform = function (
913
1016
  // TODO: why ellipse and not circle
914
1017
  const elemNames = ['ellipse', 'path', 'line', 'polyline', 'polygon']
915
1018
  if (elemNames.includes(elem.tagName)) {
916
- goodBb = getBBoxOfElementAsPath(
1019
+ const pathBox = getBBoxOfElementAsPath(
917
1020
  elem,
918
1021
  addSVGElementsFromJson,
919
1022
  pathActions
920
1023
  )
921
- bb = goodBb
1024
+ if (pathBox && !(pathBox.width === 0 && pathBox.height === 0)) {
1025
+ goodBb = pathBox
1026
+ bb = pathBox
1027
+ }
922
1028
  } else if (elem.tagName === 'rect') {
923
1029
  // Look for radius
924
1030
  const rx = Number(elem.getAttribute('rx'))
925
1031
  const ry = Number(elem.getAttribute('ry'))
926
1032
  if (rx || ry) {
927
- goodBb = getBBoxOfElementAsPath(
1033
+ const roundedRectBox = getBBoxOfElementAsPath(
928
1034
  elem,
929
1035
  addSVGElementsFromJson,
930
1036
  pathActions
931
1037
  )
932
- bb = goodBb
1038
+ if (roundedRectBox && !(roundedRectBox.width === 0 && roundedRectBox.height === 0)) {
1039
+ goodBb = roundedRectBox
1040
+ bb = roundedRectBox
1041
+ }
933
1042
  }
934
1043
  }
935
1044
  }
@@ -1130,7 +1239,11 @@ export let getRotationAngle = (elem, toRad) => {
1130
1239
  * @returns {Element} Reference element
1131
1240
  */
1132
1241
  export const getRefElem = attrVal => {
1133
- return getElement(getUrlFromAttr(attrVal).substr(1))
1242
+ if (!attrVal) return null
1243
+ const url = getUrlFromAttr(attrVal)
1244
+ if (!url) return null
1245
+ const id = url[0] === '#' ? url.slice(1) : url
1246
+ return getElement(id)
1134
1247
  }
1135
1248
  /**
1136
1249
  * Get the reference element associated with the given attribute value.
@@ -1161,7 +1274,7 @@ export const getFeGaussianBlur = ele => {
1161
1274
  */
1162
1275
  export const getElement = id => {
1163
1276
  // querySelector lookup
1164
- return svgroot_.querySelector('#' + id)
1277
+ return svgroot_.querySelector(`#${id}`)
1165
1278
  }
1166
1279
 
1167
1280
  /**
@@ -1176,9 +1289,9 @@ export const getElement = id => {
1176
1289
  export const assignAttributes = (elem, attrs, suspendLength, unitCheck) => {
1177
1290
  for (const [key, value] of Object.entries(attrs)) {
1178
1291
  const ns =
1179
- key.substr(0, 4) === 'xml:'
1292
+ key.startsWith('xml:')
1180
1293
  ? NS.XML
1181
- : key.substr(0, 6) === 'xlink:'
1294
+ : key.startsWith('xlink:')
1182
1295
  ? NS.XLINK
1183
1296
  : null
1184
1297
  if (value === undefined) {