svg-path-simplify 0.0.2 → 0.0.5

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,3 +1,4 @@
1
+ import { detectInputType } from './detect_input';
1
2
  import { combineCubicPairs } from './pathData_simplify_cubic';
2
3
  import { getPathDataVertices, pointAtT } from './svgii/geometry';
3
4
  import { getPolyBBox } from './svgii/geometry_bbox';
@@ -6,7 +7,7 @@ import { combineArcs, convertPathData, cubicCommandToArc, revertCubicQuadratic }
6
7
  import { parsePathDataNormalized } from './svgii/pathData_parse';
7
8
  import { pathDataRemoveColinear } from './svgii/pathData_remove_collinear';
8
9
  import { removeZeroLengthLinetos } from './svgii/pathData_remove_zerolength';
9
- import { pathDataToTopLeft } from './svgii/pathData_reorder';
10
+ import { optimizeClosePath, pathDataToTopLeft } from './svgii/pathData_reorder';
10
11
  import { reversePathData } from './svgii/pathData_reverse';
11
12
  import { addExtremePoints, splitSubpaths } from './svgii/pathData_split';
12
13
  import { pathDataToD } from './svgii/pathData_stringify';
@@ -14,9 +15,10 @@ import { pathDataToPolyPlus } from './svgii/pathData_toPolygon';
14
15
  import { analyzePoly } from './svgii/poly_analyze';
15
16
  import { getCurvePathData } from './svgii/poly_to_pathdata';
16
17
  import { detectAccuracy } from './svgii/rounding';
18
+ import { cleanUpSVG } from './svgii/svg_cleanup';
17
19
  import { renderPoint } from './svgii/visualize';
18
20
 
