svg-path-simplify 0.0.1 → 0.0.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 (44) hide show
  1. package/LICENSE +339 -21
  2. package/README.md +61 -2
  3. package/dist/svg-path-simplify.esm.js +4308 -0
  4. package/dist/svg-path-simplify.esm.min.js +1 -0
  5. package/dist/svg-path-simplify.js +4334 -0
  6. package/dist/svg-path-simplify.min.js +1 -0
  7. package/dist/svg-path-simplify.node.js +4331 -0
  8. package/dist/svg-path-simplify.node.min.js +1 -0
  9. package/index.html +230 -0
  10. package/package.json +5 -6
  11. package/src/constants.js +4 -0
  12. package/src/detect_input.js +42 -0
  13. package/src/index.js +21 -3
  14. package/src/pathData_simplify_cubic.js +324 -0
  15. package/src/pathData_simplify_cubic_arr.js +50 -0
  16. package/src/pathData_simplify_cubic_extrapolate.js +220 -0
  17. package/src/pathSimplify-main.js +400 -0
  18. package/src/svg_getViewbox.js +32 -0
  19. package/src/svgii/...parse.js +402 -0
  20. package/src/svgii/geometry.js +1143 -0
  21. package/src/svgii/geometry_area.js +265 -0
  22. package/src/svgii/geometry_bbox.js +223 -0
  23. package/src/svgii/pathData_analyze.js +896 -0
  24. package/src/svgii/pathData_convert.js +1180 -0
  25. package/src/svgii/pathData_parse.js +487 -0
  26. package/src/svgii/pathData_remove_collinear.js +98 -0
  27. package/src/svgii/pathData_remove_zerolength.js +28 -0
  28. package/src/svgii/pathData_reorder.js +238 -0
  29. package/src/svgii/pathData_reverse.js +124 -0
  30. package/src/svgii/pathData_scale.js +42 -0
  31. package/src/svgii/pathData_split.js +449 -0
  32. package/src/svgii/pathData_stringify.js +145 -0
  33. package/src/svgii/pathData_toPolygon.js +92 -0
  34. package/src/svgii/pathdata_cleanup.js +363 -0
  35. package/src/svgii/poly_analyze.js +172 -0
  36. package/src/svgii/poly_to_pathdata.js +185 -0
  37. package/src/svgii/rounding.js +162 -0
  38. package/src/svgii/simplify.js +248 -0
  39. package/src/svgii/simplify_bezier.js +470 -0
  40. package/src/svgii/simplify_linetos.js +93 -0
  41. package/src/svgii/simplify_polygon.js +135 -0
  42. package/src/svgii/stringify.js +103 -0
  43. package/src/svgii/svg_cleanup.js +86 -0
  44. package/src/svgii/visualize.js +317 -0
