svg-path-simplify 0.4.1 → 0.4.3

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +6 -4
  3. package/dist/svg-path-simplify.esm.js +2450 -888
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +2450 -888
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +167 -85
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/docs/privacy-webapp.md +24 -0
  10. package/index.html +333 -132
  11. package/package.json +5 -2
  12. package/src/css_parse.js +317 -0
  13. package/src/detect_input.js +34 -4
  14. package/src/pathData_simplify_harmonize_cpts.js +77 -1
  15. package/src/pathSimplify-main.js +246 -262
  16. package/src/pathSimplify-presets.js +243 -0
  17. package/src/poly-fit-curve-schneider.js +14 -7
  18. package/src/simplify_poly_RC.js +102 -0
  19. package/src/simplify_poly_RDP.js +109 -1
  20. package/src/simplify_poly_radial_distance.js +3 -3
  21. package/src/string_helpers.js +144 -0
  22. package/src/svgii/convert_units.js +8 -2
  23. package/src/svgii/geometry.js +182 -3
  24. package/src/svgii/geometry_length.js +237 -0
  25. package/src/svgii/pathData_convert.js +43 -1
  26. package/src/svgii/pathData_fix_directions.js +6 -0
  27. package/src/svgii/pathData_fromPoly.js +3 -3
  28. package/src/svgii/pathData_getLength.js +86 -0
  29. package/src/svgii/pathData_parse.js +2 -0
  30. package/src/svgii/pathData_parse_els.js +189 -189
  31. package/src/svgii/pathData_split_to_groups.js +168 -0
  32. package/src/svgii/pathData_stringify.js +26 -64
  33. package/src/svgii/pathData_toPolygon.js +3 -4
  34. package/src/svgii/poly_analyze.js +61 -0
  35. package/src/svgii/poly_normalize.js +11 -2
  36. package/src/svgii/poly_to_pathdata.js +85 -24
  37. package/src/svgii/rounding.js +8 -7
  38. package/src/svgii/svg-styles-to-attributes-const.js +1 -0
  39. package/src/svgii/svg_cleanup.js +467 -421
  40. package/src/svgii/svg_cleanup_convertPathLength.js +32 -0
  41. package/src/svgii/svg_cleanup_general_svg_atts.js +97 -0
  42. package/src/svgii/svg_cleanup_normalize_transforms.js +83 -0
  43. package/src/svgii/svg_cleanup_remove_els_and_atts.js +72 -0
  44. package/src/svgii/svg_cleanup_ungroup.js +36 -0
  45. package/src/svgii/svg_el_parse_style_props.js +76 -28
  46. package/src/svgii/svg_getElementLength.js +67 -0
  47. package/tests/testSVG_shape.js +59 -0
  48. package/tests/testSVG_transform.js +61 -0
@@ -9,28 +9,28 @@ import { parsePathDataString } from "./pathData_parse";
9
9
  import { parsePathDataNormalized } from "./pathData_convert";
10
10
  import { pathElToShape, shapeElToPath } from "./pathData_parse_els";
11
11
  //import { scaleProps } from "./svg-styles-to-attributes";
12
- import { geometryEls, renderedEls, shapeEls, strokeAtts } from "./svg-styles-to-attributes-const";
12
+ import { geometryEls, geometryProps, renderedEls, shapeEls, strokeAtts } from "./svg-styles-to-attributes-const";
13
13
  import { addTransFormProps, filterSvgElProps, parseStylesProperties } from "./svg_el_parse_style_props";
14
- import { autoRound } from "./rounding";
14
+ import { autoRound, roundTo } from "./rounding";
15
15
  import { getMatrixFromTransform } from "./svg-styles-getTransforms";
16
16
  import { qrDecomposeMatrix } from "./transform_qr_decompose";
17
17
  import { svgNs } from "../constants";
18
-
19
-
20
- export function removeEmptySVGEls(svg) {
21
- let els = svg.querySelectorAll('g, defs');
22
- els.forEach(el => {
23
- if (!el.children.length) el.remove()
24
- })
25
- }
26
-
27
- //const DOMParserPoly = globalThis.DOMParser;
18
+ import { toCamelCase, toShortStr } from "../string_helpers";
19
+ import { getElementLength } from "./svg_getElementLength";
20
+ import { removeHiddenSvgEls, removeSvgAtts, removeSvgChildAtts, removeSvgEls } from "./svg_cleanup_remove_els_and_atts";
21
+ import { cleanupSVGAttributes, removeElAtts } from "./svg_cleanup_general_svg_atts";
22
+ import { convertPathLengthAtt } from "./svg_cleanup_convertPathLength";
23
+ import { removeGroupProps, ungroupElements } from "./svg_cleanup_ungroup";
24
+ import { parseSvgCss } from "../css_parse";
25
+ import { setNormalizedTransformsToEl } from "./svg_cleanup_normalize_transforms";
28
26
 
29
27
 