19
- export function svgPathSimplify(d = '', {
21
+ export function svgPathSimplify(input = '', {
20
22
  toAbsolute = true,
21
23
  toRelative = true,
22
24
  toShorthands = true,
@@ -45,147 +47,253 @@ export function svgPathSimplify(d = '', {
45
47
  revertToQuadratics = true,
46
48
  minifyD = 0,
47
49
  tolerance = 1,
48
- reverse = false
50
+ reverse = false,
51
+
52
+ // svg cleanup options
53
+ removeHidden = true,
54
+ removeUnused = true,
55
+
56
+ // return svg markup or object
57
+ getObject = false
58
+
49
59
  } = {}) {
50
60
 
61
+ // clamp tolerance
62
+ tolerance = Math.max(0.1, tolerance);
63
+
64
+ let inputType = detectInputType(input);
51
65
 
52
- let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
66
+ let svg = '';
67
+ let svgSize = 0;
68
+ let svgSizeOpt = 0;
69
+ let compression = 0;
70
+ let report = {};
71
+ let d = '';
72
+ let mode = inputType === 'svgMarkup' ? 1 : 0;
53
73
 
54
- // create clone for fallback
55
- let pathData = JSON.parse(JSON.stringify(pathDataO));
74
+ let paths = []
56
75
 
57
- // count commands for evaluation
58
- let comCount = pathDataO.length
59
76
 
60
77
  /**
61
- * get sub paths
78
+ * normalize input
79
+ * switch mode
62
80
  */
63
- let subPathArr = splitSubpaths(pathData);
64
81
 
65
- // cleaned up pathData
66
- let pathDataArrN = [];
82
+ // original size
83
+ svgSize = new Blob([input]).size;
67
84
 
68
- for (let i = 0, l = subPathArr.length; i < l; i++) {
85
+ // single path
86
+ if (!mode) {
87
+ if (inputType === 'pathDataString') {
88
+ d = input
89
+ } else if (inputType === 'polyString') {
90
+ d = 'M' + input
91
+ }
92
+ paths.push({ d, el: null })
93
+ }
94
+ // process svg
95
+ else {
96
+ //sanitize
97
+ let returnDom = true
98
+ svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
99
+ );
100
+
101
+ // collect paths
102
+ let pathEls = svg.querySelectorAll('path')
103
+ pathEls.forEach(path => {
104
+ paths.push({ d: path.getAttribute('d'), el: path })
105
+ })
106
+ }
69
107
 
70
- //let { pathData, bb } = subPathArr[i];
71
- let pathDataSub = subPathArr[i];
108
+ //console.log(paths);
109
+ //console.log('inputType', inputType, 'mode', mode);
72
110
 
73
- // try simplification in reversed order
74
- if (reverse) pathDataSub = reversePathData(pathDataSub);
111
+ /**
112
+ * process all paths
113
+ */
114
+ paths.forEach(path => {
115
+ let { d, el } = path;
75
116
 
76
- // remove zero length linetos
77
- if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub)
117
+ let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
118
+ //console.log(pathDataO);
78
119
 
79
- // add extremes
80
- //let tMin=0.2, tMax=0.8;
81
- let tMin = 0, tMax = 1;
82
- if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax)
120
+ // create clone for fallback
121
+ let pathData = JSON.parse(JSON.stringify(pathDataO));
83
122
 
123
+ // count commands for evaluation
124
+ let comCount = pathDataO.length
84
125
 
85
- // sort to top left
86
- if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
126
+ /**
127
+ * get sub paths
128
+ */
129
+ let subPathArr = splitSubpaths(pathData);
87
130
 
88
- // remove colinear/flat
89
- if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
131
+ // cleaned up pathData
132
+ let pathDataArrN = [];
90
133
 
91
- // analyze pathdata to add info about signicant properties such as extremes, corners
92
- let pathDataPlus = analyzePathData(pathDataSub);
134
+ for (let i = 0, l = subPathArr.length; i < l; i++) {
93
135
 
136
+ //let { pathData, bb } = subPathArr[i];
137
+ let pathDataSub = subPathArr[i];
94
138
 
95
- // simplify beziers
96
- let { pathData, bb, dimA } = pathDataPlus;
139
+ // try simplification in reversed order
140
+ if (reverse) pathDataSub = reversePathData(pathDataSub);
97
141
 
142
+ // remove zero length linetos
143
+ if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub)
98
144
 
145
+ // add extremes
146
+ //let tMin=0.2, tMax=0.8;
147
+ let tMin = 0, tMax = 1;
148
+ if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax)
99
149
 
100
150
 
151
+ // sort to top left
152
+ if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
101
153
 
102
- let pathDataN = pathData;
154
+ // remove colinear/flat
155
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
103
156
 
157
+ // analyze pathdata to add info about signicant properties such as extremes, corners
158
+ let pathDataPlus = analyzePathData(pathDataSub);
104
159
 
105
160
 
106
-
107
- pathDataN = simplifyBezier ? simplifyPathData(pathDataN, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathDataN;
161
+ // simplify beziers
162
+ let { pathData, bb, dimA } = pathDataPlus;
108
163
 
164
+ //let pathDataN = pathData;
109
165
 
166
+ //console.log(pathDataPlus);
110
167
 
111
- // cubic to arcs
112
- if(cubicToArc){
168
+ pathData = simplifyBezier ? simplifyPathData(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
113
169
 
114
- let thresh = 3;
115
170
 
116
- pathDataN.forEach((com, c) => {
117
- let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
118
- if (type === 'C') {
119
- //console.log(com);
120
- let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh)
121
- if(comA.isArc) pathDataN[c] = comA.com;
122
- //if (comQ.type === 'Q') pathDataN[c] = comQ
123
- }
124
- })
171
+ // cubic to arcs
172
+ if (cubicToArc) {
173
+
174
+ let thresh = 3;
175
+
176
+ pathData.forEach((com, c) => {
177
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
178
+ if (type === 'C') {
179
+ //console.log(com);
180
+ let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh)
181
+ if (comA.isArc) pathData[c] = comA.com;
182
+ //if (comQ.type === 'Q') pathDataN[c] = comQ
183
+ }
184
+ })
125
185
 
126
- // combine adjacent cubics
127
- pathDataN = combineArcs(pathDataN)
186
+ // combine adjacent cubics
187
+ pathData = combineArcs(pathData)
128
188
 
189
+ }
190
+
191
+
192
+ // simplify to quadratics
193
+ if (revertToQuadratics) {
194
+ pathData.forEach((com, c) => {
195
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
196
+ if (type === 'C') {
197
+ //console.log(com);
198
+ let comQ = revertCubicQuadratic(p0, cp1, cp2, p)
199
+ if (comQ.type === 'Q') pathData[c] = comQ
200
+ }
201
+ })
202
+ }
203
+
204
+ // optimize close path
205
+ if(optimizeOrder) pathData=optimizeClosePath(pathData)
206
+
207
+ // update
208
+ pathDataArrN.push(pathData)
129
209
  }
130
210
 
211
+
212
+ // flatten compound paths
213
+ pathData = pathDataArrN.flat();
214
+
215
+ /**
216
+ * detect accuracy
217
+ */
218
+ if (autoAccuracy) {
219
+ decimals = detectAccuracy(pathData)
220
+ }
131
221
 
132
- // simplify to quadratics
133
- if (revertToQuadratics) {
134
- pathDataN.forEach((com, c) => {
135
- let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
136
- if (type === 'C') {
137
- //console.log(com);
138
- let comQ = revertCubicQuadratic(p0, cp1, cp2, p)
139
- if (comQ.type === 'Q') pathDataN[c] = comQ
140
- }
141
- })
222
+
223
+ // optimize
224
+ let pathOptions = {
225
+ toRelative,
226
+ toShorthands,
227
+ decimals,
142
228
  }
143
229
 
144
230
 
231
+ // optimize path data
232
+ pathData = convertPathData(pathData, pathOptions)
145
233
 
146
234
 
147
- // update
148
- pathDataArrN.push(pathDataN)
149
- }
235
+ // remove zero-length segments introduced by rounding
236
+ let pathDataOpt = []
237
+
238
+ pathData.forEach((com, i) => {
239
+ let { type, values } = com;
240
+ if (type === 'l' || type === 'v' || type === 'h') {
241
+ let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0
242
+ if (hasLength) pathDataOpt.push(com)
243
+ } else {
244
+ pathDataOpt.push(com)
245
+ }
246
+ })
150
247
 
248
+ pathData = pathDataOpt;
151
249
 
152
- // merge pathdata
153
- let pathDataFlat = pathDataArrN.flat();
154
250
 
155
- /**
156
- * detect accuracy
157
- */
158
- if (autoAccuracy) {
159
- decimals = detectAccuracy(pathDataFlat)
160
- }
251
+ // compare command count
252
+ let comCountS = pathData.length
161
253
 
254
+ let dOpt = pathDataToD(pathData, minifyD)
255
+ svgSizeOpt = new Blob([dOpt]).size;
256
+ //compression = +(100/svgSize * (svgSize - svgSizeOpt)).toFixed(2)
257
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2)
162
258
 
163
259
 
164
- // compare command count
165
- let comCountS = pathDataFlat.length
260
+ path.d = dOpt
261
+ path.report = {
262
+ original: comCount,
263
+ new: comCountS,
264
+ saved: comCount - comCountS,
265
+ compression,
266
+ decimals,
267
+ //success: comCountS < comCount
268
+ }
166
269
 
167
- // optimize
168
- let pathOptions = {
169
- toRelative,
170
- toShorthands,
171
- decimals,
172
- }
270
+ // apply new path for svgs
271
+ if (el) el.setAttribute('d', dOpt)
272
+
273
+ });
173
274
 
275
+ // stringify new SVG
276
+ if (mode) {
277
+ svg = new XMLSerializer().serializeToString(svg);
278
+ svgSizeOpt = new Blob([svg]).size
279
+ //compression = +(100/svgSize * (svgSize-svgSizeOpt)).toFixed(2)
280
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2)
174
281
 
175
- // optimize path data
176
- pathData = convertPathData(pathDataFlat, pathOptions)
177
- let dOpt = pathDataToD(pathData, minifyD)
282
+ svgSize = +(svgSize / 1024).toFixed(3)
283
+ svgSizeOpt = +(svgSizeOpt / 1024).toFixed(3)
284
+
285
+ report = {
286
+ svgSize,
287
+ svgSizeOpt,
288
+ compression
289
+ }
178
290
 
179
- let report = {
180
- original: comCount,
181
- new: comCountS,
182
- saved: comCount - comCountS,
183
- decimals,
184
- success: comCountS < comCount
291
+ } else {
292
+ ({ d, report } = paths[0]);
185
293
  }
186
294
 
187
- return { pathData, d: dOpt, report };
188
295
 
296
+ return !getObject ? (d ? d : svg) : { svg, d, report, inputType, mode };
189
297
 
190
298
  }
