@svgedit/svgcanvas 7.1.4

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/svg-exec.js ADDED
@@ -0,0 +1,1289 @@
1
+ /**
2
+ * Tools for svg.
3
+ * @module svg
4
+ * @license MIT
5
+ * @copyright 2011 Jeff Schiller
6
+ */
7
+
8
+ import { jsPDF as JsPDF } from 'jspdf/dist/jspdf.es.min.js'
9
+ import 'svg2pdf.js/dist/svg2pdf.es.js'
10
+ import html2canvas from 'html2canvas'
11
+ import * as hstry from './history.js'
12
+ import {
13
+ text2xml,
14
+ cleanupElement,
15
+ findDefs,
16
+ getHref,
17
+ preventClickDefault,
18
+ toXml,
19
+ getStrokedBBoxDefaultVisible,
20
+ createObjectURL,
21
+ dataURLToObjectURL,
22
+ walkTree,
23
+ getBBox as utilsGetBBox,
24
+ hashCode
25
+ } from './utilities.js'
26
+ import { transformPoint, transformListToTransform } from './math.js'
27
+ import { convertUnit, shortFloat, convertToNum } from '../../src/common/units.js'
28
+ import { isGecko, isChrome, isWebkit } from '../../src/common/browser.js'
29
+ import * as pathModule from './path.js'
30
+ import { NS } from './namespaces.js'
31
+ import * as draw from './draw.js'
32
+ import { recalculateDimensions } from './recalculate.js'
33
+ import { getParents, getClosest } from '../../src/common/util.js'
34
+
35
+ const {
36
+ InsertElementCommand,
37
+ RemoveElementCommand,
38
+ ChangeElementCommand,
39
+ BatchCommand
40
+ } = hstry
41
+
42
+ let svgCanvas = null
43
+
44
+ /**
45
+ * @function module:svg-exec.init
46
+ * @param {module:svg-exec.SvgCanvas#init} svgContext
47
+ * @returns {void}
48
+ */
49
+ export const init = canvas => {
50
+ svgCanvas = canvas
51
+ svgCanvas.setSvgString = setSvgString
52
+ svgCanvas.importSvgString = importSvgString
53
+ svgCanvas.uniquifyElems = uniquifyElemsMethod
54
+ svgCanvas.setUseData = setUseDataMethod
55
+ svgCanvas.convertGradients = convertGradientsMethod
56
+ svgCanvas.removeUnusedDefElems = removeUnusedDefElemsMethod // remove DOM elements inside the `<defs>` if they are notreferred to,
57
+ svgCanvas.svgCanvasToString = svgCanvasToString // Main function to set up the SVG content for output.
58
+ svgCanvas.svgToString = svgToString // Sub function ran on each SVG element to convert it to a string as desired.
59
+ svgCanvas.embedImage = embedImage // Converts a given image file to a data URL when possibl
60
+ svgCanvas.rasterExport = rasterExport // Generates a PNG (or JPG, BMP, WEBP) Data URL based on the current image
61
+ svgCanvas.exportPDF = exportPDF // Generates a PDF based on the current image, then calls "exportedPDF"
62
+ }
63
+
64
+ /**
65
+ * Main function to set up the SVG content for output.
66
+ * @function module:svgcanvas.SvgCanvas#svgCanvasToString
67
+ * @returns {string} The SVG image for output
68
+ */
69
+ const svgCanvasToString = () => {
70
+ // keep calling it until there are none to remove
71
+ while (svgCanvas.removeUnusedDefElems() > 0) {} // eslint-disable-line no-empty
72
+
73
+ svgCanvas.pathActions.clear(true)
74
+
75
+ // Keep SVG-Edit comment on top
76
+ const childNodesElems = svgCanvas.getSvgContent().childNodes
77
+ childNodesElems.forEach((node, i) => {
78
+ if (i && node.nodeType === 8 && node.data.includes('Created with')) {
79
+ svgCanvas.getSvgContent().firstChild.before(node)
80
+ }
81
+ })
82
+
83
+ // Move out of in-group editing mode
84
+ if (svgCanvas.getCurrentGroup()) {
85
+ draw.leaveContext()
86
+ svgCanvas.selectOnly([svgCanvas.getCurrentGroup()])
87
+ }
88
+
89
+ const nakedSvgs = []
90
+
91
+ // Unwrap gsvg if it has no special attributes (only id and style)
92
+ const gsvgElems = svgCanvas.getSvgContent().querySelectorAll('g[data-gsvg]')
93
+ Array.prototype.forEach.call(gsvgElems, (element) => {
94
+ const attrs = element.attributes
95
+ let len = attrs.length
96
+ for (let i = 0; i < len; i++) {
97
+ if (attrs[i].nodeName === 'id' || attrs[i].nodeName === 'style') {
98
+ len--
99
+ }
100
+ }
101
+ // No significant attributes, so ungroup
102
+ if (len <= 0) {
103
+ const svg = element.firstChild
104
+ nakedSvgs.push(svg)
105
+ element.replaceWith(svg)
106
+ }
107
+ })
108
+ const output = svgCanvas.svgToString(svgCanvas.getSvgContent(), 0)
109
+
110
+ // Rewrap gsvg
111
+ if (nakedSvgs.length) {
112
+ Array.prototype.forEach.call(nakedSvgs, (el) => {
113
+ svgCanvas.groupSvgElem(el)
114
+ })
115
+ }
116
+
117
+ return output
118
+ }
119
+
120
+ /**
121
+ * Sub function ran on each SVG element to convert it to a string as desired.
122
+ * @function module:svgcanvas.SvgCanvas#svgToString
123
+ * @param {Element} elem - The SVG element to convert
124
+ * @param {Integer} indent - Number of spaces to indent this tag
125
+ * @returns {string} The given element as an SVG tag
126
+ */
127
+ const svgToString = (elem, indent) => {
128
+ const curConfig = svgCanvas.getCurConfig()
129
+ const nsMap = svgCanvas.getNsMap()
130
+ const out = []
131
+ const unit = curConfig.baseUnit
132
+ const unitRe = new RegExp('^-?[\\d\\.]+' + unit + '$')
133
+
134
+ if (elem) {
135
+ cleanupElement(elem)
136
+ const attrs = [...elem.attributes]
137
+ const childs = elem.childNodes
138
+ attrs.sort((a, b) => {
139
+ return a.name > b.name ? -1 : 1
140
+ })
141
+
142
+ for (let i = 0; i < indent; i++) {
143
+ out.push(' ')
144
+ }
145
+ out.push('<')
146
+ out.push(elem.nodeName)
147
+ if (elem.id === 'svgcontent') {
148
+ // Process root element separately
149
+ const res = svgCanvas.getResolution()
150
+
151
+ let vb = ''
152
+ // TODO: Allow this by dividing all values by current baseVal
153
+ // Note that this also means we should properly deal with this on import
154
+ // if (curConfig.baseUnit !== 'px') {
155
+ // const unit = curConfig.baseUnit;
156
+ // const unitM = getTypeMap()[unit];
157
+ // res.w = shortFloat(res.w / unitM);
158
+ // res.h = shortFloat(res.h / unitM);
159
+ // vb = ' viewBox="' + [0, 0, res.w, res.h].join(' ') + '"';
160
+ // res.w += unit;
161
+ // res.h += unit;
162
+ // }
163
+ if (curConfig.dynamicOutput) {
164
+ vb = elem.getAttribute('viewBox')
165
+ out.push(
166
+ ' viewBox="' +
167
+ vb +
168
+ '" xmlns="' +
169
+ NS.SVG +
170
+ '"'
171
+ )
172
+ } else {
173
+ if (unit !== 'px') {
174
+ res.w = convertUnit(res.w, unit) + unit
175
+ res.h = convertUnit(res.h, unit) + unit
176
+ }
177
+ out.push(
178
+ ' width="' +
179
+ res.w +
180
+ '" height="' +
181
+ res.h +
182
+ '" xmlns="' +
183
+ NS.SVG +
184
+ '"'
185
+ )
186
+ }
187
+
188
+ const nsuris = {}
189
+
190
+ // Check elements for namespaces, add if found
191
+ const csElements = elem.querySelectorAll('*')
192
+ const cElements = Array.prototype.slice.call(csElements)
193
+ cElements.push(elem)
194
+ Array.prototype.forEach.call(cElements, (el) => {
195
+ // const el = this;
196
+ // for some elements have no attribute
197
+ const uri = el.namespaceURI
198
+ if (
199
+ uri &&
200
+ !nsuris[uri] &&
201
+ nsMap[uri] &&
202
+ nsMap[uri] !== 'xmlns' &&
203
+ nsMap[uri] !== 'xml'
204
+ ) {
205
+ nsuris[uri] = true
206
+ out.push(' xmlns:' + nsMap[uri] + '="' + uri + '"')
207
+ }
208
+ if (el.attributes.length > 0) {
209
+ for (const [, attr] of Object.entries(el.attributes)) {
210
+ const u = attr.namespaceURI
211
+ if (u && !nsuris[u] && nsMap[u] !== 'xmlns' && nsMap[u] !== 'xml') {
212
+ nsuris[u] = true
213
+ out.push(' xmlns:' + nsMap[u] + '="' + u + '"')
214
+ }
215
+ }
216
+ }
217
+ })
218
+
219
+ let i = attrs.length
220
+ const attrNames = [
221
+ 'width',
222
+ 'height',
223
+ 'xmlns',
224
+ 'x',
225
+ 'y',
226
+ 'viewBox',
227
+ 'id',
228
+ 'overflow'
229
+ ]
230
+ while (i--) {
231
+ const attr = attrs[i]
232
+ const attrVal = toXml(attr.value)
233
+
234
+ // Namespaces have already been dealt with, so skip
235
+ if (attr.nodeName.startsWith('xmlns:')) {
236
+ continue
237
+ }
238
+
239
+ // only serialize attributes we don't use internally
240
+ if (
241
+ attrVal !== '' &&
242
+ !attrNames.includes(attr.localName) &&
243
+ (!attr.namespaceURI || nsMap[attr.namespaceURI])
244
+ ) {
245
+ out.push(' ')
246
+ out.push(attr.nodeName)
247
+ out.push('="')
248
+ out.push(attrVal)
249
+ out.push('"')
250
+ }
251
+ }
252
+ } else {
253
+ // Skip empty defs
254
+ if (elem.nodeName === 'defs' && !elem.firstChild) {
255
+ return ''
256
+ }
257
+
258
+ const mozAttrs = ['-moz-math-font-style', '_moz-math-font-style']
259
+ for (let i = attrs.length - 1; i >= 0; i--) {
260
+ const attr = attrs[i]
261
+ let attrVal = toXml(attr.value)
262
+ // remove bogus attributes added by Gecko
263
+ if (mozAttrs.includes(attr.localName)) {
264
+ continue
265
+ }
266
+ if (attrVal === 'null') {
267
+ const styleName = attr.localName.replace(/-[a-z]/g, s =>
268
+ s[1].toUpperCase()
269
+ )
270
+ if (Object.prototype.hasOwnProperty.call(elem.style, styleName)) {
271
+ continue
272
+ }
273
+ }
274
+ if (attrVal !== '') {
275
+ if (attrVal.startsWith('pointer-events')) {
276
+ continue
277
+ }
278
+ if (attr.localName === 'class' && attrVal.startsWith('se_')) {
279
+ continue
280
+ }
281
+ out.push(' ')
282
+ if (attr.localName === 'd') {
283
+ attrVal = svgCanvas.pathActions.convertPath(elem, true)
284
+ }
285
+ if (!isNaN(attrVal)) {
286
+ attrVal = shortFloat(attrVal)
287
+ } else if (unitRe.test(attrVal)) {
288
+ attrVal = shortFloat(attrVal) + unit
289
+ }
290
+
291
+ // Embed images when saving
292
+ if (
293
+ svgCanvas.getSvgOptionApply() &&
294
+ elem.nodeName === 'image' &&
295
+ attr.localName === 'href' &&
296
+ svgCanvas.getSvgOptionImages() &&
297
+ svgCanvas.getSvgOptionImages() === 'embed'
298
+ ) {
299
+ const img = svgCanvas.getEncodableImages(attrVal)
300
+ if (img) {
301
+ attrVal = img
302
+ }
303
+ }
304
+
305
+ // map various namespaces to our fixed namespace prefixes
306
+ // (the default xmlns attribute itself does not get a prefix)
307
+ if (
308
+ !attr.namespaceURI ||
309
+ attr.namespaceURI === NS.SVG ||
310
+ nsMap[attr.namespaceURI]
311
+ ) {
312
+ out.push(attr.nodeName)
313
+ out.push('="')
314
+ out.push(attrVal)
315
+ out.push('"')
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ if (elem.hasChildNodes()) {
322
+ out.push('>')
323
+ indent++
324
+ let bOneLine = false
325
+
326
+ for (let i = 0; i < childs.length; i++) {
327
+ const child = childs.item(i)
328
+ switch (child.nodeType) {
329
+ case 1: // element node
330
+ out.push('\n')
331
+ out.push(svgCanvas.svgToString(child, indent))
332
+ break
333
+ case 3: {
334
+ // text node
335
+ const str = child.nodeValue.replace(/^\s+|\s+$/g, '')
336
+ if (str !== '') {
337
+ bOneLine = true
338
+ out.push(String(toXml(str)))
339
+ }
340
+ break
341
+ }
342
+ case 4: // cdata node
343
+ out.push('\n')
344
+ out.push(new Array(indent + 1).join(' '))
345
+ out.push('<![CDATA[')
346
+ out.push(child.nodeValue)
347
+ out.push(']]>')
348
+ break
349
+ case 8: // comment
350
+ out.push('\n')
351
+ out.push(new Array(indent + 1).join(' '))
352
+ out.push('<!--')
353
+ out.push(child.data)
354
+ out.push('-->')
355
+ break
356
+ } // switch on node type
357
+ }
358
+ indent--
359
+ if (!bOneLine) {
360
+ out.push('\n')
361
+ for (let i = 0; i < indent; i++) {
362
+ out.push(' ')
363
+ }
364
+ }
365
+ out.push('</')
366
+ out.push(elem.nodeName)
367
+ out.push('>')
368
+ } else {
369
+ out.push('/>')
370
+ }
371
+ }
372
+ return out.join('')
373
+ } // end svgToString()
374
+
375
+ /**
376
+ * This function sets the current drawing as the input SVG XML.
377
+ * @function module:svgcanvas.SvgCanvas#setSvgString
378
+ * @param {string} xmlString - The SVG as XML text.
379
+ * @param {boolean} [preventUndo=false] - Indicates if we want to do the
380
+ * changes without adding them to the undo stack - e.g. for initializing a
381
+ * drawing on page load.
382
+ * @fires module:svgcanvas.SvgCanvas#event:setnonce
383
+ * @fires module:svgcanvas.SvgCanvas#event:unsetnonce
384
+ * @fires module:svgcanvas.SvgCanvas#event:changed
385
+ * @returns {boolean} This function returns `false` if the set was
386
+ * unsuccessful, `true` otherwise.
387
+ */
388
+ const setSvgString = (xmlString, preventUndo) => {
389
+ const curConfig = svgCanvas.getCurConfig()
390
+ const dataStorage = svgCanvas.getDataStorage()
391
+ try {
392
+ // convert string into XML document
393
+ const newDoc = text2xml(xmlString)
394
+ if (
395
+ newDoc.firstElementChild &&
396
+ newDoc.firstElementChild.namespaceURI !== NS.SVG
397
+ ) {
398
+ return false
399
+ }
400
+
401
+ svgCanvas.prepareSvg(newDoc)
402
+
403
+ const batchCmd = new BatchCommand('Change Source')
404
+
405
+ // remove old svg document
406
+ const { nextSibling } = svgCanvas.getSvgContent()
407
+
408
+ svgCanvas.getSvgContent().remove()
409
+ const oldzoom = svgCanvas.getSvgContent()
410
+ batchCmd.addSubCommand(
411
+ new RemoveElementCommand(oldzoom, nextSibling, svgCanvas.getSvgRoot())
412
+ )
413
+
414
+ // set new svg document
415
+ // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode()
416
+ if (svgCanvas.getDOMDocument().adoptNode) {
417
+ svgCanvas.setSvgContent(
418
+ svgCanvas.getDOMDocument().adoptNode(newDoc.documentElement)
419
+ )
420
+ } else {
421
+ svgCanvas.setSvgContent(
422
+ svgCanvas.getDOMDocument().importNode(newDoc.documentElement, true)
423
+ )
424
+ }
425
+
426
+ svgCanvas.getSvgRoot().append(svgCanvas.getSvgContent())
427
+ const content = svgCanvas.getSvgContent()
428
+
429
+ svgCanvas.current_drawing_ = new draw.Drawing(
430
+ svgCanvas.getSvgContent(),
431
+ svgCanvas.getIdPrefix()
432
+ )
433
+
434
+ // retrieve or set the nonce
435
+ const nonce = svgCanvas.getCurrentDrawing().getNonce()
436
+ if (nonce) {
437
+ svgCanvas.call('setnonce', nonce)
438
+ } else {
439
+ svgCanvas.call('unsetnonce')
440
+ }
441
+
442
+ // change image href vals if possible
443
+ const elements = content.querySelectorAll('image')
444
+ Array.prototype.forEach.call(elements, (image) => {
445
+ preventClickDefault(image)
446
+ const val = svgCanvas.getHref(image)
447
+ if (val) {
448
+ if (val.startsWith('data:')) {
449
+ // Check if an SVG-edit data URI
450
+ const m = val.match(/svgedit_url=(.*?);/)
451
+ // const m = val.match(/svgedit_url=(?<url>.*?);/);
452
+ if (m) {
453
+ const url = decodeURIComponent(m[1])
454
+ // const url = decodeURIComponent(m.groups.url);
455
+ const iimg = new Image()
456
+ iimg.addEventListener('load', () => {
457
+ image.setAttributeNS(NS.XLINK, 'xlink:href', url)
458
+ })
459
+ iimg.src = url
460
+ }
461
+ }
462
+ // Add to encodableImages if it loads
463
+ svgCanvas.embedImage(val)
464
+ }
465
+ })
466
+ // Duplicate id replace changes
467
+ const nodes = content.querySelectorAll('[id]')
468
+ const ids = {}
469
+ const totalNodes = nodes.length
470
+
471
+ for (let i = 0; i < totalNodes; i++) {
472
+ const currentId = nodes[i].id ? nodes[i].id : 'undefined'
473
+ if (isNaN(ids[currentId])) {
474
+ ids[currentId] = 0
475
+ }
476
+ ids[currentId]++
477
+ }
478
+
479
+ Object.entries(ids).forEach(([key, value]) => {
480
+ if (value > 1) {
481
+ const nodes = content.querySelectorAll('[id="' + key + '"]')
482
+ for (let i = 1; i < nodes.length; i++) {
483
+ nodes[i].setAttribute('id', svgCanvas.getNextId())
484
+ }
485
+ }
486
+ })
487
+
488
+ // Wrap child SVGs in group elements
489
+ const svgElements = content.querySelectorAll('svg')
490
+ Array.prototype.forEach.call(svgElements, (element) => {
491
+ // Skip if it's in a <defs>
492
+ if (getClosest(element.parentNode, 'defs')) {
493
+ return
494
+ }
495
+
496
+ svgCanvas.uniquifyElems(element)
497
+
498
+ // Check if it already has a gsvg group
499
+ const pa = element.parentNode
500
+ if (pa.childNodes.length === 1 && pa.nodeName === 'g') {
501
+ dataStorage.put(pa, 'gsvg', element)
502
+ pa.id = pa.id || svgCanvas.getNextId()
503
+ } else {
504
+ svgCanvas.groupSvgElem(element)
505
+ }
506
+ })
507
+
508
+ // For Firefox: Put all paint elems in defs
509
+ if (isGecko()) {
510
+ const svgDefs = findDefs()
511
+ const findElems = content.querySelectorAll(
512
+ 'linearGradient, radialGradient, pattern'
513
+ )
514
+ Array.prototype.forEach.call(findElems, (ele) => {
515
+ svgDefs.appendChild(ele)
516
+ })
517
+ }
518
+
519
+ // Set ref element for <use> elements
520
+
521
+ // TODO: This should also be done if the object is re-added through "redo"
522
+ svgCanvas.setUseData(content)
523
+
524
+ svgCanvas.convertGradients(content)
525
+
526
+ const attrs = {
527
+ id: 'svgcontent',
528
+ overflow: curConfig.show_outside_canvas ? 'visible' : 'hidden'
529
+ }
530
+
531
+ let percs = false
532
+
533
+ // determine proper size
534
+ if (content.getAttribute('viewBox')) {
535
+ const viBox = content.getAttribute('viewBox')
536
+ const vb = viBox.split(' ')
537
+ attrs.width = vb[2]
538
+ attrs.height = vb[3]
539
+ // handle content that doesn't have a viewBox
540
+ } else {
541
+ ;['width', 'height'].forEach((dim) => {
542
+ // Set to 100 if not given
543
+ const val = content.getAttribute(dim) || '100%'
544
+ if (String(val).substr(-1) === '%') {
545
+ // Use user units if percentage given
546
+ percs = true
547
+ } else {
548
+ attrs[dim] = convertToNum(dim, val)
549
+ }
550
+ })
551
+ }
552
+
553
+ // identify layers
554
+ draw.identifyLayers()
555
+
556
+ // Give ID for any visible layer children missing one
557
+ const chiElems = content.children
558
+ Array.prototype.forEach.call(chiElems, (chiElem) => {
559
+ const visElems = chiElem.querySelectorAll(svgCanvas.getVisElems())
560
+ Array.prototype.forEach.call(visElems, (elem) => {
561
+ if (!elem.id) {
562
+ elem.id = svgCanvas.getNextId()
563
+ }
564
+ })
565
+ })
566
+
567
+ // Percentage width/height, so let's base it on visible elements
568
+ if (percs) {
569
+ const bb = getStrokedBBoxDefaultVisible()
570
+ attrs.width = bb.width + bb.x
571
+ attrs.height = bb.height + bb.y
572
+ }
573
+
574
+ // Just in case negative numbers are given or
575
+ // result from the percs calculation
576
+ if (attrs.width <= 0) {
577
+ attrs.width = 100
578
+ }
579
+ if (attrs.height <= 0) {
580
+ attrs.height = 100
581
+ }
582
+
583
+ for (const [key, value] of Object.entries(attrs)) {
584
+ content.setAttribute(key, value)
585
+ }
586
+ svgCanvas.contentW = attrs.width
587
+ svgCanvas.contentH = attrs.height
588
+
589
+ batchCmd.addSubCommand(new InsertElementCommand(svgCanvas.getSvgContent()))
590
+ // update root to the correct size
591
+ const width = content.getAttribute('width')
592
+ const height = content.getAttribute('height')
593
+ const changes = { width, height }
594
+ batchCmd.addSubCommand(
595
+ new ChangeElementCommand(svgCanvas.getSvgRoot(), changes)
596
+ )
597
+
598
+ // reset zoom
599
+ svgCanvas.setZoom(1)
600
+
601
+ svgCanvas.clearSelection()
602
+ pathModule.clearData()
603
+ svgCanvas.getSvgRoot().append(svgCanvas.selectorManager.selectorParentGroup)
604
+
605
+ if (!preventUndo) svgCanvas.addCommandToHistory(batchCmd)
606
+ svgCanvas.call('sourcechanged', [svgCanvas.getSvgContent()])
607
+ } catch (e) {
608
+ console.error(e)
609
+ return false
610
+ }
611
+
612
+ return true
613
+ }
614
+
615
+ /**
616
+ * This function imports the input SVG XML as a `<symbol>` in the `<defs>`, then adds a
617
+ * `<use>` to the current layer.
618
+ * @function module:svgcanvas.SvgCanvas#importSvgString
619
+ * @param {string} xmlString - The SVG as XML text.
620
+ * @fires module:svgcanvas.SvgCanvas#event:changed
621
+ * @returns {null|Element} This function returns null if the import was unsuccessful, or the element otherwise.
622
+ * @todo
623
+ * - properly handle if namespace is introduced by imported content (must add to svgcontent
624
+ * and update all prefixes in the imported node)
625
+ * - properly handle recalculating dimensions, `recalculateDimensions()` doesn't handle
626
+ * arbitrary transform lists, but makes some assumptions about how the transform list
627
+ * was obtained
628
+ */
629
+ const importSvgString = (xmlString) => {
630
+ const dataStorage = svgCanvas.getDataStorage()
631
+ let j
632
+ let ts
633
+ let useEl
634
+ try {
635
+ // Get unique ID
636
+ const uid = hashCode(xmlString)
637
+
638
+ let useExisting = false
639
+ // Look for symbol and make sure symbol exists in image
640
+ if (svgCanvas.getImportIds(uid) && svgCanvas.getImportIds(uid).symbol) {
641
+ const parents = getParents(svgCanvas.getImportIds(uid).symbol, '#svgroot')
642
+ if (parents?.length) {
643
+ useExisting = true
644
+ }
645
+ }
646
+
647
+ const batchCmd = new BatchCommand('Import Image')
648
+ let symbol
649
+ if (useExisting) {
650
+ symbol = svgCanvas.getImportIds(uid).symbol
651
+ ts = svgCanvas.getImportIds(uid).xform
652
+ } else {
653
+ // convert string into XML document
654
+ const newDoc = text2xml(xmlString)
655
+
656
+ svgCanvas.prepareSvg(newDoc)
657
+
658
+ // import new svg document into our document
659
+ // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode()
660
+ const svg = svgCanvas.getDOMDocument().adoptNode
661
+ ? svgCanvas.getDOMDocument().adoptNode(newDoc.documentElement)
662
+ : svgCanvas.getDOMDocument().importNode(newDoc.documentElement, true)
663
+
664
+ svgCanvas.uniquifyElems(svg)
665
+
666
+ const innerw = convertToNum('width', svg.getAttribute('width'))
667
+ const innerh = convertToNum('height', svg.getAttribute('height'))
668
+ const innervb = svg.getAttribute('viewBox')
669
+ // if no explicit viewbox, create one out of the width and height
670
+ const vb = innervb ? innervb.split(' ') : [0, 0, innerw, innerh]
671
+ for (j = 0; j < 4; ++j) {
672
+ vb[j] = Number(vb[j])
673
+ }
674
+
675
+ // TODO: properly handle preserveAspectRatio
676
+ const // canvasw = +svgContent.getAttribute('width'),
677
+ canvash = Number(svgCanvas.getSvgContent().getAttribute('height'))
678
+ // imported content should be 1/3 of the canvas on its largest dimension
679
+
680
+ ts =
681
+ innerh > innerw
682
+ ? 'scale(' + canvash / 3 / vb[3] + ')'
683
+ : 'scale(' + canvash / 3 / vb[2] + ')'
684
+
685
+ // Hack to make recalculateDimensions understand how to scale
686
+ ts = 'translate(0) ' + ts + ' translate(0)'
687
+
688
+ symbol = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'symbol')
689
+ const defs = findDefs()
690
+
691
+ if (isGecko()) {
692
+ // Move all gradients into root for Firefox, workaround for this bug:
693
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=353575
694
+ // TODO: Make this properly undo-able.
695
+ const elements = svg.querySelectorAll(
696
+ 'linearGradient, radialGradient, pattern'
697
+ )
698
+ Array.prototype.forEach.call(elements, (el) => {
699
+ defs.appendChild(el)
700
+ })
701
+ }
702
+
703
+ while (svg.firstChild) {
704
+ const first = svg.firstChild
705
+ symbol.append(first)
706
+ }
707
+ const attrs = svg.attributes
708
+ for (const attr of attrs) {
709
+ // Ok for `NamedNodeMap`
710
+ symbol.setAttribute(attr.nodeName, attr.value)
711
+ }
712
+ symbol.id = svgCanvas.getNextId()
713
+
714
+ // Store data
715
+ svgCanvas.setImportIds(uid, {
716
+ symbol,
717
+ xform: ts
718
+ })
719
+
720
+ findDefs().append(symbol)
721
+ batchCmd.addSubCommand(new InsertElementCommand(symbol))
722
+ }
723
+
724
+ useEl = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'use')
725
+ useEl.id = svgCanvas.getNextId()
726
+ svgCanvas.setHref(useEl, '#' + symbol.id)
727
+ ;(
728
+ svgCanvas.getCurrentGroup() ||
729
+ svgCanvas.getCurrentDrawing().getCurrentLayer()
730
+ ).append(useEl)
731
+ batchCmd.addSubCommand(new InsertElementCommand(useEl))
732
+ svgCanvas.clearSelection()
733
+
734
+ useEl.setAttribute('transform', ts)
735
+ recalculateDimensions(useEl)
736
+ dataStorage.put(useEl, 'symbol', symbol)
737
+ dataStorage.put(useEl, 'ref', symbol)
738
+ svgCanvas.addToSelection([useEl])
739
+
740
+ // TODO: Find way to add this in a recalculateDimensions-parsable way
741
+ // if (vb[0] !== 0 || vb[1] !== 0) {
742
+ // ts = 'translate(' + (-vb[0]) + ',' + (-vb[1]) + ') ' + ts;
743
+ // }
744
+ svgCanvas.addCommandToHistory(batchCmd)
745
+ svgCanvas.call('changed', [svgCanvas.getSvgContent()])
746
+ } catch (e) {
747
+ console.error(e)
748
+ return null
749
+ }
750
+
751
+ // we want to return the element so we can automatically select it
752
+ return useEl
753
+ }
754
+ /**
755
+ * Function to run when image data is found.
756
+ * @callback module:svgcanvas.ImageEmbeddedCallback
757
+ * @param {string|false} result Data URL
758
+ * @returns {void}
759
+ */
760
+ /**
761
+ * Converts a given image file to a data URL when possible, then runs a given callback.
762
+ * @function module:svgcanvas.SvgCanvas#embedImage
763
+ * @param {string} src - The path/URL of the image
764
+ * @returns {Promise<string|false>} Resolves to a Data URL (string|false)
765
+ */
766
+ const embedImage = (src) => {
767
+ // Todo: Remove this Promise in favor of making an async/await `Image.load` utility
768
+ return new Promise((resolve, reject) => {
769
+ // load in the image and once it's loaded, get the dimensions
770
+ const imgI = new Image()
771
+ imgI.addEventListener('load', e => {
772
+ // create a canvas the same size as the raster image
773
+ const cvs = document.createElement('canvas')
774
+ cvs.width = e.currentTarget.width
775
+ cvs.height = e.currentTarget.height
776
+ // load the raster image into the canvas
777
+ cvs.getContext('2d').drawImage(e.currentTarget, 0, 0)
778
+ // retrieve the data: URL
779
+ try {
780
+ let urldata = ';svgedit_url=' + encodeURIComponent(src)
781
+ urldata = cvs.toDataURL().replace(';base64', urldata + ';base64')
782
+ svgCanvas.setEncodableImages(src, urldata)
783
+ } catch (e) {
784
+ svgCanvas.setEncodableImages(src, false)
785
+ }
786
+ svgCanvas.setGoodImage(src)
787
+ resolve(svgCanvas.getEncodableImages(src))
788
+ })
789
+ imgI.addEventListener('error', e => {
790
+ reject(
791
+ new Error(
792
+ `error loading image: ${e.currentTarget.attributes.src.value}`
793
+ )
794
+ )
795
+ })
796
+ imgI.setAttribute('src', src)
797
+ })
798
+ }
799
+
800
+ /**
801
+ * @typedef {PlainObject} module:svgcanvas.IssuesAndCodes
802
+ * @property {string[]} issueCodes The locale-independent code names
803
+ * @property {string[]} issues The localized descriptions
804
+ */
805
+
806
+ /**
807
+ * Codes only is useful for locale-independent detection.
808
+ * @returns {module:svgcanvas.IssuesAndCodes}
809
+ */
810
+ const getIssues = () => {
811
+ const uiStrings = svgCanvas.getUIStrings()
812
+ // remove the selected outline before serializing
813
+ svgCanvas.clearSelection()
814
+
815
+ // Check for known CanVG issues
816
+ const issues = []
817
+ const issueCodes = []
818
+
819
+ // Selector and notice
820
+ const issueList = {
821
+ feGaussianBlur: uiStrings.NoBlur,
822
+ foreignObject: uiStrings.NoforeignObject,
823
+ '[stroke-dasharray]': uiStrings.NoDashArray
824
+ }
825
+ const content = svgCanvas.getSvgContent()
826
+
827
+ // Add font/text check if Canvas Text API is not implemented
828
+ if (!('font' in document.querySelector('CANVAS').getContext('2d'))) {
829
+ issueList.text = uiStrings.NoText
830
+ }
831
+
832
+ for (const [sel, descr] of Object.entries(issueList)) {
833
+ if (content.querySelectorAll(sel).length) {
834
+ issueCodes.push(sel)
835
+ issues.push(descr)
836
+ }
837
+ }
838
+ return { issues, issueCodes }
839
+ }
840
+ /**
841
+ * @typedef {PlainObject} module:svgcanvas.ImageedResults
842
+ * @property {string} datauri Contents as a Data URL
843
+ * @property {string} bloburl May be the empty string
844
+ * @property {string} svg The SVG contents as a string
845
+ * @property {string[]} issues The localization messages of `issueCodes`
846
+ * @property {module:svgcanvas.IssueCode[]} issueCodes CanVG issues found with the SVG
847
+ * @property {"PNG"|"JPEG"|"BMP"|"WEBP"|"ICO"} type The chosen image type
848
+ * @property {"image/png"|"image/jpeg"|"image/bmp"|"image/webp"} mimeType The image MIME type
849
+ * @property {Float} quality A decimal between 0 and 1 (for use with JPEG or WEBP)
850
+ * @property {string} WindowName A convenience for passing along a `window.name` to target a window on which the could be added
851
+ */
852
+
853
+ /**
854
+ * Generates a PNG (or JPG, BMP, WEBP) Data URL based on the current image,
855
+ * then calls "ed" with an object including the string, image
856
+ * information, and any issues found.
857
+ * @function module:svgcanvas.SvgCanvas#raster
858
+ * @param {"PNG"|"JPEG"|"BMP"|"WEBP"|"ICO"} [imgType="PNG"]
859
+ * @param {Float} [quality] Between 0 and 1
860
+ * @param {string} [WindowName]
861
+ * @param {PlainObject} [opts]
862
+ * @param {boolean} [opts.avoidEvent]
863
+ * @fires module:svgcanvas.SvgCanvas#event:ed
864
+ * @todo Confirm/fix ICO type
865
+ * @returns {Promise<module:svgcanvas.ImageedResults>} Resolves to {@link module:svgcanvas.ImageedResults}
866
+ */
867
+ const rasterExport = async (imgType, quality, WindowName, opts = {}) => {
868
+ const type = imgType === 'ICO' ? 'BMP' : imgType || 'PNG'
869
+ const mimeType = 'image/' + type.toLowerCase()
870
+ const { issues, issueCodes } = getIssues()
871
+ const svg = svgCanvas.svgCanvasToString()
872
+
873
+ const iframe = document.createElement('iframe')
874
+ iframe.onload = () => {
875
+ const iframedoc = iframe.contentDocument || iframe.contentWindow.document
876
+ const ele = svgCanvas.getSvgContent()
877
+ const cln = ele.cloneNode(true)
878
+ iframedoc.body.appendChild(cln)
879
+ setTimeout(() => {
880
+ // eslint-disable-next-line promise/catch-or-return
881
+ html2canvas(iframedoc.body, { useCORS: true, allowTaint: true }).then(
882
+ canvas => {
883
+ return new Promise(resolve => {
884
+ const dataURLType = type.toLowerCase()
885
+ const datauri = quality
886
+ ? canvas.toDataURL('image/' + dataURLType, quality)
887
+ : canvas.toDataURL('image/' + dataURLType)
888
+ iframe.parentNode.removeChild(iframe)
889
+ let bloburl
890
+
891
+ const done = () => {
892
+ const obj = {
893
+ datauri,
894
+ bloburl,
895
+ svg,
896
+ issues,
897
+ issueCodes,
898
+ type: imgType,
899
+ mimeType,
900
+ quality,
901
+ WindowName
902
+ }
903
+ if (!opts.avoidEvent) {
904
+ svgCanvas.call('ed', obj)
905
+ }
906
+ resolve(obj)
907
+ }
908
+ if (canvas.toBlob) {
909
+ canvas.toBlob(
910
+ blob => {
911
+ bloburl = createObjectURL(blob)
912
+ done()
913
+ },
914
+ mimeType,
915
+ quality
916
+ )
917
+ return
918
+ }
919
+ bloburl = dataURLToObjectURL(datauri)
920
+ done()
921
+ })
922
+ }
923
+ )
924
+ }, 1000)
925
+ }
926
+ document.body.appendChild(iframe)
927
+ }
928
+
929
+ /**
930
+ * @typedef {void|"save"|"arraybuffer"|"blob"|"datauristring"|"dataurlstring"|"dataurlnewwindow"|"datauri"|"dataurl"} external:jsPDF.OutputType
931
+ * @todo Newer version to add also allows these `outputType` values "bloburi"|"bloburl" which return strings, so document here and for `outputType` of `module:svgcanvas.PDFedResults` below if added
932
+ */
933
+ /**
934
+ * @typedef {PlainObject} module:svgcanvas.PDFedResults
935
+ * @property {string} svg The SVG PDF output
936
+ * @property {string|ArrayBuffer|Blob|window} output The output based on the `outputType`;
937
+ * if `undefined`, "datauristring", "dataurlstring", "datauri",
938
+ * or "dataurl", will be a string (`undefined` gives a document, while the others
939
+ * build as Data URLs; "datauri" and "dataurl" change the location of the current page); if
940
+ * "arraybuffer", will return `ArrayBuffer`; if "blob", returns a `Blob`;
941
+ * if "dataurlnewwindow", will change the current page's location and return a string
942
+ * if in Safari and no window object is found; otherwise opens in, and returns, a new `window`
943
+ * object; if "save", will have the same return as "dataurlnewwindow" if
944
+ * `navigator.getUserMedia` support is found without `URL.createObjectURL` support; otherwise
945
+ * returns `undefined` but attempts to save
946
+ * @property {external:jsPDF.OutputType} outputType
947
+ * @property {string[]} issues The human-readable localization messages of corresponding `issueCodes`
948
+ * @property {module:svgcanvas.IssueCode[]} issueCodes
949
+ * @property {string} WindowName
950
+ */
951
+
952
+ /**
953
+ * Generates a PDF based on the current image, then calls "edPDF" with
954
+ * an object including the string, the data URL, and any issues found.
955
+ * @function module:svgcanvas.SvgCanvas#PDF
956
+ * @param {string} [WindowName] Will also be used for the download file name here
957
+ * @param {external:jsPDF.OutputType} [outputType="dataurlstring"]
958
+ * @fires module:svgcanvas.SvgCanvas#event:edPDF
959
+ * @returns {Promise<module:svgcanvas.PDFedResults>} Resolves to {@link module:svgcanvas.PDFedResults}
960
+ */
961
+ const exportPDF = async (
962
+ WindowName,
963
+ outputType = isChrome() ? 'save' : undefined
964
+ ) => {
965
+ const res = svgCanvas.getResolution()
966
+ const orientation = res.w > res.h ? 'landscape' : 'portrait'
967
+ const unit = 'pt' // curConfig.baseUnit; // We could use baseUnit, but that is presumably not intended for purposes
968
+ const iframe = document.createElement('iframe')
969
+ iframe.onload = () => {
970
+ const iframedoc = iframe.contentDocument || iframe.contentWindow.document
971
+ const ele = svgCanvas.getSvgContent()
972
+ const cln = ele.cloneNode(true)
973
+ iframedoc.body.appendChild(cln)
974
+ setTimeout(() => {
975
+ // eslint-disable-next-line promise/catch-or-return
976
+ html2canvas(iframedoc.body, { useCORS: true, allowTaint: true }).then(
977
+ canvas => {
978
+ const imgData = canvas.toDataURL('image/png')
979
+ const doc = new JsPDF({
980
+ orientation,
981
+ unit,
982
+ format: [res.w, res.h]
983
+ })
984
+ const docTitle = svgCanvas.getDocumentTitle()
985
+ doc.setProperties({
986
+ title: docTitle
987
+ })
988
+ doc.addImage(imgData, 'PNG', 0, 0, res.w, res.h)
989
+ iframe.parentNode.removeChild(iframe)
990
+ const { issues, issueCodes } = getIssues()
991
+ outputType = outputType || 'dataurlstring'
992
+ const obj = { issues, issueCodes, WindowName, outputType }
993
+ obj.output = doc.output(
994
+ outputType,
995
+ outputType === 'save' ? WindowName || 'svg.pdf' : undefined
996
+ )
997
+ svgCanvas.call('edPDF', obj)
998
+ return obj
999
+ }
1000
+ )
1001
+ }, 1000)
1002
+ }
1003
+ document.body.appendChild(iframe)
1004
+ }
1005
+ /**
1006
+ * Ensure each element has a unique ID.
1007
+ * @function module:svgcanvas.SvgCanvas#uniquifyElems
1008
+ * @param {Element} g - The parent element of the tree to give unique IDs
1009
+ * @returns {void}
1010
+ */
1011
+ const uniquifyElemsMethod = (g) => {
1012
+ const ids = {}
1013
+ // TODO: Handle markers and connectors. These are not yet re-identified properly
1014
+ // as their referring elements do not get remapped.
1015
+ //
1016
+ // <marker id='se_marker_end_svg_7'/>
1017
+ // <polyline id='svg_7' se:connector='svg_1 svg_6' marker-end='url(#se_marker_end_svg_7)'/>
1018
+ //
1019
+ // Problem #1: if svg_1 gets renamed, we do not update the polyline's se:connector attribute
1020
+ // Problem #2: if the polyline svg_7 gets renamed, we do not update the marker id nor the polyline's marker-end attribute
1021
+ const refElems = [
1022
+ 'filter',
1023
+ 'linearGradient',
1024
+ 'pattern',
1025
+ 'radialGradient',
1026
+ 'symbol',
1027
+ 'textPath',
1028
+ 'use'
1029
+ ]
1030
+
1031
+ walkTree(g, (n) => {
1032
+ // if it's an element node
1033
+ if (n.nodeType === 1) {
1034
+ // and the element has an ID
1035
+ if (n.id) {
1036
+ // and we haven't tracked this ID yet
1037
+ if (!(n.id in ids)) {
1038
+ // add this id to our map
1039
+ ids[n.id] = { elem: null, attrs: [], hrefs: [] }
1040
+ }
1041
+ ids[n.id].elem = n
1042
+ }
1043
+
1044
+ // now search for all attributes on this element that might refer
1045
+ // to other elements
1046
+ svgCanvas.getrefAttrs().forEach((attr) => {
1047
+ const attrnode = n.getAttributeNode(attr)
1048
+ if (attrnode) {
1049
+ // the incoming file has been sanitized, so we should be able to safely just strip off the leading #
1050
+ const url = svgCanvas.getUrlFromAttr(attrnode.value)
1051
+ const refid = url ? url.substr(1) : null
1052
+ if (refid) {
1053
+ if (!(refid in ids)) {
1054
+ // add this id to our map
1055
+ ids[refid] = { elem: null, attrs: [], hrefs: [] }
1056
+ }
1057
+ ids[refid].attrs.push(attrnode)
1058
+ }
1059
+ }
1060
+ })
1061
+
1062
+ // check xlink:href now
1063
+ const href = svgCanvas.getHref(n)
1064
+ // TODO: what if an <image> or <a> element refers to an element internally?
1065
+ if (href && refElems.includes(n.nodeName)) {
1066
+ const refid = href.substr(1)
1067
+ if (refid) {
1068
+ if (!(refid in ids)) {
1069
+ // add this id to our map
1070
+ ids[refid] = { elem: null, attrs: [], hrefs: [] }
1071
+ }
1072
+ ids[refid].hrefs.push(n)
1073
+ }
1074
+ }
1075
+ }
1076
+ })
1077
+
1078
+ // in ids, we now have a map of ids, elements and attributes, let's re-identify
1079
+ for (const oldid in ids) {
1080
+ if (!oldid) {
1081
+ continue
1082
+ }
1083
+ const { elem } = ids[oldid]
1084
+ if (elem) {
1085
+ const newid = svgCanvas.getNextId()
1086
+
1087
+ // assign element its new id
1088
+ elem.id = newid
1089
+
1090
+ // remap all url() attributes
1091
+ const { attrs } = ids[oldid]
1092
+ let j = attrs.length
1093
+ while (j--) {
1094
+ const attr = attrs[j]
1095
+ attr.ownerElement.setAttribute(attr.name, 'url(#' + newid + ')')
1096
+ }
1097
+
1098
+ // remap all href attributes
1099
+ const hreffers = ids[oldid].hrefs
1100
+ let k = hreffers.length
1101
+ while (k--) {
1102
+ const hreffer = hreffers[k]
1103
+ svgCanvas.setHref(hreffer, '#' + newid)
1104
+ }
1105
+ }
1106
+ }
1107
+ }
1108
+
1109
+ /**
1110
+ * Assigns reference data for each use element.
1111
+ * @function module:svgcanvas.SvgCanvas#setUseData
1112
+ * @param {Element} parent
1113
+ * @returns {void}
1114
+ */
1115
+ const setUseDataMethod = (parent) => {
1116
+ let elems = parent
1117
+
1118
+ if (parent.tagName !== 'use') {
1119
+ // elems = elems.find('use');
1120
+ elems = elems.querySelectorAll('use')
1121
+ }
1122
+
1123
+ Array.prototype.forEach.call(elems, (el, _) => {
1124
+ const dataStorage = svgCanvas.getDataStorage()
1125
+ const id = svgCanvas.getHref(el).substr(1)
1126
+ const refElem = svgCanvas.getElement(id)
1127
+ if (!refElem) {
1128
+ return
1129
+ }
1130
+ dataStorage.put(el, 'ref', refElem)
1131
+ if (refElem.tagName === 'symbol' || refElem.tagName === 'svg') {
1132
+ dataStorage.put(el, 'symbol', refElem)
1133
+ dataStorage.put(el, 'ref', refElem)
1134
+ }
1135
+ })
1136
+ }
1137
+
1138
+ /**
1139
+ * Looks at DOM elements inside the `<defs>` to see if they are referred to,
1140
+ * removes them from the DOM if they are not.
1141
+ * @function module:svgcanvas.SvgCanvas#removeUnusedDefElems
1142
+ * @returns {Integer} The number of elements that were removed
1143
+ */
1144
+ const removeUnusedDefElemsMethod = () => {
1145
+ const defs = svgCanvas.getSvgContent().getElementsByTagNameNS(NS.SVG, 'defs')
1146
+ if (!defs || !defs.length) {
1147
+ return 0
1148
+ }
1149
+
1150
+ // if (!defs.firstChild) { return; }
1151
+
1152
+ const defelemUses = []
1153
+ let numRemoved = 0
1154
+ const attrs = [
1155
+ 'fill',
1156
+ 'stroke',
1157
+ 'filter',
1158
+ 'marker-start',
1159
+ 'marker-mid',
1160
+ 'marker-end'
1161
+ ]
1162
+ const alen = attrs.length
1163
+
1164
+ const allEls = svgCanvas.getSvgContent().getElementsByTagNameNS(NS.SVG, '*')
1165
+ const allLen = allEls.length
1166
+
1167
+ let i
1168
+ let j
1169
+ for (i = 0; i < allLen; i++) {
1170
+ const el = allEls[i]
1171
+ for (j = 0; j < alen; j++) {
1172
+ const ref = svgCanvas.getUrlFromAttr(el.getAttribute(attrs[j]))
1173
+ if (ref) {
1174
+ defelemUses.push(ref.substr(1))
1175
+ }
1176
+ }
1177
+
1178
+ // gradients can refer to other gradients
1179
+ const href = getHref(el)
1180
+ if (href && href.startsWith('#')) {
1181
+ defelemUses.push(href.substr(1))
1182
+ }
1183
+ }
1184
+
1185
+ Array.prototype.forEach.call(defs, (def, i) => {
1186
+ const defelems = def.querySelectorAll(
1187
+ 'linearGradient, radialGradient, filter, marker, svg, symbol'
1188
+ )
1189
+ i = defelems.length
1190
+ while (i--) {
1191
+ const defelem = defelems[i]
1192
+ const { id } = defelem
1193
+ if (!defelemUses.includes(id)) {
1194
+ // Not found, so remove (but remember)
1195
+ svgCanvas.setRemovedElements(id, defelem)
1196
+ defelem.remove()
1197
+ numRemoved++
1198
+ }
1199
+ }
1200
+ })
1201
+
1202
+ return numRemoved
1203
+ }
1204
+ /**
1205
+ * Converts gradients from userSpaceOnUse to objectBoundingBox.
1206
+ * @function module:svgcanvas.SvgCanvas#convertGradients
1207
+ * @param {Element} elem
1208
+ * @returns {void}
1209
+ */
1210
+ const convertGradientsMethod = (elem) => {
1211
+ let elems = elem.querySelectorAll('linearGradient, radialGradient')
1212
+ if (!elems.length && isWebkit()) {
1213
+ // Bug in webkit prevents regular *Gradient selector search
1214
+ elems = Array.prototype.filter.call(elem.querySelectorAll('*'), (
1215
+ curThis
1216
+ ) => {
1217
+ return curThis.tagName.includes('Gradient')
1218
+ })
1219
+ }
1220
+ Array.prototype.forEach.call(elems, (grad) => {
1221
+ if (grad.getAttribute('gradientUnits') === 'userSpaceOnUse') {
1222
+ const svgContent = svgCanvas.getSvgContent()
1223
+ // TODO: Support more than one element with this ref by duplicating parent grad
1224
+ let fillStrokeElems = svgContent.querySelectorAll(
1225
+ '[fill="url(#' + grad.id + ')"],[stroke="url(#' + grad.id + ')"]'
1226
+ )
1227
+ if (!fillStrokeElems.length) {
1228
+ const tmpFillStrokeElems = svgContent.querySelectorAll(
1229
+ '[*|href="#' + grad.id + '"]'
1230
+ )
1231
+ if (!tmpFillStrokeElems.length) {
1232
+ return
1233
+ } else {
1234
+ if (
1235
+ (tmpFillStrokeElems[0].tagName === 'linearGradient' ||
1236
+ tmpFillStrokeElems[0].tagName === 'radialGradient') &&
1237
+ tmpFillStrokeElems[0].getAttribute('gradientUnits') ===
1238
+ 'userSpaceOnUse'
1239
+ ) {
1240
+ fillStrokeElems = svgContent.querySelectorAll(
1241
+ '[fill="url(#' +
1242
+ tmpFillStrokeElems[0].id +
1243
+ ')"],[stroke="url(#' +
1244
+ tmpFillStrokeElems[0].id +
1245
+ ')"]'
1246
+ )
1247
+ } else {
1248
+ return
1249
+ }
1250
+ }
1251
+ }
1252
+ // get object's bounding box
1253
+ const bb = utilsGetBBox(fillStrokeElems[0])
1254
+
1255
+ // This will occur if the element is inside a <defs> or a <symbol>,
1256
+ // in which we shouldn't need to convert anyway.
1257
+ if (!bb) {
1258
+ return
1259
+ }
1260
+ if (grad.tagName === 'linearGradient') {
1261
+ const gCoords = {
1262
+ x1: grad.getAttribute('x1'),
1263
+ y1: grad.getAttribute('y1'),
1264
+ x2: grad.getAttribute('x2'),
1265
+ y2: grad.getAttribute('y2')
1266
+ }
1267
+
1268
+ // If has transform, convert
1269
+ const tlist = grad.gradientTransform.baseVal
1270
+ if (tlist?.numberOfItems > 0) {
1271
+ const m = transformListToTransform(tlist).matrix
1272
+ const pt1 = transformPoint(gCoords.x1, gCoords.y1, m)
1273
+ const pt2 = transformPoint(gCoords.x2, gCoords.y2, m)
1274
+
1275
+ gCoords.x1 = pt1.x
1276
+ gCoords.y1 = pt1.y
1277
+ gCoords.x2 = pt2.x
1278
+ gCoords.y2 = pt2.y
1279
+ grad.removeAttribute('gradientTransform')
1280
+ }
1281
+ grad.setAttribute('x1', (gCoords.x1 - bb.x) / bb.width)
1282
+ grad.setAttribute('y1', (gCoords.y1 - bb.y) / bb.height)
1283
+ grad.setAttribute('x2', (gCoords.x2 - bb.x) / bb.width)
1284
+ grad.setAttribute('y2', (gCoords.y2 - bb.y) / bb.height)
1285
+ grad.removeAttribute('gradientUnits')
1286
+ }
1287
+ }
1288
+ })
1289
+ }