svg-path-simplify 0.3.6 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,20 @@
1
1
  import { getElementAtts } from "../svg-getAttributes";
2
2
  import { flattenTransforms } from "../svg_flatten_transforms";
3
3
  import { getViewBox } from "../svg_getViewbox";
4
- import { normalizeUnits } from "./convert_units";
4
+ import { isNumericValue, normalizeUnits } from "./convert_units";
5
5
  import { getPathDataVertices } from "./geometry";
6
6
  import { checkBBoxIntersections, getPathDataBBox, getPolyBBox } from "./geometry_bbox";
7
7
  import { getElBBox } from "./geometry_bbox_element";
8
8
  import { parsePathDataString } from "./pathData_parse";
9
9
  import { parsePathDataNormalized } from "./pathData_convert";
10
10
  import { pathElToShape, shapeElToPath } from "./pathData_parse_els";
11
- import { svgStylesToAttributes } from "./svg-styles-to-attributes";
12
- import { strokeAtts } from "./svg-styles-to-attributes-const";
13
- import { parseStylesProperties } from "./svg_el_parse_style_props";
11
+ //import { scaleProps } from "./svg-styles-to-attributes";
12
+ import { geometryEls, renderedEls, shapeEls, strokeAtts } from "./svg-styles-to-attributes-const";
13
+ import { addTransFormProps, filterSvgElProps, parseStylesProperties } from "./svg_el_parse_style_props";
14
+ import { autoRound } from "./rounding";
15
+ import { getMatrixFromTransform } from "./svg-styles-getTransforms";
16
+ import { qrDecomposeMatrix } from "./transform_qr_decompose";
17
+ import { svgNs } from "../constants";
14
18
 
15
19
 
