svg-path-simplify 0.4.3 → 0.4.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/CHANGELOG.md +11 -0
- package/README.md +1 -0
- package/dist/svg-path-simplify.esm.js +1610 -495
- package/dist/svg-path-simplify.esm.min.js +2 -2
- package/dist/svg-path-simplify.js +1611 -494
- package/dist/svg-path-simplify.min.js +2 -2
- package/dist/svg-path-simplify.pathdata.esm.js +893 -456
- package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
- package/dist/svg-path-simplify.poly.cjs +9 -8
- package/index.html +58 -17
- package/package.json +1 -1
- package/src/constants.js +4 -0
- package/src/detect_input.js +47 -29
- package/src/index.js +8 -0
- package/src/pathData_simplify_cubic.js +26 -16
- package/src/pathData_simplify_revertToquadratics.js +0 -1
- package/src/pathSimplify-main.js +75 -20
- package/src/pathSimplify-only-pathdata.js +7 -2
- package/src/pathSimplify-presets.js +15 -4
- package/src/svg-getAttributes.js +4 -2
- package/src/svgii/convert_units.js +1 -1
- package/src/svgii/geometry.js +140 -2
- package/src/svgii/geometry_bbox_element.js +1 -1
- package/src/svgii/geometry_deduceRadius.js +116 -27
- package/src/svgii/geometry_length.js +17 -1
- package/src/svgii/pathData_analyze.js +18 -0
- package/src/svgii/pathData_convert.js +188 -88
- package/src/svgii/pathData_fix_directions.js +10 -18
- package/src/svgii/pathData_reorder.js +122 -16
- package/src/svgii/pathData_simplify_refineCorners.js +130 -35
- package/src/svgii/pathData_simplify_refine_round.js +420 -0
- package/src/svgii/rounding.js +79 -78
- package/src/svgii/svg_cleanup.js +68 -20
- package/src/svgii/svg_cleanup_convertPathLength.js +22 -15
- package/src/svgii/svg_cleanup_remove_els_and_atts.js +6 -1
- package/src/svgii/svg_el_parse_style_props.js +13 -10
- package/src/svgii/svg_validate.js +220 -0
- package/tests/testSVG.js +14 -1
- package/src/svgii/pathData_refine_round.js +0 -222
package/src/svgii/svg_cleanup.js
CHANGED
|
@@ -17,7 +17,7 @@ import { qrDecomposeMatrix } from "./transform_qr_decompose";
|
|
|
17
17
|
import { svgNs } from "../constants";
|
|
18
18
|
import { toCamelCase, toShortStr } from "../string_helpers";
|
|
19
19
|
import { getElementLength } from "./svg_getElementLength";
|
|
20
|
-
import { removeHiddenSvgEls, removeSvgAtts, removeSvgChildAtts, removeSvgEls } from "./svg_cleanup_remove_els_and_atts";
|
|
20
|
+
import { removeAtts, removeHiddenSvgEls, removeSvgAtts, removeSvgChildAtts, removeSvgEls } from "./svg_cleanup_remove_els_and_atts";
|
|
21
21
|
import { cleanupSVGAttributes, removeElAtts } from "./svg_cleanup_general_svg_atts";
|
|
22
22
|
import { convertPathLengthAtt } from "./svg_cleanup_convertPathLength";
|
|
23
23
|
import { removeGroupProps, ungroupElements } from "./svg_cleanup_ungroup";
|
|
@@ -54,7 +54,10 @@ export function cleanUpSVG(svgMarkup, {
|
|
|
54
54
|
cleanupSVGAtts = true,
|
|
55
55
|
removeNameSpaced = true,
|
|
56
56
|
removeNameSpacedAtts = true,
|
|
57
|
+
|
|
58
|
+
// unit conversions
|
|
57
59
|
convertPathLength = false,
|
|
60
|
+
toAbsoluteUnits = false,
|
|
58
61
|
|
|
59
62
|
// meta
|
|
60
63
|
allowMeta = false,
|
|
@@ -82,10 +85,10 @@ export function cleanUpSVG(svgMarkup, {
|
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
// resolve dependencies
|
|
85
|
-
if (unGroup || convertTransforms || minifyRgbColors || attributesToGroup)
|
|
86
|
-
|
|
88
|
+
if (unGroup || convertTransforms || minifyRgbColors || attributesToGroup)
|
|
89
|
+
stylesToAttributes = true;
|
|
87
90
|
|
|
88
|
-
if(stylesToAttributes) cleanUpStrokes = true;
|
|
91
|
+
if (stylesToAttributes) cleanUpStrokes = true;
|
|
89
92
|
|
|
90
93
|
// replace namespaced refs
|
|
91
94
|
if (fixHref) svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
|
|
@@ -136,7 +139,7 @@ export function cleanUpSVG(svgMarkup, {
|
|
|
136
139
|
removeClassNames,
|
|
137
140
|
minifyRgbColors,
|
|
138
141
|
stylesheetProps: {},
|
|
139
|
-
exclude:[]
|
|
142
|
+
exclude: []
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
// root svg inline style properties
|
|
@@ -275,9 +278,16 @@ export function cleanUpSVG(svgMarkup, {
|
|
|
275
278
|
if (stylePropsSVG['class']) delete stylePropsSVG['class']
|
|
276
279
|
if (stylePropsSVG['id']) delete stylePropsSVG['id']
|
|
277
280
|
|
|
281
|
+
// add svg props
|
|
282
|
+
inheritedProps = {
|
|
283
|
+
...stylePropsSVG,
|
|
284
|
+
...inheritedProps,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
//console.log('inheritedProps', inheritedProps);
|
|
288
|
+
|
|
278
289
|
// merge with svg props
|
|
279
290
|
styleProps = {
|
|
280
|
-
...stylePropsSVG,
|
|
281
291
|
...inheritedProps,
|
|
282
292
|
...styleProps
|
|
283
293
|
}
|
|
@@ -287,7 +297,6 @@ export function cleanUpSVG(svgMarkup, {
|
|
|
287
297
|
addTransFormProps(styleProps, transFormInherited);
|
|
288
298
|
//console.log('transFormInherited', transFormInherited);
|
|
289
299
|
//console.log('styleProps', styleProps);
|
|
290
|
-
|
|
291
300
|
remove = [...new Set([...remove, ...styleProps.remove])];
|
|
292
301
|
|
|
293
302
|
|
|
@@ -325,6 +334,46 @@ export function cleanUpSVG(svgMarkup, {
|
|
|
325
334
|
// general cleanup
|
|
326
335
|
if (cleanupSVGAtts) cleanupSVGAttributes(svg, { removeIds, removeClassNames, removeDimensions, stylesToAttributes, allowMeta, allowAriaAtts, allowDataAtts });
|
|
327
336
|
|
|
337
|
+
// all relative units to absolute
|
|
338
|
+
if (toAbsoluteUnits) {
|
|
339
|
+
normalizeTransforms = true;
|
|
340
|
+
//stylesToAttributes = true
|
|
341
|
+
//console.log(name, styleProps);
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* apply consolidated
|
|
345
|
+
* element attributes
|
|
346
|
+
* remove non-supported element props
|
|
347
|
+
*/
|
|
348
|
+
stylePropsFiltered = filterSvgElProps(name, styleProps,
|
|
349
|
+
{ removeDefaults: true, cleanUpStrokes, allowMeta, allowAriaAtts, allowDataAtts, removeIds, inheritedProps });
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
for (let prop in stylePropsFiltered.propsFiltered) {
|
|
353
|
+
let values = styleProps[prop]
|
|
354
|
+
let val = values.length ? values.join(' ') : values[0]
|
|
355
|
+
el.setAttribute(prop, val)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
//console.log('inheritedProps', inheritedProps);
|
|
359
|
+
//console.log('current props', stylePropsFiltered.propsFiltered);
|
|
360
|
+
|
|
361
|
+
let removeAttsEl = [...new Set([...remove, ...stylePropsFiltered.remove])];
|
|
362
|
+
|
|
363
|
+
// check if same value is in inherited
|
|
364
|
+
for (let prop in stylePropsFiltered.propsFiltered) {
|
|
365
|
+
let valInh = inheritedProps[prop] || [];
|
|
366
|
+
let val = stylePropsFiltered.propsFiltered[prop] || [];
|
|
367
|
+
if (valInh.join() === val.join()) {
|
|
368
|
+
removeAttsEl.push(prop)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// remove obsolete/inherited
|
|
373
|
+
removeAtts(el, removeAttsEl)
|
|
374
|
+
|
|
375
|
+
}
|
|
376
|
+
|
|
328
377
|
|
|
329
378
|
|
|
330
379
|
if (stylesToAttributes) {
|
|
@@ -345,12 +394,9 @@ export function cleanUpSVG(svgMarkup, {
|
|
|
345
394
|
* remove non-supported element props
|
|
346
395
|
*/
|
|
347
396
|
stylePropsFiltered = filterSvgElProps(name, styleProps,
|
|
348
|
-
{ removeDefaults: true, cleanUpStrokes, allowMeta, allowAriaAtts, allowDataAtts, removeIds });
|
|
397
|
+
{ removeDefaults: true, cleanUpStrokes, allowMeta, allowAriaAtts, allowDataAtts, removeIds, inheritedProps });
|
|
349
398
|
|
|
350
|
-
//remove = [...remove, ...stylePropsFiltered.remove];
|
|
351
399
|
remove = [...new Set([...remove, ...stylePropsFiltered.remove])];
|
|
352
|
-
//console.log('el remove', name, remove);
|
|
353
|
-
//console.log('!!stylePropsFiltered', name, stylePropsFiltered);
|
|
354
400
|
|
|
355
401
|
for (let prop in stylePropsFiltered.propsFiltered) {
|
|
356
402
|
let values = styleProps[prop]
|
|
@@ -363,14 +409,14 @@ export function cleanUpSVG(svgMarkup, {
|
|
|
363
409
|
* remove obsolete
|
|
364
410
|
* attributes
|
|
365
411
|
*/
|
|
366
|
-
|
|
412
|
+
removeAtts(el, remove)
|
|
367
413
|
|
|
414
|
+
/*
|
|
368
415
|
for (let i = 0; i < remove.length; i++) {
|
|
369
416
|
let att = remove[i];
|
|
370
|
-
//if (att === 'style') continue
|
|
371
|
-
//console.log('--remove att', remove, att, name);
|
|
372
417
|
el.removeAttribute(att)
|
|
373
418
|
}
|
|
419
|
+
*/
|
|
374
420
|
|
|
375
421
|
|
|
376
422
|
} // endof style processing
|
|
@@ -460,15 +506,15 @@ export function cleanUpSVG(svgMarkup, {
|
|
|
460
506
|
let values = stylePropsFiltered[prop]
|
|
461
507
|
let val = values.length ? values.join(' ') : values[0]
|
|
462
508
|
|
|
463
|
-
if(prop!=='class' && prop!=='id'){
|
|
509
|
+
if (prop !== 'class' && prop !== 'id') {
|
|
464
510
|
|
|
465
511
|
let propShort = toShortStr(prop)
|
|
466
512
|
let valShort = toShortStr(val)
|
|
467
513
|
let propStr = `${propShort}-${valShort}`;
|
|
468
|
-
|
|
514
|
+
|
|
469
515
|
// store in node property
|
|
470
516
|
if (!el.styleSet) el.styleSet = new Set()
|
|
471
|
-
if(propStr) el.styleSet.add(propStr)
|
|
517
|
+
if (propStr) el.styleSet.add(propStr)
|
|
472
518
|
}
|
|
473
519
|
}
|
|
474
520
|
|
|
@@ -524,7 +570,7 @@ export function cleanUpSVG(svgMarkup, {
|
|
|
524
570
|
|
|
525
571
|
|
|
526
572
|
function removeEmptyClassAtts(svg) {
|
|
527
|
-
let emptyClassEls = svg.querySelectorAll('[class=""');
|
|
573
|
+
let emptyClassEls = svg.querySelectorAll('[class=""]');
|
|
528
574
|
emptyClassEls.forEach(el => {
|
|
529
575
|
el.removeAttribute('class')
|
|
530
576
|
})
|
|
@@ -538,7 +584,7 @@ function sharedAttributesToGroup(svg) {
|
|
|
538
584
|
|
|
539
585
|
let els = svg.querySelectorAll(renderedEls.join(', '))
|
|
540
586
|
let len = els.length;
|
|
541
|
-
if(len===1) return;
|
|
587
|
+
if (len === 1) return;
|
|
542
588
|
|
|
543
589
|
let el0 = els[0] || null
|
|
544
590
|
let stylePrev = el0.styleSet !== undefined ? [...el0.styleSet].join('_') : ''
|
|
@@ -623,7 +669,7 @@ function sharedAttributesToGroup(svg) {
|
|
|
623
669
|
|
|
624
670
|
|
|
625
671
|
// create new group
|
|
626
|
-
if (!groupEl || groups.length>1) {
|
|
672
|
+
if (!groupEl || groups.length > 1) {
|
|
627
673
|
//console.log('new group');
|
|
628
674
|
groupEl = document.createElementNS(svgNs, 'g')
|
|
629
675
|
child0.parentNode.insertBefore(groupEl, child0)
|
|
@@ -719,7 +765,9 @@ function removeOffCanvasEls(svg, { x = 0, y = 0, width = 0, height = 0 } = {}) {
|
|
|
719
765
|
bb0.bottom = y + height
|
|
720
766
|
|
|
721
767
|
els.forEach(el => {
|
|
768
|
+
//console.log(el);
|
|
722
769
|
let bb = getElBBox(el)
|
|
770
|
+
//console.log('!!bb', bb);
|
|
723
771
|
let outside = bb.right < bb0.x || bb.bottom < bb0.y || bb.x > bb0.right || bb.y > bb.bottom
|
|
724
772
|
if (outside) el.remove();
|
|
725
773
|
})
|
|
@@ -1,29 +1,36 @@
|
|
|
1
|
+
import { scaleProps } from "./svg_cleanup_normalize_transforms";
|
|
2
|
+
import { getElementLength } from "./svg_getElementLength";
|
|
3
|
+
|
|
1
4
|
export function convertPathLengthAtt(el, {
|
|
2
5
|
styleProps = {}
|
|
3
|
-
}={}) {
|
|
6
|
+
} = {}) {
|
|
7
|
+
|
|
8
|
+
let pathLength = styleProps['pathLength'];
|
|
9
|
+
|
|
10
|
+
if (pathLength) {
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
})
|
|
12
|
+
//let strokeDasharray
|
|
13
|
+
if ((styleProps['stroke-dasharray'] || styleProps['stroke-dashoffset'])) {
|
|
14
|
+
let elLength = getElementLength(el, {
|
|
15
|
+
pathLength,
|
|
16
|
+
props: styleProps
|
|
17
|
+
})
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
let scale = elLength / pathLength
|
|
20
|
+
styleProps = scaleProps(styleProps, { props: ['stroke-dasharray', 'stroke-dashoffset'], scale })
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
// set absolute
|
|
23
|
+
if (styleProps['stroke-dasharray']) el.setAttribute('stroke-dasharray', styleProps['stroke-dasharray'].join(' '))
|
|
24
|
+
if (styleProps['stroke-dashoffset']) el.setAttribute('stroke-dashoffset', styleProps['stroke-dashoffset'][0])
|
|
25
|
+
|
|
26
|
+
}
|
|
20
27
|
|
|
21
28
|
// tag for removal
|
|
22
29
|
delete styleProps['pathLength'];
|
|
23
30
|
styleProps.remove.push('pathLength')
|
|
24
31
|
el.removeAttribute('pathLength')
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
return styleProps;
|
|
@@ -51,11 +51,16 @@ export function removeSvgEls(svg, {
|
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
53
|
export function removeSvgAtts(svg, remove = []) {
|
|
54
|
+
removeAtts(svg, remove)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function removeAtts(el, remove = []) {
|
|
54
58
|
remove.forEach(att => {
|
|
55
|
-
|
|
59
|
+
el.removeAttribute(att);
|
|
56
60
|
})
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
|
|
59
64
|
export function removeSvgChildAtts(svg, remove = []) {
|
|
60
65
|
if (remove.length) {
|
|
61
66
|
let selector = remove.map(att => { return `[${att}]` }).join(', ')
|
|
@@ -18,8 +18,8 @@ export function parseStylesProperties(el, {
|
|
|
18
18
|
autoRoundValues = false,
|
|
19
19
|
minifyRgbColors = false,
|
|
20
20
|
removeInvalid = true,
|
|
21
|
-
allowDataAtts=true,
|
|
22
|
-
allowAriaAtts=true,
|
|
21
|
+
allowDataAtts = true,
|
|
22
|
+
allowAriaAtts = true,
|
|
23
23
|
removeDefaults = true,
|
|
24
24
|
cleanUpStrokes = true,
|
|
25
25
|
normalizeTransforms = true,
|
|
@@ -94,7 +94,7 @@ export function parseStylesProperties(el, {
|
|
|
94
94
|
*/
|
|
95
95
|
|
|
96
96
|
if (removeInvalid || removeDefaults || removeNameSpaced) {
|
|
97
|
-
let propsFilteredObj = filterSvgElProps(nodeName, props, {allowDataAtts, allowAriaAtts, removeIds, removeClassNames, removeDefaults, removeNameSpaced, exclude, cleanUpStrokes, include: [...transformsStandalone, ...include], cleanUpStrokes: false })
|
|
97
|
+
let propsFilteredObj = filterSvgElProps(nodeName, props, { allowDataAtts, allowAriaAtts, removeIds, removeClassNames, removeDefaults, removeNameSpaced, exclude, cleanUpStrokes, include: [...transformsStandalone, ...include], cleanUpStrokes: false })
|
|
98
98
|
props = propsFilteredObj.propsFiltered
|
|
99
99
|
remove.push(...propsFilteredObj.remove)
|
|
100
100
|
//console.log(propsFilteredObj.remove, allowDataAtts, allowAriaAtts);
|
|
@@ -196,10 +196,11 @@ export function parseStylesProperties(el, {
|
|
|
196
196
|
|
|
197
197
|
if (prop !== 'transforms') {
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
//cleanUpStrokes &&
|
|
200
|
+
if ((prop === 'stroke-dasharray' || prop === 'stroke-dashoffset')) {
|
|
200
201
|
normalizedDiagonal = true
|
|
201
202
|
for (let i = 0; i < values.length; i++) {
|
|
202
|
-
let val = normalizeUnits(values[i].value, { unit: values[i].unit, width, height, normalizedDiagonal, fontSize })
|
|
203
|
+
let val = normalizeUnits(values[i].value, { unit: values[i].unit, width, height, normalizedDiagonal, fontSize, autoRoundValues })
|
|
203
204
|
valsNew.push(val)
|
|
204
205
|
}
|
|
205
206
|
}
|
|
@@ -244,15 +245,12 @@ export function parseStylesProperties(el, {
|
|
|
244
245
|
if (prop === 'scale' && unit === '%') {
|
|
245
246
|
valAbs = valAbs * 0.01;
|
|
246
247
|
} else {
|
|
247
|
-
if (prop === 'r')
|
|
248
|
+
if (prop === 'r' && width!==height) normalizedDiagonal = true;
|
|
248
249
|
valAbs = normalizeUnits(val.value, { unit, width, height, isHorizontal, isVertical, normalizedDiagonal, fontSize })
|
|
249
250
|
|
|
250
251
|
if (autoRoundValues && isNumeric) {
|
|
251
252
|
valAbs = autoRound(valAbs)
|
|
252
|
-
//valAbs = roundTo(valAbs, 3)
|
|
253
253
|
}
|
|
254
|
-
|
|
255
|
-
//console.log('norm', prop, valAbs, 'val', val, unit, isHorizontal, isVertical, width, height, 'isNumeric', isNumeric);
|
|
256
254
|
}
|
|
257
255
|
}
|
|
258
256
|
valsNew.push(valAbs)
|
|
@@ -466,6 +464,7 @@ export function filterSvgElProps(elNodename = '', props = {}, {
|
|
|
466
464
|
removeIds = false,
|
|
467
465
|
removeClassNames = false,
|
|
468
466
|
exclude = [],
|
|
467
|
+
inheritedProps = null,
|
|
469
468
|
} = {}) {
|
|
470
469
|
let propsFiltered = {}
|
|
471
470
|
let remove = [];
|
|
@@ -474,6 +473,7 @@ export function filterSvgElProps(elNodename = '', props = {}, {
|
|
|
474
473
|
if (!removeClassNames) include.push('class')
|
|
475
474
|
//console.log('???include', 'removeIds', removeIds, include);
|
|
476
475
|
|
|
476
|
+
|
|
477
477
|
// allow defaults for nested
|
|
478
478
|
//removeDefaults = false;
|
|
479
479
|
|
|
@@ -490,6 +490,7 @@ export function filterSvgElProps(elNodename = '', props = {}, {
|
|
|
490
490
|
(attLookup.atts[prop] ? attLookup.atts[prop].includes(elNodename) : false) :
|
|
491
491
|
false;
|
|
492
492
|
|
|
493
|
+
|
|
493
494
|
// remove null transforms
|
|
494
495
|
if (prop === 'transform' && value === 'matrix(1 0 0 1 0 0)') isValid = false;
|
|
495
496
|
|
|
@@ -498,7 +499,7 @@ export function filterSvgElProps(elNodename = '', props = {}, {
|
|
|
498
499
|
let isMeta = prop === 'title'
|
|
499
500
|
let isAria = prop.startsWith('aria-')
|
|
500
501
|
|
|
501
|
-
if
|
|
502
|
+
if ((allowDataAtts && isDataAtt) || (allowAriaAtts && isAria) || (allowMeta && isMeta)) continue
|
|
502
503
|
|
|
503
504
|
// filter out defaults
|
|
504
505
|
let isDefault = removeDefaults ?
|
|
@@ -509,6 +510,8 @@ export function filterSvgElProps(elNodename = '', props = {}, {
|
|
|
509
510
|
|
|
510
511
|
if (isDefault || isDataAtt || isMeta || isAria || isFutileStroke) isValid = false
|
|
511
512
|
if (include.includes(prop)) isValid = true;
|
|
513
|
+
if (exclude.includes(prop)) isValid = false
|
|
514
|
+
|
|
512
515
|
|
|
513
516
|
if (isValid) {
|
|
514
517
|
propsFiltered[prop] = props[prop]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export function validateSVG(markup, allowed = {}) {
|
|
4
|
+
allowed = {
|
|
5
|
+
...{
|
|
6
|
+
//useEls: 10,
|
|
7
|
+
//hasPrologue: false,
|
|
8
|
+
//hasXmlns: true,
|
|
9
|
+
useElsNested: 5000,
|
|
10
|
+
hasScripts: false,
|
|
11
|
+
hasEntity: false,
|
|
12
|
+
fileSizeKB: 10000,
|
|
13
|
+
isSymbolSprite: false,
|
|
14
|
+
isSvgFont: false
|
|
15
|
+
},
|
|
16
|
+
...allowed
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
let fileReport = analyzeSVG(markup, allowed);
|
|
21
|
+
let isValid = true;
|
|
22
|
+
let log = [];
|
|
23
|
+
|
|
24
|
+
if (!fileReport.hasEls) {
|
|
25
|
+
log.push("no elements");
|
|
26
|
+
isValid = false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (Object.keys(fileReport).length) {
|
|
30
|
+
if (fileReport.isBillionLaugh === true) {
|
|
31
|
+
log.push(`suspicious: might contain billion laugh attack`);
|
|
32
|
+
isValid = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (let key in allowed) {
|
|
36
|
+
let val = allowed[key];
|
|
37
|
+
let valRep = fileReport[key];
|
|
38
|
+
if (typeof val === "number" && valRep > val) {
|
|
39
|
+
log.push(`allowed "${key}" exceeded: ${valRep} / ${val} `);
|
|
40
|
+
isValid = false;
|
|
41
|
+
}
|
|
42
|
+
if (valRep === true && val === false) {
|
|
43
|
+
log.push(`not allowed: "${key}" `);
|
|
44
|
+
isValid = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
isValid = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
if (!isValid) {
|
|
53
|
+
log = ["SVG not valid"].concat(log);
|
|
54
|
+
//console.log(log.join("\n"));
|
|
55
|
+
if (Object.keys(fileReport).length) {
|
|
56
|
+
console.warn(fileReport);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
return { isValid, log, fileReport };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function analyzeSVG(markup, allowed = {}) {
|
|
65
|
+
markup = markup.trim();
|
|
66
|
+
let doc, svg;
|
|
67
|
+
let fileSizeKB = +(markup.length / 1024).toFixed(3);
|
|
68
|
+
|
|
69
|
+
let fileReport = {
|
|
70
|
+
totalEls: 1,
|
|
71
|
+
hasEls: true,
|
|
72
|
+
hasDefs: false,
|
|
73
|
+
geometryEls: [],
|
|
74
|
+
useEls: 0,
|
|
75
|
+
useElsNested: 0,
|
|
76
|
+
nonsensePaths: 0,
|
|
77
|
+
isSuspicious: false,
|
|
78
|
+
isBillionLaugh: false,
|
|
79
|
+
hasScripts: false,
|
|
80
|
+
hasPrologue: false,
|
|
81
|
+
hasEntity: false,
|
|
82
|
+
isPathData:false,
|
|
83
|
+
fileSizeKB,
|
|
84
|
+
hasXmlns: markup.includes("http://www.w3.org/2000/svg"),
|
|
85
|
+
isSymbolSprite: false,
|
|
86
|
+
isSvgFont: markup.includes("<glyph>")
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
let maxNested = allowed.useElsNested ? allowed.useElsNested : 2000;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* analyze nestes use references
|
|
94
|
+
*/
|
|
95
|
+
const countUseRefs = (useEls, maxNested = 2000) => {
|
|
96
|
+
let nestedCount = 0;
|
|
97
|
+
//stop loop if number of nested use references is exceeded
|
|
98
|
+
for (let i = 0; i < useEls.length && nestedCount < maxNested; i++) {
|
|
99
|
+
let use = useEls[i];
|
|
100
|
+
let refId = use.getAttribute("xlink:href")
|
|
101
|
+
? use.getAttribute("xlink:href")
|
|
102
|
+
: use.getAttribute("href");
|
|
103
|
+
refId = refId ? refId.replace("#", "") : "";
|
|
104
|
+
|
|
105
|
+
//normalize href attributes to facilitate JS selection
|
|
106
|
+
use.setAttribute("href", "#" + refId);
|
|
107
|
+
|
|
108
|
+
let refEl = svg.getElementById(refId);
|
|
109
|
+
let nestedUse = refEl.querySelectorAll("use");
|
|
110
|
+
let nestedUseLength = nestedUse.length;
|
|
111
|
+
nestedCount += nestedUseLength;
|
|
112
|
+
|
|
113
|
+
// query nested use references
|
|
114
|
+
for (let n = 0; n < nestedUse.length && nestedCount < maxNested; n++) {
|
|
115
|
+
let nested = nestedUse[n];
|
|
116
|
+
let id1 = nested.getAttribute("href").replace("#", "");
|
|
117
|
+
let refEl1 = svg.getElementById(id1);
|
|
118
|
+
let nestedUse1 = refEl1.querySelectorAll("use");
|
|
119
|
+
nestedCount += nestedUse1.length;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
fileReport.useElsNested = nestedCount;
|
|
123
|
+
return nestedCount;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* check on raw text level
|
|
128
|
+
*/
|
|
129
|
+
let hasPrologue = /\<\?xml.+\?\>|\<\!DOCTYPE.+]\>/g.test(markup);
|
|
130
|
+
let hasEntity = /\<\!ENTITY/gi.test(markup);
|
|
131
|
+
let hasScripts = /\<script/gi.test(markup) ? true : false;
|
|
132
|
+
let hasUse = /\<use/gi.test(markup) ? true : false;
|
|
133
|
+
let hasEls = /[\<path|\<polygon|\<polyline|\<rect|\<circle|\<ellipse|\<line|\<text|\<foreignObject]/gi.test(markup);
|
|
134
|
+
let hasDefs = /[\<filter|\<linearGradient|\<radialGradient|\<pattern|\<animate|\<animateMotion|\<animateTransform|\<clipPath|\<mask|\<symbol|\<marker]/gi.test(markup);
|
|
135
|
+
|
|
136
|
+
let isPathData = (markup.startsWith('M') || markup.startsWith('m')) && !/[\<svg|\<\/svg]/gi.test(markup);
|
|
137
|
+
fileReport.isPathData = isPathData;
|
|
138
|
+
|
|
139
|
+
// seems OK
|
|
140
|
+
if (!hasEntity && !hasUse && !hasScripts && (hasEls || hasDefs) && fileSizeKB < allowed.fileSizeKB) {
|
|
141
|
+
fileReport.hasEls = hasEls
|
|
142
|
+
fileReport.hasDefs = hasDefs
|
|
143
|
+
//console.log('Looks OK!', fileReport, allowed);
|
|
144
|
+
return fileReport
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
// Contains xml entity definition: highly suspicious - stop parsing!
|
|
149
|
+
if (allowed.hasEntity === false && hasEntity) {
|
|
150
|
+
fileReport.hasEntity = true;
|
|
151
|
+
//return fileReport;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* sanitizing for parsing:
|
|
156
|
+
* remove xml prologue and comments
|
|
157
|
+
*/
|
|
158
|
+
markup = markup
|
|
159
|
+
.replace(/\<\?xml.+\?\>|\<\!DOCTYPE.+]\>/g, "")
|
|
160
|
+
.replace(/(<!--.*?-->)|(<!--[\S\s]+?-->)|(<!--[\S\s]*?$)/g, "");
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Try to parse svg:
|
|
164
|
+
* invalid svg will return false via "catch"
|
|
165
|
+
*/
|
|
166
|
+
try {
|
|
167
|
+
//doc = new DOMParser().parseFromString(markup, "image/svg+xml");
|
|
168
|
+
doc = new DOMParser().parseFromString(markup, "text/html");
|
|
169
|
+
svg = doc.querySelector("svg");
|
|
170
|
+
|
|
171
|
+
// paths containing only a M command
|
|
172
|
+
let nonsensePaths = svg.querySelectorAll('path[d="M0,0"], path[d="M0 0"]').length;
|
|
173
|
+
let useEls = svg.querySelectorAll("use").length;
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
// create analyzing object
|
|
177
|
+
fileReport.totalEls = svg.querySelectorAll("*").length;
|
|
178
|
+
fileReport.geometryEls = svg.querySelectorAll(
|
|
179
|
+
"path, rect, circle, ellipse, polygon, polyline, line"
|
|
180
|
+
).length
|
|
181
|
+
|
|
182
|
+
fileReport.hasScripts = hasScripts
|
|
183
|
+
fileReport.useEls = useEls;
|
|
184
|
+
fileReport.nonsensePaths = nonsensePaths;
|
|
185
|
+
fileReport.isSuspicious = false;
|
|
186
|
+
fileReport.isBillionLaugh = false;
|
|
187
|
+
fileReport.hasXmlns = svg.getAttribute("xmlns")
|
|
188
|
+
? svg.getAttribute("xmlns") === "http://www.w3.org/2000/svg"
|
|
189
|
+
? true
|
|
190
|
+
: false
|
|
191
|
+
: false;
|
|
192
|
+
fileReport.isSymbolSprite =
|
|
193
|
+
svg.querySelectorAll("symbol").length &&
|
|
194
|
+
svg.querySelectorAll("use").length === 0
|
|
195
|
+
? true
|
|
196
|
+
: false;
|
|
197
|
+
fileReport.isSvgFont = svg.querySelectorAll("glyph").length ? true : false;
|
|
198
|
+
|
|
199
|
+
let totalEls = fileReport.totalEls;
|
|
200
|
+
let totalUseEls = fileReport.useEls;
|
|
201
|
+
let usePercentage = (100 / totalEls) * totalUseEls;
|
|
202
|
+
|
|
203
|
+
// if percentage of use elements is higher than 75% - suspicious
|
|
204
|
+
if (usePercentage > 75) {
|
|
205
|
+
fileReport.isSuspicious = true;
|
|
206
|
+
|
|
207
|
+
// check nested use references
|
|
208
|
+
let nestedCount = countUseRefs(svg.querySelectorAll("use"), maxNested);
|
|
209
|
+
if (nestedCount >= maxNested) {
|
|
210
|
+
fileReport.isBillionLaugh = true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return fileReport;
|
|
215
|
+
} catch {
|
|
216
|
+
// svg file has malformed markup
|
|
217
|
+
console.warn("svg could not be parsed");
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
package/tests/testSVG.js
CHANGED
|
@@ -19,8 +19,21 @@ let svgMarkup =
|
|
|
19
19
|
</g>
|
|
20
20
|
</svg>`
|
|
21
21
|
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
let document = new DOMParser().parseFromString(svgMarkup, 'image/svg+xml');
|
|
25
|
+
let svg = document.querySelector('svg');
|
|
26
|
+
let path = svg.querySelector('path');
|
|
27
|
+
let els = svg.querySelectorAll('path')
|
|
28
|
+
let d = path.getAttribute('d').substring(0, 10)
|
|
29
|
+
console.log(d);
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
22
35
|
// try to simplify
|
|
23
|
-
let svgOpt = svgPathSimplify(svgMarkup, {
|
|
36
|
+
let svgOpt = svgPathSimplify(svgMarkup, {preset:'high'});
|
|
24
37
|
|
|
25
38
|
// simplified pathData
|
|
26
39
|
console.log(svgOpt)
|