svg-path-simplify 0.4.2 → 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.
Files changed (61) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +7 -4
  3. package/dist/svg-path-simplify.esm.js +3593 -1279
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +3594 -1278
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +1017 -538
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/dist/svg-path-simplify.poly.cjs +9 -8
  10. package/docs/privacy-webapp.md +24 -0
  11. package/index.html +331 -152
  12. package/package.json +1 -1
  13. package/src/constants.js +4 -0
  14. package/src/css_parse.js +317 -0
  15. package/src/detect_input.js +76 -28
  16. package/src/index.js +8 -0
  17. package/src/pathData_simplify_cubic.js +26 -16
  18. package/src/pathData_simplify_harmonize_cpts.js +77 -1
  19. package/src/pathData_simplify_revertToquadratics.js +0 -1
  20. package/src/pathSimplify-main.js +304 -276
  21. package/src/pathSimplify-only-pathdata.js +7 -2
  22. package/src/pathSimplify-presets.js +254 -0
  23. package/src/poly-fit-curve-schneider.js +14 -7
  24. package/src/simplify_poly_RC.js +102 -0
  25. package/src/simplify_poly_RDP.js +109 -1
  26. package/src/simplify_poly_radial_distance.js +3 -3
  27. package/src/string_helpers.js +130 -4
  28. package/src/svg-getAttributes.js +4 -2
  29. package/src/svgii/convert_units.js +1 -1
  30. package/src/svgii/geometry.js +322 -5
  31. package/src/svgii/geometry_bbox_element.js +1 -1
  32. package/src/svgii/geometry_deduceRadius.js +116 -27
  33. package/src/svgii/geometry_length.js +253 -0
  34. package/src/svgii/pathData_analyze.js +18 -0
  35. package/src/svgii/pathData_convert.js +193 -89
  36. package/src/svgii/pathData_fix_directions.js +12 -14
  37. package/src/svgii/pathData_fromPoly.js +3 -3
  38. package/src/svgii/pathData_getLength.js +86 -0
  39. package/src/svgii/pathData_parse.js +2 -0
  40. package/src/svgii/pathData_parse_els.js +66 -68
  41. package/src/svgii/pathData_reorder.js +122 -16
  42. package/src/svgii/pathData_simplify_refineCorners.js +130 -35
  43. package/src/svgii/pathData_simplify_refine_round.js +420 -0
  44. package/src/svgii/pathData_split_to_groups.js +168 -0
  45. package/src/svgii/pathData_stringify.js +26 -64
  46. package/src/svgii/pathData_toPolygon.js +3 -4
  47. package/src/svgii/poly_analyze.js +61 -0
  48. package/src/svgii/poly_normalize.js +11 -2
  49. package/src/svgii/poly_to_pathdata.js +85 -24
  50. package/src/svgii/rounding.js +80 -78
  51. package/src/svgii/svg_cleanup.js +421 -619
  52. package/src/svgii/svg_cleanup_convertPathLength.js +39 -0
  53. package/src/svgii/svg_cleanup_general_svg_atts.js +97 -0
  54. package/src/svgii/svg_cleanup_normalize_transforms.js +83 -0
  55. package/src/svgii/svg_cleanup_remove_els_and_atts.js +77 -0
  56. package/src/svgii/svg_cleanup_ungroup.js +36 -0
  57. package/src/svgii/svg_el_parse_style_props.js +72 -47
  58. package/src/svgii/svg_getElementLength.js +67 -0
  59. package/src/svgii/svg_validate.js +220 -0
  60. package/tests/testSVG.js +14 -1
  61. package/src/svgii/pathData_refine_round.js +0 -222
@@ -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, {scaleTo:24});
36
+ let svgOpt = svgPathSimplify(svgMarkup, {preset:'high'});
24
37
 
25
38
  // simplified pathData
26
39
  console.log(svgOpt)
