svg-path-simplify 0.0.5 → 0.0.8

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.
@@ -3,7 +3,7 @@ import { getPathArea } from "./svgii/geometry_area";
3
3
  import { pathDataToD } from "./svgii/pathData_stringify";
4
4
  import { renderPath, renderPoint } from "./svgii/visualize";
5
5
 
6
- export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
6
+ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = false) {
7
7
 
8
8
  // cubic Bézier derivative
9
9
  const cubicDerivative = (p0, p1, p2, p3, t) => {
@@ -27,8 +27,10 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
27
27
  let commands = [com1, com2]
28
28
 
29
29
  // detect dominant
30
- let dist1 = getSquareDistance(com1.p0, com1.p)
31
- let dist2 = getSquareDistance(com2.p0, com2.p)
30
+ let dist1 = getDistAv(com1.p0, com1.p)
31
+ let dist2 = getDistAv(com2.p0, com2.p)
32
+
33
+
32
34
  let reverse = dist1 > dist2;
33
35
 
34
36
  //let ang1 = getAngle(com1.p0, com1.cp1)
@@ -42,6 +44,7 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
42
44
 
43
45
  if (!ptI) {
44
46
  //renderPoint(markers, com1.p, 'purple')
47
+ //console.log('nope');
45
48
  return commands
46
49
  }
47
50
 
@@ -95,13 +98,10 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
95
98
  let r = sub(P, com1.p0);
96
99
 
97
100
  //let t0_2 = t0 - dot(r, dP) / dot(dP, dP);
98
- //console.log(t0, t0_2);
99
101
 
100
102
  t0 -= dot(r, dP) / dot(dP, dP);
101
103
 
102
-
103
104
  // construct merged cubic over [t0, 1]
104
-
105
105
  let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
106
106
  let Q3 = com2.p;
107
107
 
@@ -131,31 +131,38 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
131
131
  }
132
132
 
133
133
 
134
- let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], 0.5, false, true)
134
+ let tMid = (1 - t0) * 0.5;
135
+ let tSplit = t0 - 1;
136
+ //tMid = 0.5;
137
+
138
+
139
+ let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true)
135
140
  let seg1_cp2 = ptM.cpts[2]
136
141
  //let seg2_cp1 = ptM.cpts[3]
137
142
 
138
-
139
143
  let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false)
140
144
  let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false)
141
145
 
142
146
 
147
+
148
+
143
149
  let cp1_2 = interpolate(result.p0, ptI_1, 1.333)
144
150
  let cp2_2 = interpolate(result.p, ptI_2, 1.333)
145
151
 
146
152
  // test self intersections and exit
147
- let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true )
148
- if(cp_intersection){
153
+ let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true)
154
+ if (cp_intersection) {
149
155
  //renderPoint(markers, cp_intersection )
150
156
  return commands;
151
157
  }
152
158
 
159
+ if (debug) renderPoint(markers, ptM, 'purple')
153
160
 
154
161
  result.cp1 = cp1_2
155
162
  result.cp2 = cp2_2
156
163
 
157
- // check distances
158
164
 
165
+ // check distances between original starting point and extrapolated
159
166
  let dist3 = getDistAv(com1_o.p0, result.p0)
160
167
  let dist4 = getDistAv(com2_o.p, result.p)
161
168
  let dist5 = (dist3 + dist4)
@@ -167,13 +174,29 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
167
174
  result.corner = com2_o.corner
168
175
  result.dimA = com2_o.dimA
169
176
  result.directionChange = com2_o.directionChange
177
+ result.type = 'C'
170
178
  result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y]
171
179
 
172
180
 