191
299
 
@@ -221,9 +329,9 @@ function simplifyPathData(pathData, {
221
329
 
222
330
  // cannot be combined as crossing extremes or corners
223
331
  if (
224
- (keepInflections && isDirChangeN) ||
225
- (keepCorners && corner) ||
226
- (!isDirChange && keepExtremes && extreme)
332
+ (keepInflections && isDirChangeN) ||
333
+ (keepCorners && corner) ||
334
+ (!isDirChange && keepExtremes && extreme)
227
335
  ) {
228
336
  //renderPoint(markers, p, 'red', '1%')
229
337
  pathDataN.push(com)
@@ -0,0 +1,32 @@
1
+ /**
2
+ * get viewBox
3
+ * either from explicit attribute or
4
+ * width and height attributes
5
+ */
6
+
7
+ export function getViewBox(svg = null, round = false) {
8
+
9
+ // browser default
10
+ if (!svg) return { x: 0, y: 0, width: 300, height: 150 }
11
+
12
+ let style = window.getComputedStyle(svg);
13
+
14
+ // the baseVal API method also converts physical units to pixels/user-units
15
+ let w = svg.hasAttribute('width') ? svg.width.baseVal.value : parseFloat(style.width) || 300;
16
+ let h = svg.hasAttribute('height') ? svg.height.baseVal.value : parseFloat(style.height) || 150;
17
+
18
+ let viewBox = svg.getAttribute('viewBox') ? svg.viewBox.baseVal : { x: 0, y: 0, width: w, height: h };
19
+
20
+ // remove SVG constructor
21
+ let { x, y, width, height } = viewBox;
22
+ viewBox = { x, y, width, height };
23
+
24
+ // round to integers
25
+ if (round) {
26
+ for (let prop in viewBox) {
27
+ viewBox[prop] = Math.ceil(viewBox[prop]);
28
+ }
29
+ }
30
+
31
+ return viewBox
32
+ }
@@ -52,7 +52,7 @@ export function checkLineIntersection(p1, p2, p3, p4, exact = true) {
52
52
  y: p1.y + (a * (p2.y - p1.y))
53
53
  }
54
54
 
55
- // console.log('intersectionPoint', intersectionPoint, p1, p2);
55
+ // console.log('intersectionPoint', intersectionPoint, p1, p2);
56
56
 
57
57
 
58
58
 
@@ -985,16 +985,63 @@ export function commandIsFlat(points, tolerance = 0.025) {
985
985
  }
986
986
 
987
987
 
988
+ export function checkBezierFlatness(p0, cpts, p) {
988
989
 
990
+ let isFlat = false;
991
+
992
+ let isCubic = cpts.length===2;
993
+
994
+ let cp1 = cpts[0]
995
+ let cp2 = isCubic ? cpts[1] : cp1;
996
+
997
+ if(p0.x===cp1.x && p0.y===cp1.y && p.x===cp2.x && p.y===cp2.y) return true;
998
+
999
+ let dx1 = cp1.x - p0.x;
1000
+ let dy1 = cp1.y - p0.y;
1001
+
1002
+ let dx2 = p.x - cp2.x;
1003
+ let dy2 = p.y - cp2.y;
1004
+
1005
+ let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
1006
+
1007
+ if(!cross1) return true
1008
+
1009
+ let dx0 = p.x - p0.x;
1010
+ let dy0 = p.y - p0.y;
1011
+ let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
1012
+
1013
+ if(!cross0) return true
1014
+
1015
+ //let diff = Math.abs(cross0 - cross1)
1016
+ //let rat0 = 1/cross0 * diff;
1017
+ let rat = (cross0/cross1)
1018
+
1019
+ if (rat<1.1 ) {
1020
+ //console.log('cross', cross0, cross1, 'rat', rat, rat0, diff );
1021
+ isFlat = true;
1022
+ }
1023
+
1024
+ return isFlat;
1025
+
1026
+ }
989
1027
 
990
1028
  /**
991
1029
  * sloppy distance calculation
992
1030
  * based on x/y differences
993
1031
  */
994
1032
  export function getDistAv(pt1, pt2) {
995
- let diffX = Math.abs(pt1.x - pt2.x);
996
- let diffY = Math.abs(pt1.y - pt2.y);
1033
+
1034
+ let diffX = Math.abs(pt2.x - pt1.x);
1035
+ let diffY = Math.abs(pt2.y - pt1.y);
997
1036
  let diff = (diffX + diffY) / 2;
1037
+
1038
+ /*
1039
+ let diffX = pt2.x - pt1.x;
1040
+ let diffY = pt2.y - pt1.y;
1041
+ let diff = Math.abs(diffX + diffY) / 2;
1042
+ */
1043
+
1044
+
998
1045
  return diff;
999
1046
  }
1000
1047
 
@@ -1067,7 +1114,7 @@ export function reducePoints(points, maxPoints = 48) {
1067
1114
  }
1068
1115
 
1069
1116
 
1070
- export function mirrorCpts(cpt2_0, pt0, cpt2, pt1, outgoing = true, t=0.666) {
1117
+ export function mirrorCpts(cpt2_0, pt0, cpt2, pt1, outgoing = true, t = 0.666) {
1071
1118
 
1072
1119
  // hypotenuse angle
1073
1120
  let ang0 = getAngle(pt0, pt1, true);
@@ -1,4 +1,4 @@
1
- import { getSquareDistance } from "./geometry.js";
1
+ import { checkBezierFlatness, getDistAv, getSquareDistance } from "./geometry.js";
2
2
  import { getPolygonArea } from "./geometry_area.js";
3
3
  import { renderPoint } from "./visualize.js";
4
4
 
@@ -17,49 +17,62 @@ export function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLine
17
17
  let comPrev = pathData[c - 1];
18
18
  let com = pathData[c];
19
19
  let comN = pathData[c + 1] || pathData[l - 1];
20
- let p1 = comN.type === 'Z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] }
20
+ let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] }
21
21
 
22
22
  let { type, values } = com;
23
23
  let valsL = values.slice(-2)
24
24
  p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
25
25
 
26
- let cpts = type === 'C' ?
27
- [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
28
- (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
26
+ let area = getPolygonArea([p0, p, p1], true)
29
27
 
28
+ let distSquare = getSquareDistance(p0, p1)
29
+ let distMax = distSquare / 100 * tolerance
30
30
 
31
- let area = getPolygonArea([p0, ...cpts, p, p1], true)
32
- let distSquare = getSquareDistance(p0, p)
33
- let distMax = distSquare / 500 * tolerance
31
+ let isFlat = area < distMax;
32
+ let isFlatBez = false;
34
33
 
35
- let isFlat = area < distMax
36
-
37
- if(!flatBezierToLinetos && type==='C') isFlat = false;
38
- //let isFlat = flatBezierToLinetos && type === 'C' ? area < distMax : false
39
34
 
35
+ if (!flatBezierToLinetos && type === 'C') isFlat = false;
36
+ //let isFlat = flatBezierToLinetos && type === 'C' ? area < distMax : false
40
37
 
41
38
  // convert flat beziers to linetos
42
- if (flatBezierToLinetos && type === 'C') {
39
+ if (flatBezierToLinetos && (type === 'C' || type === 'Q')) {
40
+
41
+ let cpts = type === 'C' ?
42
+ [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
43
+ (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
43
44
 
44
- let areaBez = getPolygonArea([p0, ...cpts, p], true)
45
- let isFlatBez = areaBez < distSquare / 1000
46
45
 
47
- if (isFlatBez && comPrev.type !== 'C') {
46
+ //let areaBez = getPolygonArea([p0, ...cpts, p], true)
47
+
48
+ isFlatBez = checkBezierFlatness(p0, cpts, p)
49
+ // console.log();
50
+
51
+ //isFlatBez = areaBez < distMax * 0.25
52
+ //console.log('isFlatBez', isFlatBez);
53
+ //isFlatBez = false
54
+
55
+ //&& comPrev.type !== 'C'
56
+ if (isFlatBez && c < l - 1 && comPrev.type !== 'C') {
57
+ type = "L"
48
58
  com.type = "L"
49
59
  com.values = valsL
60
+
61
+ //renderPoint(markers, p)
50
62
  }
51
63
 
52
64
  }
53
65
 
54
-
55
66
  // update end point
56
67
  p0 = p;
57
68
 
58
69
  // colinear – exclude arcs (as always =) as semicircles won't have an area
59
- if (type !== 'A' && isFlat && c < l - 1) {
70
+ if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez))) {
71
+ //console.log(area,distMax );
60
72
  continue;
61
73
  }
62
74
 
75
+
63
76
  if (type === 'M') {
64
77
  M = p
65
78
  p0 = M