30
28
  export function cleanUpSVG(svgMarkup, {
31
29
  removeHidden = true,
32
30
  //removeUnused = true,
33
31
  stylesToAttributes = true,
32
+ attributesToGroup = false,
33
+
34
34
  removePrologue = true,
35
35
  removeIds = false,
36
36
  removeClassNames = false,
@@ -50,15 +50,27 @@ export function cleanUpSVG(svgMarkup, {
50
50
 
51
51
  mergePaths = false,
52
52
  removeOffCanvas = true,
53
+
53
54
  cleanupSVGAtts = true,
54
55
  removeNameSpaced = true,
55
- attributesToGroup = true,
56
+ removeNameSpacedAtts = true,
57
+ convertPathLength = false,
58
+
59
+ // meta
60
+ allowMeta = false,
61
+ allowDataAtts = true,
62
+ allowAriaAtts = true,
63
+
56
64
  //shapesToPaths = false,
57
65
  shapeConvert = false,
58
- convert_rects = false,
59
- convert_ellipses = false,
60
- convert_poly = false,
61
- convert_lines = false,
66
+ convertShapes = [],
67
+
68
+ // remove elements
69
+ removeElements = [],
70
+
71
+ // remove attributes
72
+ removeSVGAttributes = [],
73
+ removeElAttributes = [],
62
74
 
63
75
  convertTransforms = false,
64
76
  removeDefaults = true,
@@ -67,9 +79,14 @@ export function cleanUpSVG(svgMarkup, {
67
79
  excludedEls = [],
68
80
  } = {}) {
69
81
 
70
- //attributesToGroup = cleanupSVGAtts ? true : false;
71
82
 
72
83
 
84
+ // resolve dependencies
85
+ if (unGroup || convertTransforms || minifyRgbColors || attributesToGroup)
86
+ stylesToAttributes = true;
87
+
88
+ if(stylesToAttributes) cleanUpStrokes = true;
89
+
73
90
  // replace namespaced refs
74
91
  if (fixHref) svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
75
92
 
@@ -78,21 +95,89 @@ export function cleanUpSVG(svgMarkup, {
78
95
  .parseFromString(svgMarkup, "text/html")
79
96
  .querySelector("svg");
80
97
 
98
+
81
99
  let viewBox = getViewBox(svg)
82
100
  let { x, y, width, height } = viewBox;
101
+ let remove = []
102
+
83
103
 
84
104
 
85
- // get svg styles
105
+ // add viewBox
106
+ if (addViewBox) addSvgViewBox(svg, { x, y, width, height })
107
+ if (addDimensions) {
108
+ svg.setAttribute('width', width);
109
+ svg.setAttribute('height', height);
110
+ }
111
+
112
+ // remove unused defs or optimize order
113
+ if (cleanupDefs) cleanupSvgDefs(svg, { x, y, width, height, cleanupClip });
114
+
115
+
116
+ // remove off canvas
117
+ if (removeOffCanvas) removeOffCanvasEls(svg, { x, y, width, height });
118
+
119
+
120
+ /**
121
+ * collect svg styles
122
+ * and properties
123
+ */
86
124
  let propOptions = {
87
- width: width,
88
- height: height,
125
+ width,
126
+ height,
89
127
  normalizeTransforms,
90
128
  removeDefaults: false,
91
129
  cleanUpStrokes: false,
130
+ //cleanUpStrokes,
131
+ allowMeta,
132
+ allowDataAtts,
133
+ allowAriaAtts,
92
134
  autoRoundValues,
135
+ removeIds,
136
+ removeClassNames,
93
137
  minifyRgbColors,
138
+ stylesheetProps: {},
139
+ exclude:[]
94
140
  }
141
+
142
+ // root svg inline style properties
95
143
  let stylePropsSVG = parseStylesProperties(svg, propOptions)
144
+ //console.log('stylePropsSVG', stylePropsSVG);
145
+
146
+ let styleEl = svg.querySelector('style')
147
+ let cssStylePropsSVG = {}
148
+
149
+ if (styleEl) {
150
+ cssStylePropsSVG = parseSvgCss(styleEl, { parent: svg })
151
+
152
+ //save stylesheet dependencies in node
153
+ for (let selector in cssStylePropsSVG) {
154
+ let els = svg.querySelectorAll(`${selector}`);
155
+ els.forEach(el => {
156
+ if (!el['cssRules']) el['cssRules'] = [];
157
+ el['cssRules'].push(selector)
158
+
159
+ // remove class names only used for styling
160
+ if (stylesToAttributes) {
161
+ let className = selector.substring(1)
162
+ el.classList.remove(className)
163
+ }
164
+ })
165
+ }
166
+
167
+ //console.log('cssStylePropsSVG', cssStylePropsSVG);
168
+
169
+ // remove style element from element
170
+ if (stylesToAttributes) {
171
+ styleEl.remove()
172
+ }
173
+ }
174
+ // remove style element from root SVG
175
+ if (stylesToAttributes) svg.removeAttribute('style')
176
+
177
+
178
+ // add stylesheet props
179
+ propOptions.stylesheetProps = cssStylePropsSVG;
180
+
96
181
 
97
182
  // add svg font size for scaling relative
98
183
  propOptions.fontSize = stylePropsSVG['font-size'] ? stylePropsSVG['font-size'][0] : 16;
@@ -107,7 +192,10 @@ export function cleanUpSVG(svgMarkup, {
107
192
  let groupProps = [];
108
193
 
109
194
  groups.forEach(g => {
195
+ //propOptions.exclude.push('class', 'id');
110
196
  let stylePropsG = parseStylesProperties(g, propOptions)
197
+ //console.log('stylePropsG', stylePropsG);
198
+
111
199
  groupProps.push(stylePropsG);
112
200
  let children = g.querySelectorAll(`${renderedEls.join(', ')}`)
113
201
 
@@ -122,64 +210,36 @@ export function cleanUpSVG(svgMarkup, {
122
210
 
123
211
 
124
212
 
125
- if (cleanupSVGAtts) {
126
- //console.log('cleanupSVGAtts');
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
-
132
- removeExcludedAttribues(svg, allowed)
133
- }
134
-
135
- // add viewBox
136
- if (addViewBox) addSvgViewBox(svg, { x, y, width, height })
137
- if (addDimensions) {
138
- svg.setAttribute('width', width);
139
- svg.setAttribute('height', height);
140
- }
141
-
142
-
143
- // remove unused defs or optimize order
144
- if (cleanupDefs) cleanupSvgDefs(svg, { x, y, width, height, cleanupClip });
145
-
146
-
147
- // remove off canvas
148
- if (removeOffCanvas) removeOffCanvasEls(svg, { x, y, width, height });
149
-
150
-
151
- // always remove scripts
152
- let removeEls = ['metadata', 'script', ...excludedEls]
153
-
154
- removeSVGEls(svg, { removeEls, removeNameSpaced });
155
-
156
- // an array of all elements' properties
213
+ // collect all elements' properties
157
214
  let svgElProps = []
158
215
  let els = svg.querySelectorAll(`${renderedEls.join(', ')}`)
159
216
 
160
217
 
218
+ /**
219
+ * loop all geometry elements
220
+ */
161
221
  for (let i = 0; i < els.length; i++) {
162
222
  let el = els[i];
163
223
 
164
224
  let name = el.nodeName.toLowerCase();
165
225
  //console.log(name);
166
226
 
167
- // 1. remove hidden elements
168
- let style = el.getAttribute('style') || ''
169
- let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
170
- let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
171
- if (name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden)) {
172
- el.remove();
173
- continue;
174
- }
175
-
176
-
177
227
  /**
178
- * get all style properties
228
+ * get all element style properties
179
229
  * convert relative or physical units
180
230
  * to user units
181
231
  */
182
232
  let styleProps = parseStylesProperties(el, propOptions)
233
+ let stylePropsFiltered = {}
234
+ //console.log('styleProps', name, styleProps);
235
+
236
+
237
+ // convert pathLength before transforming
238
+ if (convertPathLength) {
239
+ styleProps = convertPathLengthAtt(el, { styleProps });
240
+ remove = [...new Set([...remove, ...styleProps.remove])];
241
+ }
242
+
183
243
 
184
244
  // get parent styles
185
245
  let { parentStyleProps = [] } = el;
@@ -187,7 +247,10 @@ export function cleanUpSVG(svgMarkup, {
187
247
  let transFormInherited = []
188
248
 
189
249
 
190
- /** inherit transforms
250
+
251
+ /**
252
+ * consolidate all properties:
253
+ * merge with inherited transforms
191
254
  * and styles from group
192
255
  */
193
256
  parentStyleProps.forEach(props => {
@@ -203,169 +266,127 @@ export function cleanUpSVG(svgMarkup, {
203
266
  })
204
267
 
205
268
 
206
- //merge transforms
269
+ // merge all transforms
207
270
  transFormInherited = [...transFormInherited, ...styleProps.transformArr]
208
271
  styleProps.transformArr = transFormInherited
209
272
 
210
273
 
274
+ // don't inherit class from SVG
275
+ if (stylePropsSVG['class']) delete stylePropsSVG['class']
276
+ if (stylePropsSVG['id']) delete stylePropsSVG['id']
277
+
211
278
  // merge with svg props
212
279
  styleProps = {
213
280
  ...stylePropsSVG,
214
281
  ...inheritedProps,
215
282
  ...styleProps
216
283
  }
217
- //console.log('inheritedProps', inheritedProps, name);
284
+
218
285
 
219
286
  // add combined transforms
220
287
  addTransFormProps(styleProps, transFormInherited);
221
-
222
288
  //console.log('transFormInherited', transFormInherited);
223
289
  //console.log('styleProps', styleProps);
224
290
 
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;
291
+ remove = [...new Set([...remove, ...styleProps.remove])];
239
292
 
240
293
 
241
- if (stylesToAttributes) {
294
+ /**
295
+ * remove els and attributes
296
+ */
242
297
 
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);
298
+ // remove meta
299
+ if (!allowMeta) removeElements.push('meta', 'metadata', 'desc', 'title')
249
300
 
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
301
+ if (removeClassNames) {
302
+ removeSVGAttributes.push('class');
303
+ removeElAttributes.push('class');
304
+ }
256
305
 
257
- if (!needsTrans && scalableByAtt.includes(name)) {
306
+ if (removeIds) {
307
+ removeSVGAttributes.push('id')
308
+ removeElAttributes.push('id')
309
+ }
258
310
 
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
311
 
263
- if (styleProps.r) styleProps.r[0] = [styleProps.r[0] * scaleX]
312
+ // remove hidden elements
313
+ removeHiddenSvgEls(svg)
264
314
 
265
- if (styleProps.rx) styleProps.rx[0] = [styleProps.rx[0] * scaleX]
266
- if (styleProps.ry) styleProps.ry[0] = [styleProps.ry[0] * scaleX]
315
+ // remove SVG elements
316
+ removeSvgEls(svg, { removeElements, removeNameSpaced });
267
317
 
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;
318
+ // remove SVG attributes
319
+ removeSvgAtts(svg, removeSVGAttributes);
272
320
 
273
- let rx = styleProps.rx ? styleProps.rx[0] * scaleX : 0;
274
- let ry = styleProps.ry ? styleProps.ry[0] * scaleY : 0;
321
+ // remove SVG child element attributes
322
+ removeSvgChildAtts(svg, removeElAttributes);
275
323
 
276
- styleProps.x = [x]
277
- styleProps.y = [y]
278
324
 
279
- styleProps.rx = [rx]
280
- styleProps.ry = [ry]
325
+ // general cleanup
326
+ if (cleanupSVGAtts) cleanupSVGAttributes(svg, { removeIds, removeClassNames, removeDimensions, stylesToAttributes, allowMeta, allowAriaAtts, allowDataAtts });
281
327
 
282
- styleProps.width = [styleProps.width[0] * scaleX]
283
- styleProps.height = [styleProps.height[0] * scaleX]
284
- }
285
328
 
286
- remove.push('transform')
287
329
 
288
- // scale props like stroke width or dash-array
289
- styleProps = scaleProps(styleProps, { props: ['stroke-width', 'stroke-dasharray'], scale: scaleX })
330
+ if (stylesToAttributes) {
290
331
 
291
- } else {
292
- el.setAttribute('transform', transComponents.matrixAtt)
332
+ /**
333
+ * normalize transforms
334
+ */
335
+ if (normalizeTransforms) {
336
+ styleProps = setNormalizedTransformsToEl(el, { styleProps });
337
+ //remove = styleProps.remove;
338
+ remove = [...new Set([...remove, ...styleProps.remove])];
293
339
 
294
- }
295
340
  }
296
341
 
297
-
298
342
  /**
299
343
  * apply consolidated
300
344
  * element attributes
345
+ * remove non-supported element props
301
346
  */
347
+ stylePropsFiltered = filterSvgElProps(name, styleProps,
348
+ { removeDefaults: true, cleanUpStrokes, allowMeta, allowAriaAtts, allowDataAtts, removeIds });
302
349
 
303
- let stylePropsFiltered = filterSvgElProps(name, styleProps,
304
- { removeDefaults: true, cleanUpStrokes });
305
-
306
- remove = [...remove, ...stylePropsFiltered.remove];
350
+ //remove = [...remove, ...stylePropsFiltered.remove];
351
+ remove = [...new Set([...remove, ...stylePropsFiltered.remove])];
352
+ //console.log('el remove', name, remove);
353
+ //console.log('!!stylePropsFiltered', name, stylePropsFiltered);
307
354
 
308
355
  for (let prop in stylePropsFiltered.propsFiltered) {
309
356
  let values = styleProps[prop]
310
- //console.log('add', prop);
311
357
  let val = values.length ? values.join(' ') : values[0]
312
358
  el.setAttribute(prop, val)
313
359
  }
314
360
 
315
- // remove obsolete attributes
316
- for (let i = 0; i < remove.length; i++) {
317
- let att = remove[i];
318
- if (!stylesToAttributes && att === 'style') continue
319
-
320
- //console.log('remove att', att, name);
321
- el.removeAttribute(att)
322
- }
323
-
324
-
325
361
 
326
362
  /**
327
- * remove group styles
328
- * copied to children
329
- * or remove nesting
363
+ * remove obsolete
364
+ * attributes
330
365
  */
366
+ //removeSvgChildAtts(svg, remove)
331
367
 
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()
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
-
368
+ for (let i = 0; i < remove.length; i++) {
369
+ let att = remove[i];
370
+ //if (att === 'style') continue
371
+ //console.log('--remove att', remove, att, name);
372
+ el.removeAttribute(att)
349
373
  }
350
374
 
351
375
 
352
376
  } // endof style processing
353
377
 
354
378
 
379
+
355
380
  /**
356
381
  * element conversions:
357
382
  * shapes to paths or
358
383
  * paths to shapes
359
384
  */
360
385
 
361
-
362
386
  // force shape conversion when transform conversion is enabled
363
387
  if (convertTransforms) {
364
388
  shapeConvert = 'toPaths';
365
- convert_rects = true;
366
- convert_ellipses = true;
367
- convert_poly = true;
368
- convert_lines = true;
389
+ convertShapes = ['path', 'rect', 'ellipse', 'circle', 'line', 'polygon', 'polyline'];
369
390
  }
370
391
 
371
392
  // convert shapes to paths
@@ -373,8 +394,8 @@ export function cleanUpSVG(svgMarkup, {
373
394
 
374
395
  let { matrix = null, transComponents = null } = styleProps;
375
396
 
397
+ // scale props like stroke width or dash-array before conversion
376
398
  if (matrix && transComponents) {
377
- // scale props like stroke width or dash-array before conversion
378
399
  ['stroke-width', 'stroke-dasharray'].forEach(att => {
379
400
  let attVal = el.getAttribute(att)
380
401
  let vals = attVal ? attVal.split(' ').filter(Boolean).map(Number).map(val => val * transComponents.scaleX) : []
@@ -384,59 +405,310 @@ export function cleanUpSVG(svgMarkup, {
384
405
 
385
406
  // convert paths only if a matrix transform is required
386
407
  if (matrix ? geometryEls.includes(name) : shapeEls.includes(name)) {
387
-
388
- let path = shapeElToPath(el, { width, height, convert_rects, convert_ellipses, convert_poly, convert_lines, matrix });
408
+ //console.log('detrans', name, el.id, matrix);
409
+ let path = shapeElToPath(el, { width, height, convertShapes, matrix });
389
410
  el.replaceWith(path)
390
-
391
411
  name = 'path'
392
- el = path;
393
-
412
+ el = path; // required for node
394
413
 
395
414
  }
396
415
 
397
416
  }
398
417
 
399
- // convert paths to shapes
418
+ /**
419
+ * Reverse conversion:
420
+ * paths to shapes
421
+ */
400
422
  else if (shapeConvert === 'toShapes') {
401
423
  let paths = svg.querySelectorAll('path')
402
424
  paths.forEach(path => {
403
- let shape = pathElToShape(path, { convert_rects, convert_ellipses, convert_poly, convert_lines })
425
+ let shape = pathElToShape(path, { convertShapes })
404
426
  path.replaceWith(shape)
405
427
  path = shape;
428
+ //name = shape.nodeName.toLowerCase()
429
+ //console.log('shape', shape);
406
430
  })
407
-
408
431
  }
409
432
 
410
433
 
434
+ /**
435
+ * combine styles
436
+ * store in node property
437
+ */
438
+ if (mergePaths || attributesToGroup) {
439
+
440
+ let options = { allowMeta, allowAriaAtts, removeIds, removeClassNames, allowDataAtts }
441
+
442
+ /**
443
+ * exclude properties for
444
+ * adjacent path merging
445
+ * e.g ignore classnames or ids
446
+ */
447
+ if (mergePaths) {
448
+ options.removeIds = true;
449
+ options.removeClassNames = true;
450
+ options.allowAriaAtts = false;
451
+ options.allowMeta = false;
452
+ }
453
+
454
+ stylePropsFiltered = filterSvgElProps(name, styleProps, options).propsFiltered
455
+
456
+ for (let prop in stylePropsFiltered) {
457
+
458
+ if (geometryProps.includes(prop)) continue;
459
+
460
+ let values = stylePropsFiltered[prop]
461
+ let val = values.length ? values.join(' ') : values[0]
462
+
463
+ if(prop!=='class' && prop!=='id'){
464
+
465
+ let propShort = toShortStr(prop)
466
+ let valShort = toShortStr(val)
467
+ let propStr = `${propShort}-${valShort}`;
468
+
469
+ // store in node property
470
+ if (!el.styleSet) el.styleSet = new Set()
471
+ if(propStr) el.styleSet.add(propStr)
472
+ }
473
+ }
474
+
475
+ }
411
476
 
412
477
  }//endof element loop
413
478
 
414
479
 
480
+ /**
481
+ * remove group styles
482
+ * copied to children
483
+ * or remove nesting
484
+ */
485
+
486
+ if (unGroup) {
487
+ ungroupElements(groups)
488
+ } else {
489
+
490
+ if (stylesToAttributes) {
491
+ groups.forEach(g => {
492
+ removeElAtts(g, ['style', 'transform']);
493
+ })
494
+ }
495
+ //removeGroupProps(groups, { remove, allowDataAtts, allowAriaAtts })
496
+ }
415
497
 
416
498
 
499
+ // styles to group
500
+ if (attributesToGroup) sharedAttributesToGroup(svg);
501
+
502
+ /**
503
+ * merge paths with same styles
504
+ */
505
+ if (mergePaths) {
506
+ mergePathsWithSameProps(svg)
507
+ }
508
+
509
+ //console.log('svg', svg);
417
510
 
418
511
  // remove futile clip-paths
419
512
  if (cleanupClip) removeFutileClipPaths(svg, { x, y, width, height })
420
513
 
421
-
422
514
  // replace href attributes with namespace - required by many older applications
423
- if (legacyHref) {
424
- svg.setAttribute('xmlns:xlink', "http://www.w3.org/1999/xlink")
425
- let hrefs = svg.querySelectorAll('[href]')
426
- hrefs.forEach(el => {
427
- let href = el.getAttribute('href')
428
- el.setAttribute('xlink:href', href)
429
- el.removeAttribute('href')
430
- })
515
+ if (legacyHref) hrefToXlink(svg);
516
+
517
+
518
+ // remove empty class attributes
519
+ removeEmptyClassAtts(svg);
520
+ return { svg, svgElProps }
521
+
522
+ }
523
+
524
+
525
+
526
+ function removeEmptyClassAtts(svg) {
527
+ let emptyClassEls = svg.querySelectorAll('[class=""');
528
+ emptyClassEls.forEach(el => {
529
+ el.removeAttribute('class')
530
+ })
531
+ }
532
+
533
+
534
+ /**
535
+ * shared styles to group
536
+ */
537
+ function sharedAttributesToGroup(svg) {
538
+
539
+ let els = svg.querySelectorAll(renderedEls.join(', '))
540
+ let len = els.length;
541
+ if(len===1) return;
542
+
543
+ let el0 = els[0] || null
544
+ let stylePrev = el0.styleSet !== undefined ? [...el0.styleSet].join('_') : ''
545
+
546
+
547
+ // all props
548
+ let allProps = {}
549
+
550
+ // find attributes shared by all
551
+ let globalAtts = []
552
+
553
+ if (len) {
554
+
555
+ let groups = [[el0]];
556
+ let idx = 0;
557
+ let elPrev = el0
558
+
559
+ for (let i = 0; i < len; i++) {
560
+ let el = els[i];
561
+ let atts = getElementAtts(el)
562
+ for (let att in atts) {
563
+ let att_str = `${att}_${atts[att]}`
564
+
565
+ if (!allProps[att_str]) {
566
+ allProps[att_str] = []
567
+ }
568
+ allProps[att_str].push(el)
569
+ //
570
+ if (allProps[att_str].length === len) {
571
+ globalAtts.push(att)
572
+ }
573
+ }
574
+ }
575
+
576
+ //console.log('allProps', allProps);
577
+ //console.log('globalAtts', globalAtts);
578
+
579
+ // apply global to parent SVG
580
+ if (globalAtts.length) {
581
+ let atts0 = getElementAtts(el0)
582
+ for (let att in atts0) {
583
+ if (globalAtts.includes(att) && att !== 'transform') {
584
+ svg.setAttribute(att, atts0[att])
585
+ }
586
+ }
587
+ }
588
+
589
+ // detect groups
590
+ for (let i = 1; i < len; i++) {
591
+ let el = els[i];
592
+ let styleArr = el.styleSet !== undefined ? [...el.styleSet] : [];
593
+ let style = styleArr.length ? styleArr.join('_') : '';
594
+ //console.log('style', style);
595
+ //console.log('style === stylePrev', style, stylePrev);
596
+
597
+ // same style add to group
598
+ if (style === stylePrev && elPrev.nextElementSibling === el) {
599
+ groups[idx].push(el)
600
+ }
601
+ // start new group
602
+ else {
603
+ groups.push([el])
604
+ idx++
605
+ }
606
+ // update style
607
+ stylePrev = style
608
+ elPrev = el
609
+
610
+ }// endof el loop
611
+
612
+ //console.log('g', groups);
613
+
614
+ // create groups
615
+ for (let i = 0; i < groups.length; i++) {
616
+ let children = groups[i];
617
+ let child0 = children[0]
618
+ let atts = getElementAtts(child0)
619
+ let groupEl = child0.parentNode.closest('g')
620
+
621
+ // only 1 child - nothing to group
622
+ if (children.length === 1) continue
623
+
624
+
625
+ // create new group
626
+ if (!groupEl || groups.length>1) {
627
+ //console.log('new group');
628
+ groupEl = document.createElementNS(svgNs, 'g')
629
+ child0.parentNode.insertBefore(groupEl, child0)
630
+ groupEl.append(...children)
631
+ }
632
+
633
+ // move attributes to group
634
+ for (let att in atts) {
635
+ let val = atts[att];
636
+ //console.log('att', atts, val);
637
+
638
+ //|| att === 'transform'
639
+ let excludeAtts = ['id', 'class'];
640
+ if (!geometryProps.includes(att) && !excludeAtts.includes(att)) {
641
+ if (!globalAtts.includes(att) || att === 'transform') {
642
+ groupEl.setAttribute(att, val)
643
+ }
644
+ children.forEach(child => {
645
+ child.removeAttribute(att)
646
+ })
647
+ }
648
+ }
649
+
650
+
651
+ } // endof groups
652
+
431
653
  }
654
+ }
432
655
 
433
656
 
657
+ // merge adjacent paths
658
+ function mergePathsWithSameProps(svg) {
659
+ let paths = svg.querySelectorAll('path')
660
+ let len = paths.length;
434
661
 
435
- return { svg, svgElProps }
662
+ if (len) {
663
+ let path0 = paths[0]
664
+ let d0 = path0.getAttribute('d')
665
+ let stylePrev = path0.styleSet !== undefined ? [...path0.styleSet].join(' ') : ''
666
+ //console.log('path0', path0);
667
+
668
+ let remove = []
669
+
670
+ for (let i = 1; i < len; i++) {
671
+ let path = paths[i];
672
+ let style = path.styleSet !== undefined ? [...path.styleSet].join(' ') : ''
673
+ let isSibling = path.previousElementSibling === path0;
674
+ let d = path.getAttribute('d');
675
+ let isAbs = d.startsWith('M')
676
+ //console.log('path.previousElementSibling', path.previousElementSibling);
677
+ //console.log(isSibling, style, stylePrev, path.id);
678
+
679
+ if (isSibling && style === stylePrev) {
680
+ let dAbs = isAbs ? d : parsePathDataString(d).pathData.map(com => `${com.type} ${com.values.join(' ')}`).join(' ')
681
+ //console.log('same style', dAbs, isAbs);
682
+ d0 += dAbs;
683
+ path0.setAttribute('d', d0)
684
+ //console.log('remove', path);
685
+ remove.push(path)
686
+ //path.remove();
687
+
688
+ } else {
689
+ path0 = path
690
+ //console.log('path0', path0, path);
691
+ d0 = isAbs ? d : parsePathDataString(d).pathData.map(com => `${com.type} ${com.values.join(' ')}`).join(' ')
692
+
693
+ }
694
+
695
+ // update style
696
+ stylePrev = style
697
+ }
698
+
699
+
700
+ //console.log('remove', remove);
701
+ remove.forEach(el => {
702
+ el.remove()
703
+ })
704
+
705
+ }
436
706
 
437
707
  }
438
708
 
439
709
 
710
+
711
+
440
712
  function removeOffCanvasEls(svg, { x = 0, y = 0, width = 0, height = 0 } = {}) {
441
713
  let els = [...svg.querySelectorAll('path, polygon, polyline, line, rect, circle, ellipse, text')];
442
714
  els = els.filter(el => !el.parentNode.closest('defs') && !el.parentNode.closest('symbol') && !el.parentNode.closest('clipPath') && !el.parentNode.closest('mask') && !el.parentNode.closest('pattern'))
@@ -462,6 +734,13 @@ function addSvgViewBox(svg, { x = 0, y = 0, width = 0, height = 0 } = {}) {
462
734
  svg.setAttribute('viewBox', [x, y, width, height].join(' '))
463
735
  }
464
736
 
737
+ export function removeEmptySVGEls(svg) {
738
+ let els = svg.querySelectorAll('g, defs');
739
+ els.forEach(el => {
740
+ if (!el.children.length) el.remove()
741
+ })
742
+ }
743
+
465
744
 
466
745
  function cleanupSvgDefs(svg, { x = 0, y = 0, width = 0, height = 0, cleanupClip = true } = {}) {
467
746
  let defs = svg.querySelectorAll('defs')
@@ -498,9 +777,6 @@ function cleanupSvgDefs(svg, { x = 0, y = 0, width = 0, height = 0, cleanupClip
498
777
  }
499
778
  })
500
779
 
501
- // remove futile clip-paths
502
- //if (cleanupClip) removeFutileClipPaths(svg, {x, y, width, height})
503
-
504
780
  }
505
781
 
506
782
 
@@ -554,253 +830,23 @@ function removeFutileClipPaths(svg, { x = 0, y = 0, width = 0, height = 0 } = {}
554
830
  }
555
831
 
556
832
 
557
-
558
- function moveAttributesToGroup(svgElProps = [], mergePaths = true) {
559
-
560
- let combine = [[svgElProps[0]]]
561
- let idx = 0;
562
- let lastProps = '';
563
- let l = svgElProps.length;
564
- let itemsWithProps = svgElProps.filter(item => item.propstr)
565
- let path0;
566
-
567
-
568
- // merge paths without properties
569
- let dCombined = ''
570
- if (!itemsWithProps.length && mergePaths) {
571
- let path0 = null;
572
-
573
- for (let i = 0; i < l; i++) {
574
- let item = svgElProps[i]
575
- if (item.name !== 'path') continue;
576
- let remove = true;
577
-
578
-
579
- let path = item.el;
580
-
581
- // set 1st path
582
- if (!path0) {
583
- path0 = path;
584
- remove = false;
585
- }
586
-
587
- let d = item.propsFiltered.d
588
- let isAbs = d.startsWith('M')
589
- let dAbs = isAbs ? d : parsePathDataString(d).pathData.map(com => `${com.type} ${com.values.join(' ')}`).join(' ')
590
-
591
- dCombined += dAbs;
592
-
593
- // delete path el
594
- if (remove) path.remove();
595
- }
596
-
597
- //console.log('dCombined', dCombined);
598
- if (path0) path0.setAttribute('d', dCombined)
599
- return
600
- }
601
-
602
-
603
- // add to combine chunks
604
- for (let i = 0; i < l; i++) {
605
- let item = svgElProps[i];
606
- let props = item.propsFiltered;
607
- let propstr = [];
608
- for (let prop in props) {
609
- if (prop !== 'd' && prop !== 'id') {
610
- propstr.push(`${prop}:${props[prop]}`)
611
- }
612
- }
613
- propstr = propstr.join('_')
614
- item.propstr = propstr;
615
-
616
- if (l > 1 && propstr === lastProps) {
617
- combine[idx].push(item)
618
- } else {
619
- if (l > 1 && combine[idx].length) {
620
- combine.push([])
621
- idx++
622
- }
623
- }
624
- lastProps = propstr;
625
- }
626
-
627
-
628
- // add att groups
629
- for (let i = 0; i < combine.length; i++) {
630
- let group = combine[i]
631
-
632
- if (group.length > 1) {
633
- // 1st el
634
- let el0 = group[0].el;
635
- let props = group[0].propsFiltered;
636
- let g = el0.parentNode.closest('g') ? el0.parentNode.closest('g') : null;
637
-
638
- // wrap in group if not existent
639
- if (!g) {
640
- g = document.createElementNS(svgNs, 'g');
641
- el0.parentNode.insertBefore(g, el0)
642
- group.forEach(item => {
643
- g.append(item.el)
644
- })
645
- }
646
-
647
- let children = [...g.children]
648
- for (let prop in props) {
649
- if (prop !== 'd' && prop !== 'id') {
650
- let value = props[prop]
651
- // apply to parent group
652
- g.setAttribute(prop, value)
653
-
654
- // remove from children
655
- children.forEach(el => {
656
- if (el.getAttribute(prop) === value) {
657
- el.removeAttribute(prop)
658
- }
659
- })
660
- }
661
-
662
-
663
- if (mergePaths) {
664
- group = group.filter(Boolean)
665
- let l = group.length
666
- // nothing to merge
667
- if (l === 1) return group[0].el;
668
-
669
- path0 = group[0].el;
670
- let dCombined = group[0].propsFiltered.d;
671
-
672
- for (let i = 1; i < l; i++) {
673
- let item = group[i]
674
- let path = item.el;
675
- let d = item.propsFiltered.d
676
- let isAbs = d.startsWith('M')
677
-
678
- let dAbs = isAbs ? d : parsePathDataString(d).pathData.map(com => `${com.type} ${com.values.join(' ')}`).join(' ')
679
-
680
- console.log('dAbs', dAbs);
681
-
682
- //console.log(isAbs, dAbs);
683
- // concat pathdata string
684
- dCombined += dAbs;
685
-
686
- // delete path el
687
- path.remove();
688
- }
689
-
690
- path0.setAttribute('d', dCombined)
691
-
692
- }
693
-
694
- }
695
- }
696
- }
697
-
698
- }
699
-
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
- }
833
+ function hrefToXlink(svg) {
834
+ svg.setAttribute('xmlns:xlink', "http://www.w3.org/1999/xlink")
835
+ let hrefs = svg.querySelectorAll('[href]')
836
+ hrefs.forEach(el => {
837
+ let href = el.getAttribute('href')
838
+ el.setAttribute('xlink:href', href)
839
+ //el.removeAttribute('href')
726
840
  })
727
841
  }
728
842
 
729
843
 
730
- function cleanSvgPrologue(svgString) {
731
- return (
732
- svgString
733
- // Remove XML prologues like <?xml ... ?>
734
- .replace(/<\?xml[\s\S]*?\?>/gi, "")
735
- // Remove DOCTYPE declarations
736
- .replace(/<!DOCTYPE[\s\S]*?>/gi, "")
737
- // Remove comments <!-- ... -->
738
- .replace(/<!--[\s\S]*?-->/g, "")
739
- // Trim extra whitespace
740
- .trim()
741
- );
742
- }
743
-
744
- function removeExcludedAttribues(el, allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class']) {
745
- let atts = [...el.attributes].map((att) => att.name);
746
- atts.forEach((att) => {
747
- if (!allowed.includes(att)) {
748
- el.removeAttribute(att);
749
- }
750
- });
751
- }
752
-
753
-
754
- function removeAtts(el, exclude = [], include = []) {
755
- let atts = [...el.attributes].map((att) => att.name);
756
- atts.forEach((att) => {
757
- if (exclude.includes(att) && !include.includes(att)) {
758
- el.removeAttribute(att);
759
- }
760
- });
761
- }
762
-
763
-
764
- function removeNameSpaceAtts(el, {
765
- include = ['xlink:href']
766
- } = {}) {
767
- let atts = [...el.attributes].map((att) => att.name);
768
- atts.forEach((att) => {
769
- if (att.includes(":") && !include.includes(att)) {
770
- el.removeAttribute(att);
771
- }
772
- });
773
- }
774
844
 
775
- export function stringifySVG(svg, {
776
- omitNamespace = false,
777
- removeComments = true,
778
- } = {}) {
779
- let markup = new XMLSerializer().serializeToString(svg);
780
845
 
781
- if (omitNamespace) {
782
- markup = markup.replaceAll('xmlns="http://www.w3.org/2000/svg"', '')
783
- }
784
846
 
785
- if (removeComments) {
786
- markup = markup
787
- .replace(/(<!--.*?-->)|(<!--[\S\s]+?-->)|(<!--[\S\s]*?$)/g, '')
788
- }
789
847
 
790
- markup = markup
791
- .replace(/\t/g, "")
792
- .replace(/[\n\r|]/g, "\n")
793
- .replace(/\n\s*\n/g, '\n')
794
- .replace(/ +/g, ' ')
795
- //.replace(/ +/g, ' ')
796
- .replace(/> </g, '><')
797
- .trim()
798
- // sanitize linebreaks within pathdata
799
- .replaceAll('&#10;', '\n');
800
848
 
801
849
 
802
850
 
803
851
 
804
852
 
805
- return markup
806
- }