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.
- package/LICENSE +339 -21
- package/README.md +61 -2
- package/dist/svg-path-simplify.esm.js +4308 -0
- package/dist/svg-path-simplify.esm.min.js +1 -0
- package/dist/svg-path-simplify.js +4334 -0
- package/dist/svg-path-simplify.min.js +1 -0
- package/dist/svg-path-simplify.node.js +4331 -0
- package/dist/svg-path-simplify.node.min.js +1 -0
- package/index.html +230 -0
- package/package.json +5 -6
- package/src/constants.js +4 -0
- package/src/detect_input.js +42 -0
- package/src/index.js +21 -3
- package/src/pathData_simplify_cubic.js +324 -0
- package/src/pathData_simplify_cubic_arr.js +50 -0
- package/src/pathData_simplify_cubic_extrapolate.js +220 -0
- package/src/pathSimplify-main.js +400 -0
- package/src/svg_getViewbox.js +32 -0
- package/src/svgii/...parse.js +402 -0
- package/src/svgii/geometry.js +1143 -0
- package/src/svgii/geometry_area.js +265 -0
- package/src/svgii/geometry_bbox.js +223 -0
- package/src/svgii/pathData_analyze.js +896 -0
- package/src/svgii/pathData_convert.js +1180 -0
- package/src/svgii/pathData_parse.js +487 -0
- package/src/svgii/pathData_remove_collinear.js +98 -0
- package/src/svgii/pathData_remove_zerolength.js +28 -0
- package/src/svgii/pathData_reorder.js +238 -0
- package/src/svgii/pathData_reverse.js +124 -0
- package/src/svgii/pathData_scale.js +42 -0
- package/src/svgii/pathData_split.js +449 -0
- package/src/svgii/pathData_stringify.js +145 -0
- package/src/svgii/pathData_toPolygon.js +92 -0
- package/src/svgii/pathdata_cleanup.js +363 -0
- package/src/svgii/poly_analyze.js +172 -0
- package/src/svgii/poly_to_pathdata.js +185 -0
- package/src/svgii/rounding.js +162 -0
- package/src/svgii/simplify.js +248 -0
- package/src/svgii/simplify_bezier.js +470 -0
- package/src/svgii/simplify_linetos.js +93 -0
- package/src/svgii/simplify_polygon.js +135 -0
- package/src/svgii/stringify.js +103 -0
- package/src/svgii/svg_cleanup.js +86 -0
- 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
|
+
}
|