@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/blur-event.js +156 -0
- package/clear.js +43 -0
- package/coords.js +298 -0
- package/copy-elem.js +45 -0
- package/dataStorage.js +28 -0
- package/dist/svgcanvas.js +515 -0
- package/dist/svgcanvas.js.map +1 -0
- package/draw.js +1064 -0
- package/elem-get-set.js +1077 -0
- package/event.js +1388 -0
- package/history.js +619 -0
- package/historyrecording.js +161 -0
- package/json.js +110 -0
- package/layer.js +228 -0
- package/math.js +221 -0
- package/namespaces.js +40 -0
- package/package.json +54 -0
- package/paint.js +88 -0
- package/paste-elem.js +127 -0
- package/path-actions.js +1237 -0
- package/path-method.js +1012 -0
- package/path.js +781 -0
- package/recalculate.js +794 -0
- package/rollup.config.js +40 -0
- package/sanitize.js +252 -0
- package/select.js +543 -0
- package/selected-elem.js +1297 -0
- package/selection.js +482 -0
- package/svg-exec.js +1289 -0
- package/svgcanvas.js +1347 -0
- package/svgroot.js +36 -0
- package/text-actions.js +530 -0
- package/touch.js +51 -0
- package/undo.js +279 -0
- package/utilities.js +1214 -0
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
|
+
}
|