@@ -0,0 +1,400 @@
1
+ import { detectInputType } from './detect_input';
2
+ import { combineCubicPairs } from './pathData_simplify_cubic';
3
+ import { getPathDataVertices, pointAtT } from './svgii/geometry';
4
+ import { getPolyBBox } from './svgii/geometry_bbox';
5
+ import { analyzePathData, analyzePathData2 } from './svgii/pathData_analyze';
6
+ import { combineArcs, convertPathData, cubicCommandToArc, revertCubicQuadratic } from './svgii/pathData_convert';
7
+ import { parsePathDataNormalized } from './svgii/pathData_parse';
8
+ import { pathDataRemoveColinear } from './svgii/pathData_remove_collinear';
9
+ import { removeZeroLengthLinetos } from './svgii/pathData_remove_zerolength';
10
+ import { pathDataToTopLeft } from './svgii/pathData_reorder';
11
+ import { reversePathData } from './svgii/pathData_reverse';
12
+ import { addExtremePoints, splitSubpaths } from './svgii/pathData_split';
13
+ import { pathDataToD } from './svgii/pathData_stringify';
14
+ import { pathDataToPolyPlus } from './svgii/pathData_toPolygon';
15
+ import { analyzePoly } from './svgii/poly_analyze';
16
+ import { getCurvePathData } from './svgii/poly_to_pathdata';
17
+ import { detectAccuracy } from './svgii/rounding';
18
+ import { cleanUpSVG } from './svgii/svg_cleanup';
19
+ import { renderPoint } from './svgii/visualize';
20
+
21
+ export function svgPathSimplify(input = '', {
22
+ toAbsolute = true,
23
+ toRelative = true,
24
+ toShorthands = true,
25
+ decimals = 3,
26
+ //optimize = 0,
27
+
28
+ // not necessary unless you need cubics only
29
+ quadraticToCubic = true,
30
+
31
+ // mostly a fallback if arc calculations fail
32
+ arcToCubic = false,
33
+ cubicToArc = false,
34
+
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,
42
+ optimizeOrder = true,
43
+ removeColinear = true,
44
+ simplifyBezier = true,
45
+ autoAccuracy = true,
46
+ flatBezierToLinetos = true,
47
+ revertToQuadratics = true,
48
+ minifyD = 0,
49
+ tolerance = 1,
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
+
59
+ } = {}) {
60
+
61
+ // clamp tolerance
62
+ tolerance = Math.max(0.1, tolerance);
63
+
64
+ let inputType = detectInputType(input);
65
+
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;
73
+
74
+ let paths = []
75
+
76
+
77
+ /**
78
+ * normalize input
79
+ * switch mode
80
+ */
81
+
82
+ // original size
83
+ svgSize = new Blob([input]).size;
84
+
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
+ }
107
+
108
+ //console.log(paths);
109
+ //console.log('inputType', inputType, 'mode', mode);
110
+
111
+ /**
112
+ * process all paths
113
+ */
114
+ paths.forEach(path => {
115
+ let { d, el } = path;
116
+
117
+ 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));
122
+
123
+ // count commands for evaluation
124
+ let comCount = pathDataO.length
125
+
126
+ /**
127
+ * get sub paths
128
+ */
129
+ let subPathArr = splitSubpaths(pathData);
130
+
131
+ // cleaned up pathData
132
+ let pathDataArrN = [];
133
+
134
+ for (let i = 0, l = subPathArr.length; i < l; i++) {
135
+
136
+ //let { pathData, bb } = subPathArr[i];
137
+ let pathDataSub = subPathArr[i];
138
+
139
+ // try simplification in reversed order
140
+ if (reverse) pathDataSub = reversePathData(pathDataSub);
141
+
142
+ // remove zero length linetos
143
+ if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub)
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)
149
+
150
+
151
+ // sort to top left
152
+ if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
153
+
154
+ // remove colinear/flat
155
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
156
+
157
+ // analyze pathdata to add info about signicant properties such as extremes, corners
158
+ let pathDataPlus = analyzePathData(pathDataSub);
159
+
160
+
161
+ // simplify beziers
162
+ let { pathData, bb, dimA } = pathDataPlus;
163
+
164
+ //let pathDataN = pathData;
165
+
166
+ //console.log(pathDataPlus);
167
+
168
+ pathData = simplifyBezier ? simplifyPathData(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
169
+
170
+
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
+ })
185
+
186
+ // combine adjacent cubics
187
+ pathData = combineArcs(pathData)
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
+
205
+ // update
206
+ pathDataArrN.push(pathData)
207
+ }
208
+
209
+
210
+ // flatten compound paths
211
+ pathData = pathDataArrN.flat();
212
+
213
+ /**
214
+ * detect accuracy
215
+ */
216
+ if (autoAccuracy) {
217
+ decimals = detectAccuracy(pathData)
218
+ }
219
+
220
+
221
+ // optimize
222
+ let pathOptions = {
223
+ toRelative,
224
+ toShorthands,
225
+ decimals,
226
+ }
227
+
228
+
229
+ // optimize path data
230
+ pathData = convertPathData(pathData, pathOptions)
231
+
232
+
233
+ // remove zero-length segments introduced by rounding
234
+ let pathDataOpt = []
235
+
236
+ pathData.forEach((com, i) => {
237
+ let { type, values } = com;
238
+ if (type === 'l' || type === 'v' || type === 'h') {
239
+ let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0
240
+ if (hasLength) pathDataOpt.push(com)
241
+ } else {
242
+ pathDataOpt.push(com)
243
+ }
244
+ })
245
+
246
+ pathData = pathDataOpt;
247
+
248
+
249
+ // compare command count
250
+ let comCountS = pathData.length
251
+
252
+ let dOpt = pathDataToD(pathData, minifyD)
253
+ svgSizeOpt = new Blob([dOpt]).size;
254
+ //compression = +(100/svgSize * (svgSize - svgSizeOpt)).toFixed(2)
255
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2)
256
+
257
+
258
+ path.d = dOpt
259
+ path.report = {
260
+ original: comCount,
261
+ new: comCountS,
262
+ saved: comCount - comCountS,
263
+ compression,
264
+ decimals,
265
+ //success: comCountS < comCount
266
+ }
267
+
268
+ // apply new path for svgs
269
+ if (el) el.setAttribute('d', dOpt)
270
+
271
+ });
272
+
273
+ // stringify new SVG
274
+ if (mode) {
275
+ svg = new XMLSerializer().serializeToString(svg);
276
+ svgSizeOpt = new Blob([svg]).size
277
+ //compression = +(100/svgSize * (svgSize-svgSizeOpt)).toFixed(2)
278
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2)
279
+
280
+ svgSize = +(svgSize / 1024).toFixed(3)
281
+ svgSizeOpt = +(svgSizeOpt / 1024).toFixed(3)
282
+
283
+ report = {
284
+ svgSize,
285
+ svgSizeOpt,
286
+ compression
287
+ }
288
+
289
+ } else {
290
+ ({ d, report } = paths[0]);
291
+ }
292
+
293
+
294
+ return !getObject ? (d ? d : svg) : { svg, d, report, inputType, mode };
295
+
296
+ }
297
+
298
+
299
+
300
+ function simplifyPathData(pathData, {
301
+ keepExtremes = true,
302
+ keepInflections = true,
303
+ keepCorners = true,
304
+ extrapolateDominant = true,
305
+ tolerance = 1,
306
+ reverse = false
307
+ } = {}) {
308
+
309
+ let pathDataN = [pathData[0]];
310
+
311
+ for (let i = 2, l = pathData.length; l && i <= l; i++) {
312
+ let com = pathData[i - 1];
313
+ let comN = i < l ? pathData[i] : null;
314
+ let typeN = comN?.type || null;
315
+ //let isCornerN = comN?.corner || null;
316
+ //let isExtremeN = comN?.extreme || null;
317
+ let isDirChange = com?.directionChange || null;
318
+ let isDirChangeN = comN?.directionChange || null;
319
+
320
+ let { type, values, p0, p, cp1 = null, cp2 = null, extreme = false, corner = false, dimA = 0 } = com;
321
+
322
+ // count simplifications
323
+ let success = 0;
324
+
325
+ // next is also cubic
326
+ if (type === 'C' && typeN === 'C') {
327
+
328
+ // cannot be combined as crossing extremes or corners
329
+ if (
330
+ (keepInflections && isDirChangeN) ||
331
+ (keepCorners && corner) ||
332
+ (!isDirChange && keepExtremes && extreme)
333
+ ) {
334
+ //renderPoint(markers, p, 'red', '1%')
335
+ pathDataN.push(com)
336
+ }
337
+
338
+ // try simplification
339
+ else {
340
+ //renderPoint(markers, p, 'magenta', '1%')
341
+ let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance)
342
+ let error = 0;
343
+
344
+ // combining successful! try next segment
345
+ if (combined.length === 1) {
346
+ com = combined[0]
347
+ let offset = 1;
348
+ error += com.error;
349
+ //console.log('!error', error);
350
+
351
+ // find next candidates
352
+ for (let n = i + 1; error < tolerance && n < l; n++) {
353
+ let comN = pathData[n]
354
+ if (comN.type !== 'C' ||
355
+ (
356
+ (keepInflections && comN.directionChange) ||
357
+ (keepCorners && com.corner) ||
358
+ (keepExtremes && com.extreme)
359
+ )
360
+ ) {
361
+ break
362
+ }
363
+
364
+ let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance)
365
+ if (combined.length === 1) {
366
+ offset++
367
+ }
368
+ com = combined[0]
369
+ }
370
+
371
+ //com.opt = true
372
+ pathDataN.push(com)
373
+
374
+ if (i < l) {
375
+ i += offset
376
+ }
377
+
378
+ } else {
379
+ pathDataN.push(com)
380
+ }
381
+ }
382
+
383
+ } // end of bezier command
384
+
385
+
386
+ // other commands
387
+ else {
388
+ pathDataN.push(com)
389
+ }
390
+
391
+ } // end command loop
392
+
393
+ // reverse back
394
+ if (reverse) pathDataN = reversePathData(pathDataN)
395
+
396
+ return pathDataN
397
+ }
398
+
399
+
400
+
@@ -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
+ }