@@ -1,222 +0,0 @@
1
- import { checkLineIntersection, getAngle, getDistAv, getDistance, getPointOnEllipse, getSquareDistance, pointAtT, rotatePoint } from "./geometry";
2
- import { getPolygonArea } from "./geometry_area";
3
- import { getArcFromPoly } from "./geometry_deduceRadius";
4
- import { arcToBezierResolved, revertCubicQuadratic } from "./pathData_convert";
5
- import { pathDataToD } from "./pathData_stringify";
6
- import { renderPath, renderPoint, renderPoly } from "./visualize";
7
-
8
- export function refineRoundSegments(pathData, {
9
- threshold = 0,
10
- tolerance = 1,
11
- // take arcs or cubic beziers
12
- toCubic = false,
13
- debug = false
14
- } = {}) {
15
-
16
-
17
- // min size threshold for corners
18
- threshold *= tolerance;
19
-
20
- let l = pathData.length;
21
-
22
- // add fist command
23
- let pathDataN = [pathData[0]]
24
-
25
- // just for debugging
26
- let pathDataTest = []
27
-
28
- for (let i = 1; i < l; i++) {
29
- let com = pathData[i];
30
- let { type } = com;
31
- let comP = pathData[i - 1];
32
- let comN = pathData[i + 1] ? pathData[i + 1] : null;
33
- let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
34
- let comN3 = pathData[i + 3] ? pathData[i + 3] : null;
35
- let comBez = null;
36
-
37
- if ((com.type === 'C' || com.type === 'Q')) comBez = com;
38
- else if (comN && (comN.type === 'C' || comN.type === 'Q')) comBez = comN;
39
-
40
-
41
- let cpts = comBez ? (comBez.type === 'C' ? [comBez.p0, comBez.cp1, comBez.cp2, comBez.p] : [comBez.p0, comBez.cp1, comBez.p]) : []
42
-
43
- let areaBez = 0;
44
- let areaLines = 0;
45
- let signChange = false;
46
- let L1, L2;
47
- let combine = false
48
-
49
- let p0_S, p_S;
50
- let poly = []
51
- let pMid;
52
-
53
-
54
- // 2. line-line-bezier-line-line
55
- if (
56
- comP.type === 'L' &&
57
- type === 'L' &&
58
- comBez &&
59
- comN2.type === 'L' &&
60
- comN3 && (comN3.type === 'L' || comN3.type === 'Z')
61
- ) {
62
-
63
- L1 = [com.p0, com.p];
64
- L2 = [comN2.p0, comN2.p];
65
- p0_S = com.p0
66
- p_S = comN2.p
67
-
68
- // don't allow sign changes
69
- areaBez = getPolygonArea(cpts, false)
70
- areaLines = getPolygonArea([...L1, ...L2], false)
71
- signChange = (areaBez < 0 && areaLines > 0) || (areaBez > 0 && areaLines < 0)
72
-
73
- if (!signChange) {
74
-
75
- // mid point of mid bezier
76
- pMid = pointAtT(cpts, 0.5)
77
-
78
- // add to poly
79
- poly = [p0_S, pMid, p_S]
80
-
81
- combine = true
82
- }
83
-
84
- }
85
-
86
- // 1. line-bezier-bezier-line
87
- else if ((type === 'C' || type === 'Q') && comP.type === 'L') {
88
-
89
- // 1.2 next is cubic next is lineto
90
- if ((comN.type === 'C' || comN.type === 'Q') && comN2.type === 'L') {
91
-
92
- combine = true
93
-
94
- L1 = [comP.p0, comP.p];
95
- L2 = [comN2.p0, comN2.p];
96
- p0_S = comP.p
97
- p_S = comN2.p0
98
-
99
- // mid point of mid bezier
100
- pMid = comBez.p
101
-
102
- // add to poly
103
- poly = [p0_S, comBez.p, p_S]
104
-
105
-
106
- }
107
- }
108
-
109
-
110
- /**
111
- * calculate either combined
112
- * cubic or arc commands
113
- */
114
- if (combine) {
115
-
116
-
117
- // try to find center of arc
118
- let arcProps = getArcFromPoly(poly)
119
- if (arcProps) {
120
-
121
- let { centroid, r, deltaAngle, startAngle, endAngle } = arcProps;
122
-
123
- let xAxisRotation = 0;
124
- let sweep = deltaAngle > 0 ? 1 : 0;
125
- let largeArc = Math.abs(deltaAngle) > Math.PI ? 1 : 0;
126
-
127
- let pCM = rotatePoint(p0_S, centroid.x, centroid.y, deltaAngle * 0.5)
128
-
129
-
130
- let dist2 = getDistAv(pCM, pMid)
131
- let thresh = getDistAv(p0_S, p_S) * 0.05
132
- let bezierCommands;
133
-
134
- // point is close enough
135
- if (dist2 < thresh) {
136
-
137
- //toCubic = false;
138
-
139
- bezierCommands = arcToBezierResolved(
140
- {
141
- p0: p0_S,
142
- p: p_S,
143
- centroid,
144
- rx: r,
145
- ry: r,
146
- xAxisRotation,
147
- sweep,
148
- largeArc,
149
- deltaAngle,
150
- startAngle,
151
- endAngle
152
- }
153
- );
154
-
155
- if(bezierCommands.length === 1){
156
-
157
- // prefer more compact quadratic - otherwise arcs
158
- let comBezier = revertCubicQuadratic(p0_S, bezierCommands[0].cp1, bezierCommands[0].cp2, p_S)
159
-
160
- if (comBezier.type === 'Q') {
161
- toCubic = true
162
- }
163
-
164
- com = comBezier
165
- }
166
-
167
-
168
- // prefer arcs if 2 cubics are required
169
- if (bezierCommands.length > 1) toCubic = false;
170
-
171
-
172
- //toCubic = false
173
-
174
- // return elliptic arc commands
175
- if (!toCubic) {
176
- // rewrite simplified command
177
- com.type = 'A'
178
- com.values = [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y];
179
- }
180
-
181
- com.p0 = p0_S;
182
- com.p = p_S;
183
- com.extreme = false;
184
- com.corner = false;
185
-
186
- // test rendering
187
- //debug=true
188
-
189
- if (debug) {
190
- // arcs
191
- if (!toCubic) {
192
- pathDataTest = [
193
- { type: 'M', values: [p0_S.x, p0_S.y] },
194
- { type: 'A', values: [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y] },
195
- ]
196
- }
197
- // cubics
198
- else {
199
- pathDataTest = [
200
- { type: 'M', values: [p0_S.x, p0_S.y] },
201
- ...bezierCommands
202
- ]
203
- }
204
-
205
- let d = pathDataToD(pathDataTest);
206
- renderPath(markers, d, 'orange', '0.5%', '0.5')
207
- }
208
-
209
- pathDataN.push(com);
210
- i++
211
- continue
212
-
213
- }
214
- }
215
- }
216
-
217
- // pass through
218
- pathDataN.push(com)
219
- }
220
-
221
- return pathDataN;
222
- }