16
20
  export function removeEmptySVGEls(svg) {
@@ -22,6 +26,7 @@ export function removeEmptySVGEls(svg) {
22
26
 
23
27
  //const DOMParserPoly = globalThis.DOMParser;
24
28
 
29
+
25
30
  export function cleanUpSVG(svgMarkup, {
26
31
  removeHidden = true,
27
32
  //removeUnused = true,
@@ -36,6 +41,12 @@ export function cleanUpSVG(svgMarkup, {
36
41
  cleanupClip = true,
37
42
  addViewBox = false,
38
43
  addDimensions = false,
44
+ minifyRgbColors = false,
45
+
46
+ normalizeTransforms = true,
47
+ autoRoundValues = true,
48
+
49
+ unGroup = false,
39
50
 
40
51
  mergePaths = false,
41
52
  removeOffCanvas = true,
@@ -47,20 +58,22 @@ export function cleanUpSVG(svgMarkup, {
47
58
  convert_rects = false,
48
59
  convert_ellipses = false,
49
60
  convert_poly = false,
50
- convert_lines=false,
61
+ convert_lines = false,
51
62
 
52
63
  convertTransforms = false,
64
+ removeDefaults = true,
53
65
  cleanUpStrokes = true,
54
66
  decimals = -1,
55
67
  excludedEls = [],
56
68
  } = {}) {
57
69
 
58
- attributesToGroup = cleanupSVGAtts ? true : false;
70
+ //attributesToGroup = cleanupSVGAtts ? true : false;
59
71
 
60
72
 
61
73
  // replace namespaced refs
62
74
  if (fixHref) svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
63
75
 
76
+
64
77
  let svg = new DOMParser()
65
78
  .parseFromString(svgMarkup, "text/html")
66
79
  .querySelector("svg");
@@ -69,16 +82,60 @@ export function cleanUpSVG(svgMarkup, {
69
82
  let { x, y, width, height } = viewBox;
70
83
 
71
84
 
85
+ // get svg styles
86
+ let propOptions = {
87
+ width: width,
88
+ height: height,
89
+ normalizeTransforms,
90
+ removeDefaults: false,
91
+ cleanUpStrokes: false,
92
+ autoRoundValues,
93
+ minifyRgbColors,
94
+ }
95
+ let stylePropsSVG = parseStylesProperties(svg, propOptions)
96
+
97
+ // add svg font size for scaling relative
98
+ propOptions.fontSize = stylePropsSVG['font-size'] ? stylePropsSVG['font-size'][0] : 16;
99
+
100
+
101
+ /**
102
+ * get group styles
103
+ * especially transformations to
104
+ * be inherited by children
105
+ */
106
+ let groups = svg.querySelectorAll('g')
107
+ let groupProps = [];
108
+
109
+ groups.forEach(g => {
110
+ let stylePropsG = parseStylesProperties(g, propOptions)
111
+ groupProps.push(stylePropsG);
112
+ let children = g.querySelectorAll(`${renderedEls.join(', ')}`)
113
+
114
+ // store parent styles to child property
115
+ children.forEach(child => {
116
+ if (child.parentStyleProps === undefined) {
117
+ child.parentStyleProps = []
118
+ }
119
+ child.parentStyleProps.push(stylePropsG)
120
+ })
121
+ })
122
+
123
+
124
+
72
125
  if (cleanupSVGAtts) {
73
126
  //console.log('cleanupSVGAtts');
74
- let allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'];
127
+ let allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
128
+ if (!stylesToAttributes) {
129
+ allowed.push('fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'font-size', 'font-family', 'font-style', 'style');
130
+ }
131
+
75
132
  removeExcludedAttribues(svg, allowed)
76
133
  }
77
134
 
78
135
  // add viewBox
79
136
  if (addViewBox) addSvgViewBox(svg, { x, y, width, height })
80
137
  if (addDimensions) {
81
- svg.setAttribute('width', width);
138
+ svg.setAttribute('width', width);
82
139
  svg.setAttribute('height', height);
83
140
  }
84
141
 
@@ -94,44 +151,20 @@ export function cleanUpSVG(svgMarkup, {
94
151
  // always remove scripts
95
152
  let removeEls = ['metadata', 'script', ...excludedEls]
96
153
 
97
- let els = svg.querySelectorAll('*')
154
+ removeSVGEls(svg, { removeEls, removeNameSpaced });
98
155
 
99
156
  // an array of all elements' properties
100
157
  let svgElProps = []
101
-
102
- let geometryElements = ['polygon', 'polyline', 'line', 'rect', 'circle', 'ellipse']
103
-
104
- //console.log('shapeConvert', shapeConvert);
105
-
106
-
107
- /** convert paths to shapes */
108
- if(shapeConvert === 'toShapes'){
109
- let paths = svg.querySelectorAll('path')
110
- paths.forEach(path=>{
111
- let shape = pathElToShape(path, {convert_rects, convert_ellipses, convert_poly, convert_lines})
112
- path.replaceWith(shape)
113
- path = shape;
114
- //console.log('path', path);
115
- })
116
-
117
- }
158
+ let els = svg.querySelectorAll(`${renderedEls.join(', ')}`)
118
159
 
119
160
 
120
161
  for (let i = 0; i < els.length; i++) {
121
162
  let el = els[i];
122
163
 
123
164
  let name = el.nodeName.toLowerCase();
165
+ //console.log(name);
124
166
 
125
- // convert shapes
126
- if (shapeConvert === 'toPaths' && name !== 'path' && geometryElements.includes(name)) {
127
- let path = shapeElToPath(el, { width, height, convert_rects, convert_ellipses, convert_poly, convert_lines });
128
- el.replaceWith(path)
129
- name = 'path'
130
- el = path;
131
- //console.log('shapesToPaths', el.outerHTML);
132
- }
133
-
134
- // remove hidden elements
167
+ // 1. remove hidden elements
135
168
  let style = el.getAttribute('style') || ''
136
169
  let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
137
170
  let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
@@ -146,94 +179,241 @@ export function cleanUpSVG(svgMarkup, {
146
179
  * convert relative or physical units
147
180
  * to user units
148
181
  */
182
+ let styleProps = parseStylesProperties(el, propOptions)
183
+
184
+ // get parent styles
185
+ let { parentStyleProps = [] } = el;
186
+ let inheritedProps = {}
187
+ let transFormInherited = []
149
188
 
150
189
 
151
- /*
152
- let styleProps = parseStylesProperties(el, {
153
- width:viewBox.width,
154
- height:viewBox.height
190
+ /** inherit transforms
191
+ * and styles from group
192
+ */
193
+ parentStyleProps.forEach(props => {
194
+ // transforms from groups are applied cumulatively
195
+ let { transformArr = [] } = props
196
+ transFormInherited.push(...transformArr)
197
+
198
+ // merge
199
+ inheritedProps = {
200
+ ...inheritedProps,
201
+ ...props
202
+ };
155
203
  })
156
- */
157
204
 
158
205
 
159
- /*
160
- let propTest = normalizeUnits('50%',{width:200, height:100, isHorizontal:true})
161
- console.log('propTest', propTest);
162
- */
206
+ //merge transforms
207
+ transFormInherited = [...transFormInherited, ...styleProps.transformArr]
208
+ styleProps.transformArr = transFormInherited
163
209
 
164
210
 
165
- // styles to attributes
166
- if (stylesToAttributes || attributesToGroup || mergePaths || cleanUpStrokes) {
167
- let propsFiltered = svgStylesToAttributes(el, { removeNameSpaced, decimals })
168
- //if (name === 'path') {}
169
- svgElProps.push({ el, name, idx: i, propsFiltered })
211
+ // merge with svg props
212
+ styleProps = {
213
+ ...stylePropsSVG,
214
+ ...inheritedProps,
215
+ ...styleProps
170
216
  }
217
+ //console.log('inheritedProps', inheritedProps, name);
171
218
 
172
- }
219
+ // add combined transforms
220
+ addTransFormProps(styleProps, transFormInherited);
221
+
222
+ //console.log('transFormInherited', transFormInherited);
223
+ //console.log('styleProps', styleProps);
224
+
225
+
226
+ let { remove, matrix, transComponents } = styleProps;
227
+
228
+ // mark attributes for removal
229
+ if (removeClassNames) styleProps.remove.push('class')
230
+ if (removeIds) styleProps.remove.push('id')
231
+ if (removeDimensions) {
232
+ styleProps.remove.push('width')
233
+ styleProps.remove.push('height')
234
+ }
235
+
236
+
237
+ // styles to atts
238
+ if (unGroup || convertTransforms || minifyRgbColors ) stylesToAttributes = true;
239
+
240
+
241
+ if (stylesToAttributes) {
242
+
243
+ /**
244
+ * normalize transforms
245
+ */
246
+ if (normalizeTransforms && matrix) {
247
+ let { rotate, scaleX, scaleY, skewX, translateX, translateY } = transComponents;
248
+ //console.log(rotate, scaleX, scaleY, skewX, skewY, translateX, translateY);
249
+
250
+ // scale attributes instead of transform
251
+ let hasRot = rotate !== 0 || skewX !== 0;
252
+ let unProportional = scaleX !== scaleY;
253
+ let scalableByAtt = ['circle', 'ellipse', 'rect']
254
+ let needsTrans = convertTransforms || (name === 'g') || (hasRot) || unProportional
255
+ //needsTrans = true
256
+
257
+ if (!needsTrans && scalableByAtt.includes(name)) {
258
+
259
+ if (name === 'circle' || name === 'ellipse') {
260
+ styleProps.cx[0] = [styleProps.cx[0] * scaleX + translateX]
261
+ styleProps.cy[0] = [styleProps.cy[0] * scaleX + translateY]
262
+
263
+ if (styleProps.r) styleProps.r[0] = [styleProps.r[0] * scaleX]
173
264
 
265
+ if (styleProps.rx) styleProps.rx[0] = [styleProps.rx[0] * scaleX]
266
+ if (styleProps.ry) styleProps.ry[0] = [styleProps.ry[0] * scaleX]
174
267
 
268
+ }
269
+ else if (name === 'rect') {
270
+ let x = styleProps.x ? styleProps.x[0] + translateX : translateX;
271
+ let y = styleProps.y ? styleProps.y[0] + translateY : translateY;
272
+
273
+ let rx = styleProps.rx ? styleProps.rx[0] * scaleX : 0;
274
+ let ry = styleProps.ry ? styleProps.ry[0] * scaleY : 0;
275
+
276
+ styleProps.x = [x]
277
+ styleProps.y = [y]
278
+
279
+ styleProps.rx = [rx]
280
+ styleProps.ry = [ry]
175
281
 
176
- // remove stroke properties if no stroke color applied - common inkscape issue
177
- if (cleanUpStrokes) {
282
+ styleProps.width = [styleProps.width[0] * scaleX]
283
+ styleProps.height = [styleProps.height[0] * scaleX]
284
+ }
285
+
286
+ remove.push('transform')
178
287
 
179
- for (let item of svgElProps) {
288
+ // scale props like stroke width or dash-array
289
+ styleProps = scaleProps(styleProps, { props: ['stroke-width', 'stroke-dasharray'], scale: scaleX })
180
290
 
181
- let { el, propsFiltered } = item;
182
- let strokeProps = Object.keys(propsFiltered)
291
+ } else {
292
+ el.setAttribute('transform', transComponents.matrixAtt)
293
+
294
+ }
295
+ }
296
+
297
+
298
+ /**
299
+ * apply consolidated
300
+ * element attributes
301
+ */
302
+
303
+ let stylePropsFiltered = filterSvgElProps(name, styleProps,
304
+ { removeDefaults: true, cleanUpStrokes });
305
+
306
+ remove = [...remove, ...stylePropsFiltered.remove];
307
+
308
+ for (let prop in stylePropsFiltered.propsFiltered) {
309
+ let values = styleProps[prop]
310
+ //console.log('add', prop);
311
+ let val = values.length ? values.join(' ') : values[0]
312
+ el.setAttribute(prop, val)
313
+ }
183
314
 
184
- if (!strokeProps.includes('stroke')) {
185
- strokeAtts.forEach(att => {
186
- el.removeAttribute(att)
315
+ // remove obsolete attributes
316
+ for (let i = 0; i < remove.length; i++) {
317
+ let att = remove[i];
318
+ if (!stylesToAttributes && att === 'style') continue
187
319
 
188
- // delete in property object
189
- if (item['propsFiltered'][att] !== undefined) delete item['propsFiltered'][att]
320
+ //console.log('remove att', att, name);
321
+ el.removeAttribute(att)
322
+ }
323
+
324
+
325
+
326
+ /**
327
+ * remove group styles
328
+ * copied to children
329
+ * or remove nesting
330
+ */
331
+
332
+ if (unGroup) {
333
+ groups.forEach((g, i) => {
334
+ let children = [...g.children];
335
+
336
+ children.forEach(child => {
337
+ g.parentNode.insertBefore(child, g)
338
+ })
339
+ g.remove()
190
340
  })
341
+ } else {
342
+ groups.forEach((g, i) => {
343
+ let atts = [...Object.keys(groupProps[i]), 'style', 'transform'];
344
+ atts.forEach(att => {
345
+ g.removeAttribute(att)
346
+ })
347
+ })
348
+
191
349
  }
350
+
351
+
352
+ } // endof style processing
353
+
354
+
355
+ /**
356
+ * element conversions:
357
+ * shapes to paths or
358
+ * paths to shapes
359
+ */
360
+
361
+
362
+ // force shape conversion when transform conversion is enabled
363
+ if (convertTransforms) {
364
+ shapeConvert = 'toPaths';
365
+ convert_rects = true;
366
+ convert_ellipses = true;
367
+ convert_poly = true;
368
+ convert_lines = true;
192
369
  }
193
- }
194
370
 
195
- // group styles
196
- if (attributesToGroup || mergePaths) {
197
- moveAttributesToGroup(svgElProps, mergePaths)
198
- }
371
+ // convert shapes to paths
372
+ if (shapeConvert === 'toPaths') {
199
373
 
200
- if (removeDimensions) {
201
- svg.removeAttribute('width')
202
- svg.removeAttribute('height')
203
- }
374
+ let { matrix = null, transComponents = null } = styleProps;
204
375
 
205
- if (removeClassNames || removeIds) {
206
- let att = removeClassNames ? 'class' : 'id';
207
- let selector = `[${att}]`
208
- let els = svg.querySelectorAll(selector)
209
- svg.removeAttribute(att)
210
- els.forEach(el => {
211
- el.removeAttribute(att)
212
- })
213
- }
376
+ if (matrix && transComponents) {
377
+ // scale props like stroke width or dash-array before conversion
378
+ ['stroke-width', 'stroke-dasharray'].forEach(att => {
379
+ let attVal = el.getAttribute(att)
380
+ let vals = attVal ? attVal.split(' ').filter(Boolean).map(Number).map(val => val * transComponents.scaleX) : []
381
+ if (vals.length) el.setAttribute(att, vals.join(' '))
382
+ })
383
+ }
214
384
 
215
- //console.log('!!!svgMarkup', svgMarkup);
385
+ // convert paths only if a matrix transform is required
386
+ if (matrix ? geometryEls.includes(name) : shapeEls.includes(name)) {
216
387
 
388
+ let path = shapeElToPath(el, { width, height, convert_rects, convert_ellipses, convert_poly, convert_lines, matrix });
389
+ el.replaceWith(path)
217
390
 
391
+ name = 'path'
392
+ el = path;
218
393
 
219
- /**
220
- * refine properties
221
- * such as transforms or properties including units
222
- */
223
394
 
224
- /*
225
- for(let i=0; i<svgElProps.length; i++){
226
- let item = svgElProps[i];
227
- let {propsFiltered} = item;
395
+ }
228
396
 
229
- for(let prop in propsFiltered){
397
+ }
230
398
 
231
- let propOb = parseStyleProperty(prop, propsFiltered[prop])
399
+ // convert paths to shapes
400
+ else if (shapeConvert === 'toShapes') {
401
+ let paths = svg.querySelectorAll('path')
402
+ paths.forEach(path => {
403
+ let shape = pathElToShape(path, { convert_rects, convert_ellipses, convert_poly, convert_lines })
404
+ path.replaceWith(shape)
405
+ path = shape;
406
+ })
232
407
 
233
408
  }
234
409
 
235
- }
236
- */
410
+
411
+
412
+ }//endof element loop
413
+
414
+
415
+
416
+
237
417
 
238
418
  // remove futile clip-paths
239
419
  if (cleanupClip) removeFutileClipPaths(svg, { x, y, width, height })
@@ -457,7 +637,7 @@ function moveAttributesToGroup(svgElProps = [], mergePaths = true) {
457
637
 
458
638
  // wrap in group if not existent
459
639
  if (!g) {
460
- g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
640
+ g = document.createElementNS(svgNs, 'g');
461
641
  el0.parentNode.insertBefore(g, el0)
462
642
  group.forEach(item => {
463
643
  g.append(item.el)
@@ -518,6 +698,35 @@ function moveAttributesToGroup(svgElProps = [], mergePaths = true) {
518
698
  }
519
699
 
520
700
 
701
+ export function scaleProps(styleProps = {}, { props = [], scale = 1 } = {}) {
702
+ if (scale === 1 || !props.length) return props;
703
+
704
+ for (let i = 0; i < props.length; i++) {
705
+ let prop = props[i];
706
+
707
+ if (styleProps[prop] !== undefined) {
708
+ styleProps[prop] = styleProps[prop].map(val => val * scale)
709
+ }
710
+ }
711
+ return styleProps
712
+ }
713
+
714
+ export function removeSVGEls(svg, {
715
+ remove = ['metadata', 'script'],
716
+ removeNameSpaced = true,
717
+ } = {}) {
718
+ let els = svg.querySelectorAll('*')
719
+ els.forEach(el => {
720
+ let nodeName = el.nodeName;
721
+ if ((removeNameSpaced && nodeName.includes(':')) ||
722
+ remove.includes(nodeName)
723
+ ) {
724
+ el.remove()
725
+ }
726
+ })
727
+ }
728
+
729
+
521
730
  function cleanSvgPrologue(svgString) {
522
731
  return (
523
732
  svgString
@@ -552,22 +761,32 @@ function removeAtts(el, exclude = [], include = []) {
552
761
  }
553
762
 
554
763
 
555
- function removeNameSpaceAtts(el) {
764
+ function removeNameSpaceAtts(el, {
765
+ include = ['xlink:href']
766
+ } = {}) {
556
767
  let atts = [...el.attributes].map((att) => att.name);
557
768
  atts.forEach((att) => {
558
- if (att.includes(":")) {
769
+ if (att.includes(":") && !include.includes(att)) {
559
770
  el.removeAttribute(att);
560
771
  }
561
772
  });
562
773
  }
563
774
 
564
- export function stringifySVG(svg, omitNamespace = false) {
775
+ export function stringifySVG(svg, {
776
+ omitNamespace = false,
777
+ removeComments = true,
778
+ } = {}) {
565
779
  let markup = new XMLSerializer().serializeToString(svg);
566
780
 
567
781
  if (omitNamespace) {
568
782
  markup = markup.replaceAll('xmlns="http://www.w3.org/2000/svg"', '')
569
783
  }
570
784
 
785
+ if (removeComments) {
786
+ markup = markup
787
+ .replace(/(<!--.*?-->)|(<!--[\S\s]+?-->)|(<!--[\S\s]*?$)/g, '')
788
+ }
789
+
571
790
  markup = markup
572
791
  .replace(/\t/g, "")
573
792
  .replace(/[\n\r|]/g, "\n")
@@ -576,9 +795,12 @@ export function stringifySVG(svg, omitNamespace = false) {
576
795
  //.replace(/ +/g, ' ')
577
796
  .replace(/> </g, '><')
578
797
  .trim()
579
- // sanitize linebreaks within pathdata
798
+ // sanitize linebreaks within pathdata
580
799
  .replaceAll('&#10;', '\n');
581
800
 
582
801
 
802
+
803
+
804
+
583
805
  return markup
584
806
  }