@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/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,22 +352,24 @@ 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
 
@@ -378,10 +378,8 @@ export const getUrlFromAttr = function (attrVal) {
378
378
  * @param {Element} elem
379
379
  * @returns {string} The given element's `href` value
380
380
  */
381
- export let getHref = function (elem) {
382
- // Prefer 'href', fallback to 'xlink:href'
383
- return elem.getAttribute('href') || elem.getAttributeNS(NS.XLINK, 'href')
384
- }
381
+ export let getHref = (elem) =>
382
+ elem.getAttribute('href') ?? elem.getAttributeNS(NS.XLINK, 'href')
385
383
 
386
384
  /**
387
385
  * Sets the given element's `href` value.
@@ -390,7 +388,7 @@ export let getHref = function (elem) {
390
388
  * @param {string} val
391
389
  * @returns {void}
392
390
  */
393
- export let setHref = function (elem, val) {
391
+ export let setHref = (elem, val) => {
394
392
  elem.setAttribute('href', val)
395
393
  }
396
394
 
@@ -398,21 +396,23 @@ export let setHref = function (elem, val) {
398
396
  * @function module:utilities.findDefs
399
397
  * @returns {SVGDefsElement} The document's `<defs>` element, creating it first if necessary
400
398
  */
401
- export const findDefs = function () {
399
+ export const findDefs = () => {
402
400
  const svgElement = svgCanvas.getSvgContent()
403
- let defs = svgElement.getElementsByTagNameNS(NS.SVG, 'defs')
404
- if (defs.length > 0) {
405
- 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)
406
412
  } else {
407
- defs = svgElement.ownerDocument.createElementNS(NS.SVG, 'defs')
408
- if (svgElement.firstChild) {
409
- // first child is a comment, so call nextSibling
410
- svgElement.insertBefore(defs, svgElement.firstChild.nextSibling)
411
- // svgElement.firstChild.nextSibling.before(defs); // Not safe
412
- } else {
413
- svgElement.append(defs)
414
- }
413
+ svgElement.append(defs)
415
414
  }
415
+
416
416
  return defs
417
417
  }
418
418
 
@@ -425,33 +425,28 @@ export const findDefs = function () {
425
425
  * @param {SVGPathElement} path - The path DOM element to get the BBox for
426
426
  * @returns {module:utilities.BBoxObject} A BBox-like object
427
427
  */
428
- export const getPathBBox = function (path) {
428
+ export const getPathBBox = (path) => {
429
429
  const seglist = path.pathSegList
430
- const tot = seglist.numberOfItems
430
+ const totalSegments = seglist.numberOfItems
431
431
 
432
432
  const bounds = [[], []]
433
433
  const start = seglist.getItem(0)
434
434
  let P0 = [start.x, start.y]
435
435
 
436
- const getCalc = function (j, P1, P2, P3) {
437
- return function (t) {
438
- return (
439
- 1 -
440
- t ** 3 * P0[j] +
441
- 3 * 1 -
442
- t ** 2 * t * P1[j] +
443
- 3 * (1 - t) * t ** 2 * P2[j] +
444
- t ** 3 * P3[j]
445
- )
446
- }
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
+ )
447
444
  }
448
445
 
449
- for (let i = 0; i < tot; i++) {
446
+ for (let i = 0; i < totalSegments; i++) {
450
447
  const seg = seglist.getItem(i)
451
448
 
452
- if (seg.x === undefined) {
453
- continue
454
- }
449
+ if (seg.x === undefined) continue
455
450
 
456
451
  // Add actual points to limits
457
452
  bounds[0].push(P0[0])
@@ -499,15 +494,14 @@ export const getPathBBox = function (path) {
499
494
  }
500
495
  }
501
496
 
502
- const x = Math.min.apply(null, bounds[0])
503
- const w = Math.max.apply(null, bounds[0]) - x
504
- const y = Math.min.apply(null, bounds[1])
505
- const h = Math.max.apply(null, bounds[1]) - y
497
+ const x = Math.min(...bounds[0])
498
+ const y = Math.min(...bounds[1])
499
+
506
500
  return {
507
501
  x,
508
502
  y,
509
- width: w,
510
- height: h
503
+ width: Math.max(...bounds[0]) - x,
504
+ height: Math.max(...bounds[1]) - y
511
505
  }
512
506
  }
513
507
 
@@ -516,13 +510,12 @@ export const getPathBBox = function (path) {
516
510
  * usable when necessary.
517
511
  * @function module:utilities.getBBox
518
512
  * @param {Element} elem - Optional DOM element to get the BBox for
519
- * @returns {module:utilities.BBoxObject} Bounding box object
513
+ * @returns {module:utilities.BBoxObject|null} Bounding box object
520
514
  */
521
- export const getBBox = function (elem) {
522
- const selected = elem || svgCanvas.getSelectedElements()[0]
523
- if (elem.nodeType !== 1) {
524
- return null
525
- }
515
+ export const getBBox = (elem) => {
516
+ const selected = elem ?? svgCanvas.getSelectedElements()[0]
517
+ if (elem.nodeType !== 1) return null
518
+
526
519
  const elname = selected.nodeName
527
520
 
528
521
  let ret = null
@@ -572,6 +565,55 @@ export const getBBox = function (elem) {
572
565
  }
573
566
  }
574
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
+ }
575
617
  ret = bboxToObj(ret)
576
618
  }
577
619
 
@@ -593,26 +635,23 @@ export const getBBox = function (elem) {
593
635
  * @param {module:utilities.PathSegmentArray[]} pathSegments - An array of path segments to be converted
594
636
  * @returns {string} The converted path d attribute.
595
637
  */
596
- export const getPathDFromSegments = function (pathSegments) {
597
- let d = ''
598
-
599
- pathSegments.forEach(function ([singleChar, pts], _j) {
600
- d += singleChar
601
- for (let i = 0; i < pts.length; i += 2) {
602
- 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]}`)
603
643
  }
604
- })
605
-
606
- return d
644
+ return command + coords.join(' ')
645
+ }).join(' ')
607
646
  }
608
647
 
609
648
  /**
610
649
  * Make a path 'd' attribute from a simple SVG element shape.
611
650
  * @function module:utilities.getPathDFromElement
612
651
  * @param {Element} elem - The element to be converted
613
- * @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.
614
653
  */
615
- export const getPathDFromElement = function (elem) {
654
+ export const getPathDFromElement = (elem) => {
616
655
  // Possibly the cubed root of 6, but 1.81 works best
617
656
  let num = 1.81
618
657
  let d
@@ -642,20 +681,19 @@ export const getPathDFromElement = function (elem) {
642
681
  case 'path':
643
682
  d = elem.getAttribute('d')
644
683
  break
645
- case 'line':
646
- {
647
- const x1 = elem.getAttribute('x1')
648
- const y1 = elem.getAttribute('y1')
649
- const x2 = elem.getAttribute('x2')
650
- const y2 = elem.getAttribute('y2')
651
- d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2
652
- }
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}`
653
690
  break
691
+ }
654
692
  case 'polyline':
655
- d = 'M' + elem.getAttribute('points')
693
+ d = `M${elem.getAttribute('points')}`
656
694
  break
657
695
  case 'polygon':
658
- d = 'M' + elem.getAttribute('points') + ' Z'
696
+ d = `M${elem.getAttribute('points')} Z`
659
697
  break
660
698
  case 'rect': {
661
699
  rx = Number(elem.getAttribute('rx'))
@@ -713,19 +751,16 @@ export const getPathDFromElement = function (elem) {
713
751
  * @param {Element} elem - The element to be probed
714
752
  * @returns {PlainObject<"marker-start"|"marker-end"|"marker-mid"|"filter"|"clip-path", string>} An object with attributes.
715
753
  */
716
- export const getExtraAttributesForConvertToPath = function (elem) {
717
- const attrs = {}
754
+ export const getExtraAttributesForConvertToPath = (elem) => {
718
755
  // TODO: make this list global so that we can properly maintain it
719
756
  // TODO: what about @transform, @clip-rule, @fill-rule, etc?
720
- ;['marker-start', 'marker-end', 'marker-mid', 'filter', 'clip-path'].forEach(
721
- function (item) {
722
- const a = elem.getAttribute(item)
723
- if (a) {
724
- attrs[item] = a
725
- }
726
- }
727
- )
728
- 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
+ }, {})
729
764
  }
730
765
 
731
766
  /**
@@ -736,11 +771,11 @@ export const getExtraAttributesForConvertToPath = function (elem) {
736
771
  * @param {module:path.pathActions} pathActions - If a transform exists, `pathActions.resetOrientation()` is used. See: canvas.pathActions.
737
772
  * @returns {DOMRect|false} The resulting path's bounding box object.
738
773
  */
739
- export const getBBoxOfElementAsPath = function (
774
+ export const getBBoxOfElementAsPath = (
740
775
  elem,
741
776
  addSVGElementsFromJson,
742
777
  pathActions
743
- ) {
778
+ ) => {
744
779
  const path = addSVGElementsFromJson({
745
780
  element: 'path',
746
781
  attr: getExtraAttributesForConvertToPath(elem)
@@ -752,11 +787,7 @@ export const getBBoxOfElementAsPath = function (
752
787
  }
753
788
 
754
789
  const { parentNode } = elem
755
- if (elem.nextSibling) {
756
- elem.before(path)
757
- } else {
758
- parentNode.append(path)
759
- }
790
+ elem.nextSibling ? elem.before(path) : parentNode.append(path)
760
791
 
761
792
  const d = getPathDFromElement(elem)
762
793
  if (d) {
@@ -773,6 +804,20 @@ export const getBBoxOfElementAsPath = function (
773
804
  } catch (e) {
774
805
  // Firefox fails
775
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
+ }
776
821
  path.remove()
777
822
  return bb
778
823
  }
@@ -873,7 +918,7 @@ export const convertToPath = (elem, attrs, svgCanvas) => {
873
918
  * @param {boolean} hasAMatrixTransform - True if there is a matrix transform
874
919
  * @returns {boolean} True if the bbox can be optimized.
875
920
  */
876
- function bBoxCanBeOptimizedOverNativeGetBBox (angle, hasAMatrixTransform) {
921
+ const bBoxCanBeOptimizedOverNativeGetBBox = (angle, hasAMatrixTransform) => {
877
922
  const angleModulo90 = angle % 90
878
923
  const closeTo90 = angleModulo90 < -89.99 || angleModulo90 > 89.99
879
924
  const closeTo0 = angleModulo90 > -0.001 && angleModulo90 < 0.001
@@ -886,21 +931,78 @@ function bBoxCanBeOptimizedOverNativeGetBBox (angle, hasAMatrixTransform) {
886
931
  * @param {Element} elem - The DOM element to be converted
887
932
  * @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
888
933
  * @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
889
- * @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
890
935
  */
891
- export const getBBoxWithTransform = function (
936
+ export const getBBoxWithTransform = (
892
937
  elem,
893
938
  addSVGElementsFromJson,
894
939
  pathActions
895
- ) {
940
+ ) => {
896
941
  // TODO: Fix issue with rotated groups. Currently they work
897
942
  // fine in FF, but not in other browsers (same problem mentioned
898
943
  // in Issue 339 comment #2).
899
944
 
900
945
  let bb = getBBox(elem)
901
-
902
- if (!bb) {
903
- 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
+ }
904
1006
  }
905
1007
 
906
1008
  const tlist = getTransformList(elem)
@@ -914,23 +1016,29 @@ export const getBBoxWithTransform = function (
914
1016
  // TODO: why ellipse and not circle
915
1017
  const elemNames = ['ellipse', 'path', 'line', 'polyline', 'polygon']
916
1018
  if (elemNames.includes(elem.tagName)) {
917
- goodBb = getBBoxOfElementAsPath(
1019
+ const pathBox = getBBoxOfElementAsPath(
918
1020
  elem,
919
1021
  addSVGElementsFromJson,
920
1022
  pathActions
921
1023
  )
922
- bb = goodBb
1024
+ if (pathBox && !(pathBox.width === 0 && pathBox.height === 0)) {
1025
+ goodBb = pathBox
1026
+ bb = pathBox
1027
+ }
923
1028
  } else if (elem.tagName === 'rect') {
924
1029
  // Look for radius
925
1030
  const rx = Number(elem.getAttribute('rx'))
926
1031
  const ry = Number(elem.getAttribute('ry'))
927
1032
  if (rx || ry) {
928
- goodBb = getBBoxOfElementAsPath(
1033
+ const roundedRectBox = getBBoxOfElementAsPath(
929
1034
  elem,
930
1035
  addSVGElementsFromJson,
931
1036
  pathActions
932
1037
  )
933
- bb = goodBb
1038
+ if (roundedRectBox && !(roundedRectBox.width === 0 && roundedRectBox.height === 0)) {
1039
+ goodBb = roundedRectBox
1040
+ bb = roundedRectBox
1041
+ }
934
1042
  }
935
1043
  }
936
1044
  }
@@ -1131,7 +1239,11 @@ export let getRotationAngle = (elem, toRad) => {
1131
1239
  * @returns {Element} Reference element
1132
1240
  */
1133
1241
  export const getRefElem = attrVal => {
1134
- 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)
1135
1247
  }
1136
1248
  /**
1137
1249
  * Get the reference element associated with the given attribute value.
@@ -1162,7 +1274,7 @@ export const getFeGaussianBlur = ele => {
1162
1274
  */
1163
1275
  export const getElement = id => {
1164
1276
  // querySelector lookup
1165
- return svgroot_.querySelector('#' + id)
1277
+ return svgroot_.querySelector(`#${id}`)
1166
1278
  }
1167
1279
 
1168
1280
  /**
@@ -1177,9 +1289,9 @@ export const getElement = id => {
1177
1289
  export const assignAttributes = (elem, attrs, suspendLength, unitCheck) => {
1178
1290
  for (const [key, value] of Object.entries(attrs)) {
1179
1291
  const ns =
1180
- key.substr(0, 4) === 'xml:'
1292
+ key.startsWith('xml:')
1181
1293
  ? NS.XML
1182
- : key.substr(0, 6) === 'xlink:'
1294
+ : key.startsWith('xlink:')
1183
1295
  ? NS.XLINK
1184
1296
  : null
1185
1297
  if (value === undefined) {