@svgedit/svgcanvas 7.2.7 → 7.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/json.js CHANGED
@@ -27,7 +27,7 @@ let svgdoc_ = null
27
27
  */
28
28
  export const init = (canvas) => {
29
29
  svgCanvas = canvas
30
- svgdoc_ = canvas.getDOMDocument()
30
+ svgdoc_ = canvas.getDOMDocument?.() || (typeof document !== 'undefined' ? document : null)
31
31
  }
32
32
  /**
33
33
  * @function module:json.getJsonFromSvgElements Iterate element and return json format
@@ -35,8 +35,12 @@ export const init = (canvas) => {
35
35
  * @returns {svgRootElement}
36
36
  */
37
37
  export const getJsonFromSvgElements = (data) => {
38
+ if (!data) return null
39
+
38
40
  // Text node
39
- if (data.nodeType === 3) return data.nodeValue
41
+ if (data.nodeType === 3 || data.nodeType === 4) return data.nodeValue
42
+ // Ignore non-element nodes (e.g., comments)
43
+ if (data.nodeType !== 1) return null
40
44
 
41
45
  const retval = {
42
46
  element: data.tagName,
@@ -46,13 +50,25 @@ export const getJsonFromSvgElements = (data) => {
46
50
  }
47
51
 
48
52
  // Iterate attributes
49
- for (let i = 0, attr; (attr = data.attributes[i]); i++) {
50
- retval.attr[attr.name] = attr.value
53
+ const attributes = data.attributes
54
+ if (attributes) {
55
+ for (let i = 0; i < attributes.length; i++) {
56
+ const attr = attributes[i]
57
+ if (!attr) continue
58
+ retval.attr[attr.name] = attr.value
59
+ }
51
60
  }
52
61
 
53
62
  // Iterate children
54
- for (let i = 0, node; (node = data.childNodes[i]); i++) {
55
- retval.children[i] = getJsonFromSvgElements(node)
63
+ const childNodes = data.childNodes
64
+ if (childNodes) {
65
+ for (let i = 0; i < childNodes.length; i++) {
66
+ const node = childNodes[i]
67
+ const child = getJsonFromSvgElements(node)
68
+ if (child !== null && child !== undefined) {
69
+ retval.children.push(child)
70
+ }
71
+ }
56
72
  }
57
73
 
58
74
  return retval
@@ -65,11 +81,29 @@ export const getJsonFromSvgElements = (data) => {
65
81
  */
66
82
 
67
83
  export const addSVGElementsFromJson = (data) => {
84
+ if (!svgdoc_) { return null }
85
+ if (data === null || data === undefined) return svgdoc_.createTextNode('')
68
86
  if (typeof data === 'string') return svgdoc_.createTextNode(data)
69
87
 
70
- let shape = getElement(data.attr.id)
88
+ const attrs = data.attr || {}
89
+ const id = attrs.id
90
+ let shape = null
91
+ if (typeof id === 'string' && id) {
92
+ try {
93
+ shape = getElement(id)
94
+ } catch (e) {
95
+ // Ignore (CSS selector may be invalid); fallback to getElementById below
96
+ }
97
+ if (!shape) {
98
+ const byId = svgdoc_.getElementById?.(id)
99
+ const svgRoot = svgCanvas?.getSvgRoot?.()
100
+ if (byId && (!svgRoot || svgRoot.contains(byId))) {
101
+ shape = byId
102
+ }
103
+ }
104
+ }
71
105
  // if shape is a path but we need to create a rect/ellipse, then remove the path
72
- const currentLayer = svgCanvas.getDrawing().getCurrentLayer()
106
+ const currentLayer = svgCanvas?.getDrawing?.()?.getCurrentLayer?.()
73
107
  if (shape && data.element !== shape.tagName) {
74
108
  shape.remove()
75
109
  shape = null
@@ -81,8 +115,10 @@ export const addSVGElementsFromJson = (data) => {
81
115
  (svgCanvas.getCurrentGroup() || currentLayer).append(shape)
82
116
  }
83
117
  }
84
- const curShape = svgCanvas.getCurShape()
118
+ const curShape = svgCanvas.getCurShape?.() || {}
85
119
  if (data.curStyles) {
120
+ const curOpacity = Number(curShape.opacity)
121
+ const opacity = Number.isFinite(curOpacity) ? (curOpacity / 2) : 0.5
86
122
  assignAttributes(shape, {
87
123
  fill: curShape.fill,
88
124
  stroke: curShape.stroke,
@@ -92,17 +128,23 @@ export const addSVGElementsFromJson = (data) => {
92
128
  'stroke-linecap': curShape.stroke_linecap,
93
129
  'stroke-opacity': curShape.stroke_opacity,
94
130
  'fill-opacity': curShape.fill_opacity,
95
- opacity: curShape.opacity / 2,
131
+ opacity,
96
132
  style: 'pointer-events:inherit'
97
133
  }, 100)
98
134
  }
99
- assignAttributes(shape, data.attr, 100)
135
+ assignAttributes(shape, attrs, 100)
100
136
  cleanupElement(shape)
101
137
 
102
138
  // Children
103
139
  if (data.children) {
140
+ while (shape.firstChild) {
141
+ shape.firstChild.remove()
142
+ }
104
143
  data.children.forEach((child) => {
105
- shape.append(addSVGElementsFromJson(child))
144
+ const childNode = addSVGElementsFromJson(child)
145
+ if (childNode) {
146
+ shape.append(childNode)
147
+ }
106
148
  })
107
149
  }
108
150
 
package/core/layer.js CHANGED
@@ -40,19 +40,16 @@ class Layer {
40
40
  const layerTitle = svgdoc.createElementNS(NS.SVG, 'title')
41
41
  layerTitle.textContent = name
42
42
  this.group_.append(layerTitle)
43
- if (group) {
44
- group.insertAdjacentElement('afterend', this.group_)
45
- } else {
46
- svgElem.append(this.group_)
47
- }
43
+
44
+ group ? group.insertAdjacentElement('afterend', this.group_) : svgElem.append(this.group_)
48
45
  }
49
46
 
50
47
  addLayerClass(this.group_)
51
48
  walkTree(this.group_, function (e) {
52
- e.setAttribute('style', 'pointer-events:inherit')
49
+ e.style.pointerEvents = 'inherit'
53
50
  })
54
51
 
55
- this.group_.setAttribute('style', svgElem ? 'pointer-events:all' : 'pointer-events:none')
52
+ this.group_.style.pointerEvents = svgElem ? 'all' : 'none'
56
53
  }
57
54
 
58
55
  /**
@@ -76,7 +73,7 @@ class Layer {
76
73
  * @returns {void}
77
74
  */
78
75
  activate () {
79
- this.group_.setAttribute('style', 'pointer-events:all')
76
+ this.group_.style.pointerEvents = 'all'
80
77
  }
81
78
 
82
79
  /**
@@ -84,7 +81,7 @@ class Layer {
84
81
  * @returns {void}
85
82
  */
86
83
  deactivate () {
87
- this.group_.setAttribute('style', 'pointer-events:none')
84
+ this.group_.style.pointerEvents = 'none'
88
85
  }
89
86
 
90
87
  /**
@@ -93,7 +90,7 @@ class Layer {
93
90
  * @returns {void}
94
91
  */
95
92
  setVisible (visible) {
96
- const expected = visible === undefined || visible ? 'inline' : 'none'
93
+ const expected = (visible === undefined || visible) ? 'inline' : 'none'
97
94
  const oldDisplay = this.group_.getAttribute('display')
98
95
  if (oldDisplay !== expected) {
99
96
  this.group_.setAttribute('display', expected)
@@ -114,10 +111,7 @@ class Layer {
114
111
  */
115
112
  getOpacity () {
116
113
  const opacity = this.group_.getAttribute('opacity')
117
- if (!opacity) {
118
- return 1
119
- }
120
- return Number.parseFloat(opacity)
114
+ return opacity ? Number.parseFloat(opacity) : 1
121
115
  }
122
116
 
123
117
  /**
@@ -208,7 +202,7 @@ Layer.CLASS_NAME = 'layer'
208
202
  /**
209
203
  * @property {RegExp} CLASS_REGEX - Used to test presence of class Layer.CLASS_NAME
210
204
  */
211
- Layer.CLASS_REGEX = new RegExp('(\\s|^)' + Layer.CLASS_NAME + '(\\s|$)')
205
+ Layer.CLASS_REGEX = new RegExp(`(\\s|^)${Layer.CLASS_NAME}(\\s|$)`)
212
206
 
213
207
  /**
214
208
  * Add class `Layer.CLASS_NAME` to the element (usually `class='layer'`).
@@ -216,12 +210,12 @@ Layer.CLASS_REGEX = new RegExp('(\\s|^)' + Layer.CLASS_NAME + '(\\s|$)')
216
210
  * @param {SVGGElement} elem - The SVG element to update
217
211
  * @returns {void}
218
212
  */
219
- function addLayerClass (elem) {
213
+ const addLayerClass = (elem) => {
220
214
  const classes = elem.getAttribute('class')
221
215
  if (!classes || !classes.length) {
222
216
  elem.setAttribute('class', Layer.CLASS_NAME)
223
217
  } else if (!Layer.CLASS_REGEX.test(classes)) {
224
- elem.setAttribute('class', classes + ' ' + Layer.CLASS_NAME)
218
+ elem.setAttribute('class', `${classes} ${Layer.CLASS_NAME}`)
225
219
  }
226
220
  }
227
221
 
package/core/math.js CHANGED
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import { NS } from './namespaces.js'
23
+ import { warn } from '../common/logger.js'
23
24
 
24
25
  // Constants
25
26
  const NEAR_ZERO = 1e-10
@@ -27,6 +28,38 @@ const NEAR_ZERO = 1e-10
27
28
  // Create a throwaway SVG element for matrix operations
28
29
  const svg = document.createElementNS(NS.SVG, 'svg')
29
30
 
31
+ const createTransformFromMatrix = (m) => {
32
+ const createFallback = (matrix) => {
33
+ const fallback = svg.createSVGMatrix()
34
+ Object.assign(fallback, {
35
+ a: matrix.a,
36
+ b: matrix.b,
37
+ c: matrix.c,
38
+ d: matrix.d,
39
+ e: matrix.e,
40
+ f: matrix.f
41
+ })
42
+ return fallback
43
+ }
44
+
45
+ try {
46
+ return svg.createSVGTransformFromMatrix(m)
47
+ } catch (e) {
48
+ const t = svg.createSVGTransform()
49
+ try {
50
+ t.setMatrix(m)
51
+ return t
52
+ } catch (err) {
53
+ try {
54
+ return svg.createSVGTransformFromMatrix(createFallback(m))
55
+ } catch (e2) {
56
+ t.setMatrix(createFallback(m))
57
+ return t
58
+ }
59
+ }
60
+ }
61
+ }
62
+
30
63
  /**
31
64
  * Transforms a point by a given matrix without DOM calls.
32
65
  * @function transformPoint
@@ -56,7 +89,7 @@ export const getTransformList = elem => {
56
89
  if (elem.patternTransform?.baseVal) {
57
90
  return elem.patternTransform.baseVal
58
91
  }
59
- console.warn('No transform list found. Check browser compatibility.', elem)
92
+ warn('No transform list found. Check browser compatibility.', elem, 'math')
60
93
  }
61
94
 
62
95
  /**
@@ -66,7 +99,12 @@ export const getTransformList = elem => {
66
99
  * @returns {boolean} True if it's an identity matrix (1,0,0,1,0,0)
67
100
  */
68
101
  export const isIdentity = m =>
69
- m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0
102
+ Math.abs(m.a - 1) < NEAR_ZERO &&
103
+ Math.abs(m.b) < NEAR_ZERO &&
104
+ Math.abs(m.c) < NEAR_ZERO &&
105
+ Math.abs(m.d - 1) < NEAR_ZERO &&
106
+ Math.abs(m.e) < NEAR_ZERO &&
107
+ Math.abs(m.f) < NEAR_ZERO
70
108
 
71
109
  /**
72
110
  * Multiplies multiple matrices together (m1 * m2 * ...).
@@ -76,22 +114,54 @@ export const isIdentity = m =>
76
114
  * @returns {SVGMatrix} The resulting matrix
77
115
  */
78
116
  export const matrixMultiply = (...args) => {
79
- // If no matrices are given, return an identity matrix
80
117
  if (args.length === 0) {
81
118
  return svg.createSVGMatrix()
82
119
  }
83
120
 
84
- const m = args.reduceRight((prev, curr) => curr.multiply(prev))
121
+ const normalizeNearZero = (matrix) => {
122
+ const props = ['a', 'b', 'c', 'd', 'e', 'f']
123
+ for (const prop of props) {
124
+ if (Math.abs(matrix[prop]) < NEAR_ZERO) {
125
+ matrix[prop] = 0
126
+ }
127
+ }
128
+ return matrix
129
+ }
85
130
 
86
- // Round near-zero values to zero
87
- if (Math.abs(m.a) < NEAR_ZERO) m.a = 0
88
- if (Math.abs(m.b) < NEAR_ZERO) m.b = 0
89
- if (Math.abs(m.c) < NEAR_ZERO) m.c = 0
90
- if (Math.abs(m.d) < NEAR_ZERO) m.d = 0
91
- if (Math.abs(m.e) < NEAR_ZERO) m.e = 0
92
- if (Math.abs(m.f) < NEAR_ZERO) m.f = 0
131
+ if (typeof DOMMatrix === 'function' && typeof DOMMatrix.fromMatrix === 'function') {
132
+ const result = args.reduce(
133
+ (acc, curr) => acc.multiply(DOMMatrix.fromMatrix(curr)),
134
+ new DOMMatrix()
135
+ )
93
136
 
94
- return m
137
+ const out = svg.createSVGMatrix()
138
+ Object.assign(out, {
139
+ a: result.a,
140
+ b: result.b,
141
+ c: result.c,
142
+ d: result.d,
143
+ e: result.e,
144
+ f: result.f
145
+ })
146
+
147
+ return normalizeNearZero(out)
148
+ }
149
+
150
+ let m = svg.createSVGMatrix()
151
+ for (const curr of args) {
152
+ const next = svg.createSVGMatrix()
153
+ Object.assign(next, {
154
+ a: m.a * curr.a + m.c * curr.b,
155
+ b: m.b * curr.a + m.d * curr.b,
156
+ c: m.a * curr.c + m.c * curr.d,
157
+ d: m.b * curr.c + m.d * curr.d,
158
+ e: m.a * curr.e + m.c * curr.f + m.e,
159
+ f: m.b * curr.e + m.d * curr.f + m.f
160
+ })
161
+ m = next
162
+ }
163
+
164
+ return normalizeNearZero(m)
95
165
  }
96
166
 
97
167
  /**
@@ -172,25 +242,34 @@ export const transformBox = (l, t, w, h, m) => {
172
242
  */
173
243
  export const transformListToTransform = (tlist, min = 0, max = null) => {
174
244
  if (!tlist) {
175
- return svg.createSVGTransformFromMatrix(svg.createSVGMatrix())
245
+ return createTransformFromMatrix(svg.createSVGMatrix())
176
246
  }
177
247
 
178
248
  const start = Number.parseInt(min, 10)
179
249
  const end = Number.parseInt(max ?? tlist.numberOfItems - 1, 10)
180
- const low = Math.min(start, end)
181
- const high = Math.max(start, end)
250
+ const [low, high] = [Math.min(start, end), Math.max(start, end)]
182
251
 
183
- let combinedMatrix = svg.createSVGMatrix()
252
+ const matrices = []
184
253
  for (let i = low; i <= high; i++) {
185
- // If out of range, use identity
186
- const currentMatrix =
187
- i >= 0 && i < tlist.numberOfItems
188
- ? tlist.getItem(i).matrix
189
- : svg.createSVGMatrix()
190
- combinedMatrix = matrixMultiply(combinedMatrix, currentMatrix)
254
+ const matrix = (i >= 0 && i < tlist.numberOfItems)
255
+ ? tlist.getItem(i).matrix
256
+ : svg.createSVGMatrix()
257
+ matrices.push(matrix)
191
258
  }
192
259
 
193
- return svg.createSVGTransformFromMatrix(combinedMatrix)
260
+ const combinedMatrix = matrixMultiply(...matrices)
261
+
262
+ const out = svg.createSVGMatrix()
263
+ Object.assign(out, {
264
+ a: combinedMatrix.a,
265
+ b: combinedMatrix.b,
266
+ c: combinedMatrix.c,
267
+ d: combinedMatrix.d,
268
+ e: combinedMatrix.e,
269
+ f: combinedMatrix.f
270
+ })
271
+
272
+ return createTransformFromMatrix(out)
194
273
  }
195
274
 
196
275
  /**
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  /**
8
- * Common namepaces constants in alpha order.
8
+ * Common namespaces constants in alpha order.
9
9
  * @enum {string}
10
10
  * @type {PlainObject}
11
11
  * @memberof module:namespaces
@@ -29,12 +29,12 @@ export const NS = {
29
29
 
30
30
  /**
31
31
  * @function module:namespaces.getReverseNS
32
- * @returns {string} The NS with key values switched and lowercase
32
+ * @returns {PlainObject<string, string>} The namespace URI map with values swapped to their lowercase keys
33
33
  */
34
- export const getReverseNS = function () {
34
+ export const getReverseNS = () => {
35
35
  const reverseNS = {}
36
- Object.entries(NS).forEach(([name, URI]) => {
36
+ for (const [name, URI] of Object.entries(NS)) {
37
37
  reverseNS[URI] = name.toLowerCase()
38
- })
38
+ }
39
39
  return reverseNS
40
40
  }
package/core/paint.js CHANGED
@@ -2,12 +2,95 @@
2
2
  *
3
3
  */
4
4
  export default class Paint {
5
+ static #normalizeAlpha (alpha) {
6
+ const numeric = Number(alpha)
7
+ if (!Number.isFinite(numeric)) return 100
8
+ return Math.min(100, Math.max(0, numeric))
9
+ }
10
+
11
+ static #normalizeSolidColor (color) {
12
+ if (color === null || color === undefined) return null
13
+ const str = String(color).trim()
14
+ if (!str) return null
15
+ if (str === 'none') return 'none'
16
+ return str.startsWith('#') ? str.slice(1) : str
17
+ }
18
+
19
+ static #extractHrefId (hrefAttr) {
20
+ if (!hrefAttr) return null
21
+ const href = String(hrefAttr).trim()
22
+ if (!href) return null
23
+ if (href.startsWith('#')) return href.slice(1)
24
+ const urlMatch = href.match(/url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/)
25
+ if (urlMatch?.[1]) return urlMatch[1]
26
+ const hashIndex = href.lastIndexOf('#')
27
+ if (hashIndex >= 0 && hashIndex < href.length - 1) {
28
+ return href.slice(hashIndex + 1)
29
+ }
30
+ return null
31
+ }
32
+
33
+ static #resolveGradient (gradient) {
34
+ if (!gradient?.cloneNode) return null
35
+ const doc = gradient.ownerDocument || document
36
+ const visited = new Set()
37
+ const clone = gradient.cloneNode(true)
38
+
39
+ let refId = Paint.#extractHrefId(
40
+ clone.getAttribute('href') || clone.getAttribute('xlink:href')
41
+ )
42
+
43
+ while (refId && !visited.has(refId)) {
44
+ visited.add(refId)
45
+
46
+ const referenced = doc.getElementById(refId)
47
+ if (!referenced?.getAttribute) break
48
+
49
+ const cloneTag = String(clone.tagName || '').toLowerCase()
50
+ const referencedTag = String(referenced.tagName || '').toLowerCase()
51
+ if (
52
+ !['lineargradient', 'radialgradient'].includes(referencedTag) ||
53
+ referencedTag !== cloneTag
54
+ ) {
55
+ break
56
+ }
57
+
58
+ // Copy missing attributes from referenced gradient (matches SVG href inheritance).
59
+ for (const attr of referenced.attributes || []) {
60
+ const name = attr.name
61
+ if (name === 'id' || name === 'href' || name === 'xlink:href') continue
62
+ const current = clone.getAttribute(name)
63
+ if (current === null || current === '') {
64
+ clone.setAttribute(name, attr.value)
65
+ }
66
+ }
67
+
68
+ // If the referencing gradient has no stops, inherit stops from the referenced gradient.
69
+ if (clone.querySelectorAll('stop').length === 0) {
70
+ for (const stop of referenced.querySelectorAll?.('stop') || []) {
71
+ clone.append(stop.cloneNode(true))
72
+ }
73
+ }
74
+
75
+ // Prepare to continue resolving deeper links if present.
76
+ refId = Paint.#extractHrefId(
77
+ referenced.getAttribute('href') || referenced.getAttribute('xlink:href')
78
+ )
79
+ }
80
+
81
+ // The clone is now self-contained; remove any href.
82
+ clone.removeAttribute('href')
83
+ clone.removeAttribute('xlink:href')
84
+
85
+ return clone
86
+ }
87
+
5
88
  /**
6
89
  * @param {module:jGraduate.jGraduatePaintOptions} [opt]
7
90
  */
8
91
  constructor (opt) {
9
92
  const options = opt || {}
10
- this.alpha = isNaN(options.alpha) ? 100 : options.alpha
93
+ this.alpha = Paint.#normalizeAlpha(options.alpha)
11
94
  // copy paint object
12
95
  if (options.copy) {
13
96
  /**
@@ -20,7 +103,7 @@ export default class Paint {
20
103
  * @name module:jGraduate~Paint#alpha
21
104
  * @type {Float}
22
105
  */
23
- this.alpha = options.copy.alpha
106
+ this.alpha = Paint.#normalizeAlpha(options.copy.alpha)
24
107
  /**
25
108
  * Represents #RRGGBB hex of color.
26
109
  * @name module:jGraduate~Paint#solidColor
@@ -42,13 +125,17 @@ export default class Paint {
42
125
  case 'none':
43
126
  break
44
127
  case 'solidColor':
45
- this.solidColor = options.copy.solidColor
128
+ this.solidColor = Paint.#normalizeSolidColor(options.copy.solidColor)
46
129
  break
47
130
  case 'linearGradient':
48
- this.linearGradient = options.copy.linearGradient.cloneNode(true)
131
+ this.linearGradient = options.copy.linearGradient?.cloneNode
132
+ ? options.copy.linearGradient.cloneNode(true)
133
+ : null
49
134
  break
50
135
  case 'radialGradient':
51
- this.radialGradient = options.copy.radialGradient.cloneNode(true)
136
+ this.radialGradient = options.copy.radialGradient?.cloneNode
137
+ ? options.copy.radialGradient.cloneNode(true)
138
+ : null
52
139
  break
53
140
  }
54
141
  // create linear gradient paint
@@ -56,33 +143,17 @@ export default class Paint {
56
143
  this.type = 'linearGradient'
57
144
  this.solidColor = null
58
145
  this.radialGradient = null
59
- const hrefAttr =
60
- options.linearGradient.getAttribute('href') ||
61
- options.linearGradient.getAttribute('xlink:href')
62
- if (hrefAttr) {
63
- const xhref = document.getElementById(hrefAttr.replace(/^#/, ''))
64
- this.linearGradient = xhref.cloneNode(true)
65
- } else {
66
- this.linearGradient = options.linearGradient.cloneNode(true)
67
- }
146
+ this.linearGradient = Paint.#resolveGradient(options.linearGradient)
68
147
  // create linear gradient paint
69
148
  } else if (options.radialGradient) {
70
149
  this.type = 'radialGradient'
71
150
  this.solidColor = null
72
151
  this.linearGradient = null
73
- const hrefAttr =
74
- options.radialGradient.getAttribute('href') ||
75
- options.radialGradient.getAttribute('xlink:href')
76
- if (hrefAttr) {
77
- const xhref = document.getElementById(hrefAttr.replace(/^#/, ''))
78
- this.radialGradient = xhref.cloneNode(true)
79
- } else {
80
- this.radialGradient = options.radialGradient.cloneNode(true)
81
- }
152
+ this.radialGradient = Paint.#resolveGradient(options.radialGradient)
82
153
  // create solid color paint
83
154
  } else if (options.solidColor) {
84
155
  this.type = 'solidColor'
85
- this.solidColor = options.solidColor
156
+ this.solidColor = Paint.#normalizeSolidColor(options.solidColor)
86
157
  // create empty paint
87
158
  } else {
88
159
  this.type = 'none'