173
-
174
- // check if completely off
181
+ // extrapolated starting point is not completely off
175
182
  if (dist5 < maxDist) {
176
183
 
184
+ // split t to meet original mid segment start point
185
+ let tSplit = reverse ? 1 + t0 : Math.abs(t0);
186
+ //console.log('t0', t0, tMid, 'tSplit', tSplit);
187
+
188
+ let ptSplit = pointAtT([result.p0, result.cp1, result.cp2, result.p], tSplit);
189
+ let distSplit = getDistAv(ptSplit, com1.p)
190
+ //console.log('distS', distS, maxDist );
191
+
192
+ // not close enough - exit
193
+ if (distSplit > maxDist * tolerance) {
194
+ //renderPoint(markers, ptSplit, 'cyan', '1%')
195
+ //renderPoint(markers, com1.p, 'red', '0.5%')
196
+ return commands;
197
+ }
198
+
199
+
177
200
  // compare combined with original area
178
201
  let pathData0 = [
179
202
  { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
@@ -190,28 +213,23 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
190
213
  let areaN = getPathArea(pathDataN)
191
214
  let areaDiff = Math.abs(areaN / area0 - 1)
192
215
 
193
- result.error = areaDiff * 10 * tolerance;
216
+ result.error = areaDiff * 5 * tolerance;
194
217
  //result.error = areaDiff + dist5;
195
218
 
196
- let d = pathDataToD(pathDataN)
197
219
 
198
- // success
199
- if (areaDiff < 0.01) {
200
- commands = [result];
201
- //renderPath(markers, d, 'orange')
202
- //console.log('areaDiff', areaDiff);
203
-
204
- } else {
205
- // renderPath(markers, d, 'red')
206
- // console.log('areaDiff', areaDiff);
220
+ if (debug) {
221
+ let d = pathDataToD(pathDataN)
222
+ renderPath(markers, d, 'orange')
207
223
  }
208
224
 
209
-
210
- //renderPath(markers, d, 'orange')
225
+ // success!!!
226
+ if (areaDiff < 0.05 * tolerance) {
227
+ commands = [result];
228
+ //console.log('areaDiff', areaDiff);
229
+ }
211
230
  }
212
231
 
213
232
 
214
-
215
233
  //console.log(commands);
216
234
 
217
235
  return commands
@@ -2,27 +2,33 @@ import { detectInputType } from './detect_input';
2
2
  import { combineCubicPairs } from './pathData_simplify_cubic';
3
3
  import { getPathDataVertices, pointAtT } from './svgii/geometry';
4
4
  import { getPolyBBox } from './svgii/geometry_bbox';
5
- import { analyzePathData, analyzePathData2 } from './svgii/pathData_analyze';
5
+ import { analyzePathData } from './svgii/pathData_analyze';
6
6
  import { combineArcs, convertPathData, cubicCommandToArc, revertCubicQuadratic } from './svgii/pathData_convert';
7
7
  import { parsePathDataNormalized } from './svgii/pathData_parse';
8
+ import { shapeElToPath } from './svgii/pathData_parse_els';
8
9
  import { pathDataRemoveColinear } from './svgii/pathData_remove_collinear';
10
+ import { removeOrphanedM } from './svgii/pathData_remove_orphaned';
9
11
  import { removeZeroLengthLinetos } from './svgii/pathData_remove_zerolength';
10
12
  import { optimizeClosePath, pathDataToTopLeft } from './svgii/pathData_reorder';
11
13
  import { reversePathData } from './svgii/pathData_reverse';
12
14
  import { addExtremePoints, splitSubpaths } from './svgii/pathData_split';
13
15
  import { pathDataToD } from './svgii/pathData_stringify';
14
- import { pathDataToPolyPlus } from './svgii/pathData_toPolygon';
16
+ //import { pathDataToPolyPlus, pathDataToPolySingle } from './svgii/pathData_toPolygon';
15
17
  import { analyzePoly } from './svgii/poly_analyze';
16
18
  import { getCurvePathData } from './svgii/poly_to_pathdata';
17
19
  import { detectAccuracy } from './svgii/rounding';
18
- import { cleanUpSVG } from './svgii/svg_cleanup';
20
+ import { refineAdjacentExtremes } from './svgii/simplify_refineExtremes';
21
+ import { cleanUpSVG, removeEmptySVGEls, stringifySVG } from './svgii/svg_cleanup';
19
22
  import { renderPoint } from './svgii/visualize';
20
23
 
21
24
  export function svgPathSimplify(input = '', {
25
+
26
+ // return svg markup or object
27
+ getObject = false,
28
+
22
29
  toAbsolute = true,
23
30
  toRelative = true,
24
31
  toShorthands = true,
25
- decimals = 3,
26
32
  //optimize = 0,
27
33
 
28
34
  // not necessary unless you need cubics only
@@ -32,29 +38,36 @@ export function svgPathSimplify(input = '', {
32
38
  arcToCubic = false,
33
39
  cubicToArc = false,
34
40
 
35
- // arc to cubic precision - adds more segments for better precision
36
- arcAccuracy = 4,
37
- keepExtremes = true,
38
- keepCorners = true,
39
- keepInflections = true,
40
- extrapolateDominant = false,
41
- addExtremes = false,
41
+
42
+ simplifyBezier = true,
42
43
  optimizeOrder = true,
43
44
  removeColinear = true,
44
- simplifyBezier = true,
45
- autoAccuracy = true,
46
45
  flatBezierToLinetos = true,
47
46
  revertToQuadratics = true,
47
+
48
+ refineExtremes = true,
49
+ keepExtremes = true,
50
+ keepCorners = true,
51
+ extrapolateDominant = true,
52
+ keepInflections = false,
53
+ addExtremes = false,
54
+ removeOrphanSubpaths = false,
55
+
56
+
57
+ // svg path optimizations
58
+ decimals = 3,
59
+ autoAccuracy = true,
60
+
48
61
  minifyD = 0,
49
62
  tolerance = 1,
50
63
  reverse = false,
51
64
 
52
65
  // svg cleanup options
66
+ mergePaths = false,
53
67
  removeHidden = true,
54
68
  removeUnused = true,
69
+ shapesToPaths = true,
55
70
 
56
- // return svg markup or object
57
- getObject = false
58
71
 
59
72
  } = {}) {
60
73
 
@@ -98,6 +111,14 @@ export function svgPathSimplify(input = '', {
98
111
  svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
99
112
  );
100
113
 
114
+ if(shapesToPaths){
115
+ let shapes = svg.querySelectorAll('polygon, polyline, line, rect, circle, ellipse');
116
+ shapes.forEach(shape=>{
117
+ let path = shapeElToPath(shape);
118
+ shape.replaceWith(path)
119
+ })
120
+ }
121
+
101
122
  // collect paths
102
123
  let pathEls = svg.querySelectorAll('path')
103
124
  pathEls.forEach(path => {
@@ -111,18 +132,37 @@ export function svgPathSimplify(input = '', {
111
132
  /**
112
133
  * process all paths
113
134
  */
135
+
136
+ // SVG optimization options
137
+ let pathOptions = {
138
+ toRelative,
139
+ toShorthands,
140
+ decimals,
141
+ }
142
+
143
+ // combinded path data for SVGs with mergePaths enabled
144
+ let pathData_merged = [];
145
+
114
146
  paths.forEach(path => {
115
147
  let { d, el } = path;
116
148
 
149
+ ///let t0 = performance.now()
117
150
  let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
118
- //console.log(pathDataO);
119
-
120
- // create clone for fallback
121
- let pathData = JSON.parse(JSON.stringify(pathDataO));
151
+ //console.log('pathDataO', pathDataO);
122
152
 
123
153
  // count commands for evaluation
124
154
  let comCount = pathDataO.length
125
155
 
156
+
157
+ // create clone for fallback
158
+ //let pathData = JSON.parse(JSON.stringify(pathDataO));
159
+ let pathData = pathDataO;
160
+ //let t1 = performance.now() - t0;
161
+ //console.log('t1', t1);
162
+
163
+
164
+ if(removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
165
+
126
166
  /**
127
167
  * get sub paths
128
168
  */
@@ -164,8 +204,17 @@ export function svgPathSimplify(input = '', {
164
204
  //let pathDataN = pathData;
165
205
 
166
206
  //console.log(pathDataPlus);
207
+ //let t0=performance.now()
208
+ pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
209
+ //let t1=performance.now() - t0;
210
+ //console.log('t1', t1);
211
+
167
212
 
168
- pathData = simplifyBezier ? simplifyPathData(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
213
+ // refine extremes
214
+ if(refineExtremes){
215
+ let thresholdEx = (bb.width + bb.height) / 2 * 0.05
216
+ pathData = refineAdjacentExtremes(pathData, {threshold:thresholdEx, tolerance})
217
+ }
169
218
 
170
219
 
171
220
  // cubic to arcs
@@ -189,6 +238,9 @@ export function svgPathSimplify(input = '', {
189
238
  }
190
239
 
191
240
 
241
+
242
+
243
+
192
244
  // simplify to quadratics
193
245
  if (revertToQuadratics) {
194
246
  pathData.forEach((com, c) => {
@@ -196,85 +248,122 @@ export function svgPathSimplify(input = '', {
196
248
  if (type === 'C') {
197
249
  //console.log(com);
198
250
  let comQ = revertCubicQuadratic(p0, cp1, cp2, p)
199
- if (comQ.type === 'Q') pathData[c] = comQ
251
+ if (comQ.type === 'Q') {
252
+ /*
253
+ comQ.p0 = com.p0
254
+ comQ.cp1 = {x:comQ.values[0], y:comQ.values[1]}
255
+ comQ.p = com.p
256
+ */
257
+ comQ.extreme = com.extreme
258
+ comQ.corner = com.corner
259
+ comQ.dimA = com.dimA
260
+
261
+ pathData[c] = comQ
262
+ }
200
263
  }
201
264
  })
202
265
  }
203
266
 
267
+ //if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
268
+
269
+
204
270
  // optimize close path
205
- if(optimizeOrder) pathData=optimizeClosePath(pathData)
271
+ if (optimizeOrder) pathData = optimizeClosePath(pathData)
272
+
273
+ // poly
274
+ //let poly = pathDataToPolySingle(pathData, true)
275
+ //console.log('poly', poly);
276
+
206
277
 
207
278
  // update
208
279
  pathDataArrN.push(pathData)
209
280
  }
210
281
 
211
-
212
282
  // flatten compound paths
213
283
  pathData = pathDataArrN.flat();
214
284
 
215
- /**
216
- * detect accuracy
217
- */
218
- if (autoAccuracy) {
219
- decimals = detectAccuracy(pathData)
285
+
286
+ // collect for merged svg paths
287
+ if (el && mergePaths) {
288
+ pathData_merged.push(...pathData)
220
289
  }
290
+ // single output
291
+ else {
221
292
 
293
+ /**
294
+ * detect accuracy
295
+ */
296
+ if (autoAccuracy) {
297
+ decimals = detectAccuracy(pathData)
298
+ pathOptions.decimals = decimals
299
+ //console.log('!decimals', decimals);
300
+ }
222
301
 
223
- // optimize
224
- let pathOptions = {
225
- toRelative,
226
- toShorthands,
227
- decimals,
228
- }
302
+ //console.log('autoAccuracy', autoAccuracy, decimals);
229
303
 
304
+ // optimize path data
305
+ pathData = convertPathData(pathData, pathOptions)
230
306
 
231
- // optimize path data
232
- pathData = convertPathData(pathData, pathOptions)
307
+ // remove zero-length segments introduced by rounding
308
+ pathData = removeZeroLengthLinetos(pathData);
233
309
 
310
+ // compare command count
311
+ let comCountS = pathData.length
234
312
 
235
- // remove zero-length segments introduced by rounding
236
- let pathDataOpt = []
313
+ let dOpt = pathDataToD(pathData, minifyD)
314
+ svgSizeOpt = new Blob([dOpt]).size;
315
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2)
237
316
 
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)
317
+
318
+ path.d = dOpt
319
+ path.report = {
320
+ original: comCount,
321
+ new: comCountS,
322
+ saved: comCount - comCountS,
323
+ compression,
324
+ decimals,
325
+ //success: comCountS < comCount
245
326
  }
246
- })
247
327
 
248
- pathData = pathDataOpt;
328
+ // apply new path for svgs
329
+ if (el) el.setAttribute('d', dOpt)
330
+ }
331
+ });
249
332
 
333
+ /**
334
+ * stringify new SVG
335
+ */
336
+ if (mode) {
250
337
 
251
- // compare command count
252
- let comCountS = pathData.length
338
+ if (pathData_merged.length) {
339
+ // optimize path data
340
+ let pathData = convertPathData(pathData_merged, pathOptions)
341
+
342
+ // remove zero-length segments introduced by rounding
343
+ //pathData = removeZeroLengthLinetos_post(pathData);
344
+ pathData = removeZeroLengthLinetos(pathData);
253
345
 
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)
258
346
 
347
+ let dOpt = pathDataToD(pathData, minifyD)
259
348
 
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
349
+
350
+ // apply new path for svgs
351
+ paths[0].el.setAttribute('d', dOpt)
352
+
353
+
354
+ // remove other paths
355
+ for (let i = 1; i < paths.length; i++) {
356
+ let pathEl = paths[i].el
357
+ if (pathEl) pathEl.remove()
358
+ }
359
+
360
+ // remove empty groups e.g groups
361
+ removeEmptySVGEls(svg);
268
362
  }
269
363
 
270
- // apply new path for svgs
271
- if (el) el.setAttribute('d', dOpt)
272
364
 
273
- });
274
365
 
275
- // stringify new SVG
276
- if (mode) {
277
- svg = new XMLSerializer().serializeToString(svg);
366
+ svg = stringifySVG(svg);
278
367
  svgSizeOpt = new Blob([svg]).size
279
368
  //compression = +(100/svgSize * (svgSize-svgSizeOpt)).toFixed(2)
280
369
  compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2)
@@ -299,7 +388,7 @@ export function svgPathSimplify(input = '', {
299
388
 
300
389
 
301
390
 
302
- function simplifyPathData(pathData, {
391
+ function simplifyPathDataCubic(pathData, {
303
392
  keepExtremes = true,
304
393
  keepInflections = true,
305
394
  keepCorners = true,
@@ -347,6 +436,8 @@ function simplifyPathData(pathData, {
347
436
  if (combined.length === 1) {
348
437
  com = combined[0]
349
438
  let offset = 1;
439
+
440
+ // add cumulative error to prevent distortions
350
441
  error += com.error;
351
442
  //console.log('!error', error);
352
443
 
@@ -365,6 +456,10 @@ function simplifyPathData(pathData, {
365
456
 
366
457
  let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance)
367
458
  if (combined.length === 1) {
459
+ // add cumulative error to prevent distortions
460
+ //console.log('combined', combined);
461
+ error += combined[0].error * 0.5;
462
+ //error += combined[0].error * 1;
368
463
  offset++
369
464
  }
370
465
  com = combined[0]