svg-path-simplify 0.4.1 → 0.4.3

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 (48) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +6 -4
  3. package/dist/svg-path-simplify.esm.js +2450 -888
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +2450 -888
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +167 -85
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/docs/privacy-webapp.md +24 -0
  10. package/index.html +333 -132
  11. package/package.json +5 -2
  12. package/src/css_parse.js +317 -0
  13. package/src/detect_input.js +34 -4
  14. package/src/pathData_simplify_harmonize_cpts.js +77 -1
  15. package/src/pathSimplify-main.js +246 -262
  16. package/src/pathSimplify-presets.js +243 -0
  17. package/src/poly-fit-curve-schneider.js +14 -7
  18. package/src/simplify_poly_RC.js +102 -0
  19. package/src/simplify_poly_RDP.js +109 -1
  20. package/src/simplify_poly_radial_distance.js +3 -3
  21. package/src/string_helpers.js +144 -0
  22. package/src/svgii/convert_units.js +8 -2
  23. package/src/svgii/geometry.js +182 -3
  24. package/src/svgii/geometry_length.js +237 -0
  25. package/src/svgii/pathData_convert.js +43 -1
  26. package/src/svgii/pathData_fix_directions.js +6 -0
  27. package/src/svgii/pathData_fromPoly.js +3 -3
  28. package/src/svgii/pathData_getLength.js +86 -0
  29. package/src/svgii/pathData_parse.js +2 -0
  30. package/src/svgii/pathData_parse_els.js +189 -189
  31. package/src/svgii/pathData_split_to_groups.js +168 -0
  32. package/src/svgii/pathData_stringify.js +26 -64
  33. package/src/svgii/pathData_toPolygon.js +3 -4
  34. package/src/svgii/poly_analyze.js +61 -0
  35. package/src/svgii/poly_normalize.js +11 -2
  36. package/src/svgii/poly_to_pathdata.js +85 -24
  37. package/src/svgii/rounding.js +8 -7
  38. package/src/svgii/svg-styles-to-attributes-const.js +1 -0
  39. package/src/svgii/svg_cleanup.js +467 -421
  40. package/src/svgii/svg_cleanup_convertPathLength.js +32 -0
  41. package/src/svgii/svg_cleanup_general_svg_atts.js +97 -0
  42. package/src/svgii/svg_cleanup_normalize_transforms.js +83 -0
  43. package/src/svgii/svg_cleanup_remove_els_and_atts.js +72 -0
  44. package/src/svgii/svg_cleanup_ungroup.js +36 -0
  45. package/src/svgii/svg_el_parse_style_props.js +76 -28
  46. package/src/svgii/svg_getElementLength.js +67 -0
  47. package/tests/testSVG_shape.js +59 -0
  48. package/tests/testSVG_transform.js +61 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svg-path-simplify",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
 
5
5
  "type": "module",
6
6
 
@@ -34,7 +34,10 @@
34
34
  "watch": "rollup -c --watch",
35
35
  "test": "node tests/testSVG.js",
36
36
  "test1": "node tests/test.js",
37
- "test2": "node tests/testSVG2.js"
37
+ "test2": "node tests/testSVG2.js",
38
+ "test3": "node tests/testSVG_transform.js",
39
+ "test4": "node tests/testSVG_shape.js"
40
+
38
41
  },
39
42
 
40
43
  "keywords": [
@@ -0,0 +1,317 @@
1
+ import { parseValue } from "./svgii/svg_el_parse_style_props";
2
+
3
+ /**
4
+ * Parse nested CSS text into a flat object structure
5
+ * Supports arbitrary nesting depth and & parent selector reference
6
+ * Respects !important modifiers and handles data URLs
7
+ */
8
+ export function parseSvgCss(css, {
9
+ parent=null,
10
+ removeUnused=true,
11
+ flatten = true
12
+ }={}) {
13
+
14
+ let type = typeof css
15
+ if(type==='string') removeUnused = false;
16
+
17
+ // get style element text content
18
+ if(type!=='string' ){
19
+ if(css.nodeName==='style'){
20
+ css = css.innerHTML;
21
+ }
22
+ else if(css.nodeName==='svg'){
23
+ let styleEl = css.querySelector('style')
24
+ if(!styleEl) return {}
25
+ parent = css;
26
+ css = styleEl.innerHTML;
27
+ }
28
+
29
+ //invalid input
30
+ else{
31
+ console.warn('invalid CSS input')
32
+ return {}
33
+ }
34
+ }
35
+
36
+ css = css.trim();
37
+ if (!css) return {};
38
+
39
+ // Remove comments
40
+ css = css.replace(/\/\*[\s\S]*?\*\//g, "");
41
+
42
+
43
+ function parseBlock(text, parentSelector = "") {
44
+ let i = 0;
45
+ let rules = {};
46
+ let l = text.length
47
+
48
+
49
+ while (i < l) {
50
+ // Skip whitespace
51
+ while (/\s/.test(text[i])) i++;
52
+ if (i >= l) break;
53
+
54
+ // Peek ahead to check if this is a selector or a declaration
55
+ let peekIdx = i;
56
+ let isSelector = false;
57
+
58
+ // Look for '{' before ';' to determine if it's a selector
59
+ while (peekIdx < l && text[peekIdx] !== ";") {
60
+ if (text[peekIdx] === "{") {
61
+ isSelector = true;
62
+ break;
63
+ }
64
+ peekIdx++;
65
+ }
66
+
67
+ if (!isSelector) {
68
+ // It's a declaration, skip it (will be handled below)
69
+ i = peekIdx + 1;
70
+ continue;
71
+ }
72
+
73
+ // Read selector (up to '{')
74
+ let selector = "";
75
+ while (i < l && text[i] !== "{") {
76
+ selector += text[i];
77
+ i++;
78
+ }
79
+
80
+ selector = selector.trim();
81
+ if (!selector || text[i] !== "{") continue;
82
+
83
+ i++; // skip '{'
84
+
85
+ // Find matching closing brace
86
+ let blockContent = "";
87
+ let depth = 1;
88
+
89
+ while (i < l && depth > 0) {
90
+ if (text[i] === "{") depth++;
91
+ else if (text[i] === "}") depth--;
92
+
93
+ if (depth > 0) blockContent += text[i];
94
+ i++;
95
+ }
96
+
97
+ // Compose full selector
98
+ let fullSelector = selector;
99
+ if (parentSelector) {
100
+ if (selector.includes("&")) {
101
+ fullSelector = selector.replace(/&/g, parentSelector);
102
+ } else {
103
+ fullSelector = parentSelector + " " + selector;
104
+ }
105
+ }
106
+ fullSelector = fullSelector.replace(/\s+/g, " ").trim();
107
+
108
+ // Separate declarations from nested rules
109
+ let { declarations, hasNested } = extractDeclarations(blockContent, fullSelector);
110
+
111
+ // Add declarations for this selector (respect !important)
112
+ if (Object.keys(declarations).length) {
113
+ if (!rules[fullSelector]) {
114
+ rules[fullSelector] = declarations;
115
+ } else {
116
+ // Merge declarations, preserving !important
117
+ for (let prop in declarations) {
118
+ let existingValue = rules[fullSelector][prop];
119
+ let newValue = declarations[prop];
120
+
121
+ // Only override if existing doesn't have !important, or new has !important
122
+ let existingHasImportant =
123
+ existingValue && existingValue.includes("!important");
124
+ let newHasImportant = newValue.includes("!important");
125
+
126
+ if (!existingHasImportant || newHasImportant) {
127
+ rules[fullSelector][prop] = newValue;
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ // If block contains nested rules, parse them recursively
134
+ if (hasNested) {
135
+ parseBlock(blockContent, fullSelector);
136
+ }
137
+ }
138
+
139
+ return rules
140
+
141
+ }
142
+
143
+ function extractDeclarations(content) {
144
+ let declarations = {};
145
+ let i = 0;
146
+ let l= content.length;
147
+ let hasNested = false;
148
+
149
+ while (i < l) {
150
+ // Skip whitespace
151
+ while (i < l && /\s/.test(content[i])) i++;
152
+ if (i >= l) break;
153
+
154
+ // Check if next thing is a nested selector or a declaration
155
+ let checkIdx = i;
156
+ let isNested = false;
157
+
158
+ // Scan until we hit ':' or '{' or ';'
159
+ while (checkIdx < l) {
160
+ if (content[checkIdx] === "{") {
161
+ isNested = true;
162
+ break;
163
+ }
164
+ if (content[checkIdx] === ":") {
165
+ // It's a declaration
166
+ break;
167
+ }
168
+ if (content[checkIdx] === ";") {
169
+ // Empty or malformed
170
+ break;
171
+ }
172
+ checkIdx++;
173
+ }
174
+
175
+ if (isNested) {
176
+ // Skip nested rule (will be handled by recursive call)
177
+ hasNested = true;
178
+ // Skip to closing brace of this nested rule
179
+ let depth = 0;
180
+ while (i < l) {
181
+ if (content[i] === "{") depth++;
182
+ if (content[i] === "}") depth--;
183
+ i++;
184
+ if (depth === 0) break;
185
+ }
186
+ } else {
187
+ // It's a declaration, read until ';' (but respect url() and quotes)
188
+ let decl = "";
189
+ let inUrl = false;
190
+ let inQuotes = false;
191
+ let quoteChar = "";
192
+
193
+ while (i < l) {
194
+ let char = content[i];
195
+ let nextChar = content[i + 1];
196
+
197
+ // Track if we're inside url()
198
+ if (
199
+ char === "u" &&
200
+ nextChar === "r" &&
201
+ content.slice(i, i + 4) === "url("
202
+ ) {
203
+ inUrl = true;
204
+ }
205
+
206
+ // Track quotes
207
+ if (
208
+ (char === '"' || char === "'") &&
209
+ (i === 0 || content[i - 1] !== "\\")
210
+ ) {
211
+ if (!inQuotes) {
212
+ inQuotes = true;
213
+ quoteChar = char;
214
+ } else if (char === quoteChar) {
215
+ inQuotes = false;
216
+ quoteChar = "";
217
+ }
218
+ }
219
+
220
+ // Check for end of url()
221
+ if (inUrl && char === ")" && !inQuotes) {
222
+ inUrl = false;
223
+ }
224
+
225
+ // Only break on semicolon if we're not inside url() or quotes
226
+ if (char === ";" && !inUrl && !inQuotes) {
227
+ i++; // skip ';'
228
+ break;
229
+ }
230
+
231
+ decl += char;
232
+ i++;
233
+ }
234
+
235
+ decl = decl.trim();
236
+ if (decl) {
237
+ let colonIdx = decl.indexOf(":");
238
+ if (colonIdx > -1) {
239
+ let prop = decl.substring(0, colonIdx).trim();
240
+ let value = decl.substring(colonIdx + 1).trim();
241
+ if (prop && value) {
242
+ //console.log('selector', selector, isId);
243
+ //declarations[prop] = isId && !value.includes('!important') ? value+'!important' : value;
244
+ declarations[prop] = value;
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+
251
+ return { declarations, hasNested };
252
+ }
253
+
254
+ let rules = parseBlock(css);
255
+ if(parent && removeUnused) rules = removeUnusedSelectors(parent, rules)
256
+ if(flatten) rules = flattenCssProps(rules)
257
+
258
+ // emulate specificity: prioritize ids and important
259
+ let rulesID = {};
260
+ let rulesImportant = {};
261
+ for(let rule in rules){
262
+ if(rule.startsWith('#')){
263
+ rulesID[rule] = rules[rule]
264
+ delete rules[rule];
265
+ }
266
+
267
+ for(let prop in rules[rule]){
268
+ let val = rules[rule][prop]
269
+ if(val.includes('!important')){
270
+ if(!rulesImportant[rule]) rulesImportant[rule]={}
271
+ rulesImportant[rule][prop] = val
272
+ }
273
+ }
274
+ }
275
+
276
+ rules= {
277
+ ...rules,
278
+ ...rulesID,
279
+ ...rulesImportant
280
+ }
281
+
282
+ return rules;
283
+ }
284
+
285
+ function flattenCssProps(rules) {
286
+ for (let selector in rules) {
287
+ let targets = selector.split(/,/).map((sel) => sel.trim());
288
+ let values = rules[selector];
289
+ if (targets.length > 1) {
290
+ targets.forEach((target) => {
291
+ let props = rules[target];
292
+ for (let prop in props) {
293
+ let value = props[prop];
294
+ if (!value.includes("!important")) {
295
+ rules[target][prop] = value;
296
+ }
297
+ }
298
+ });
299
+ delete rules[selector];
300
+ }
301
+ }
302
+ return rules;
303
+ }
304
+
305
+
306
+ function removeUnusedSelectors(parent=null, props={}){
307
+ let selectors = Object.keys(props);
308
+ selectors.forEach(selector=>{
309
+ let el = parent.querySelector(selector)
310
+ // remove
311
+ if(!el && selector!==':root') {
312
+ //console.log( selector, 'doesnt exist')
313
+ delete props[selector]
314
+ }
315
+ })
316
+ return props
317
+ }
@@ -14,12 +14,12 @@ export function detectInputType(input) {
14
14
  if (Array.isArray(input[0])) {
15
15
  //console.log('is array', input[0], input[0][0])
16
16
 
17
- if(input[0].length===2){
17
+ if (input[0].length === 2) {
18
18
  //console.log('is single poly value array')
19
19
  return 'polyArray'
20
20
  }
21
21
 
22
- else if (Array.isArray(input[0][0]) && input[0][0].length === 2 ) {
22
+ else if (Array.isArray(input[0][0]) && input[0][0].length === 2) {
23
23
  //console.log('is complex poly point value array', input[0][0])
24
24
  return 'polyComplexArray'
25
25
  }
@@ -30,7 +30,7 @@ export function detectInputType(input) {
30
30
  }
31
31
 
32
32
  // is point array
33
- else if (input[0].x!==undefined && input[0].y!==undefined) {
33
+ else if (input[0].x !== undefined && input[0].y !== undefined) {
34
34
  //console.log('is nested point object array')
35
35
  return 'polyObjectArray'
36
36
  }
@@ -48,13 +48,23 @@ export function detectInputType(input) {
48
48
  if (typeof input === "string") {
49
49
  input = input.trim();
50
50
  let isSVG = input.includes('<svg') && input.includes('</svg');
51
+ let isSymbol = input.startsWith('<symbol') && input.includes('</symbol');
51
52
  let isPathData = input.startsWith('M') || input.startsWith('m');
52
53
  let isPolyString = !isNaN(input.substring(0, 1)) && !isNaN(input.substring(input.length - 1, input.length))
53
-
54
+ let isJson = isNumberJson(input)
55
+ //console.log('isNumberJson', isJson);
54
56
 
55
57
  if (isSVG) {
56
58
  type = 'svgMarkup'
57
59
  }
60
+
61
+ else if (isJson) {
62
+ type = 'json'
63
+ }
64
+
65
+ else if (isSymbol) {
66
+ type = 'symbol'
67
+ }
58
68
  else if (isPathData) {
59
69
  type = 'pathDataString'
60
70
  }
@@ -68,11 +78,31 @@ export function detectInputType(input) {
68
78
  type = url || dataUrl ? "url" : "string";
69
79
  }
70
80
 
81
+
71
82
  return type
72
83
  }
73
84
 
74
85
  type = typeof input
75
86
  let constructor = input.constructor.name
76
87
 
88
+
89
+
77
90
  return (constructor || type).toLowerCase();
78
91
  }
92
+
93
+
94
+ function isNumberJson(str) {
95
+
96
+ str = str.trim();
97
+
98
+ let hasNumber = /\d/.test(str)
99
+ let hasInvalid = /[abcdfghijklmnopqrstuvwz]/gi.test(str)
100
+ if (!hasNumber || hasInvalid) return false
101
+
102
+
103
+ // is JSON like
104
+ let isJson = str.startsWith('[') && str.endsWith(']');
105
+
106
+ return isJson
107
+
108
+ }
@@ -1,5 +1,81 @@
1
1
  import { checkLineIntersection, getDistManhattan, interpolate, pointAtT } from "./svgii/geometry";
2
- import { renderPoint } from "./svgii/visualize";
2
+ import { renderPoint, renderPoly } from "./svgii/visualize";
3
+
4
+
5
+ export function fixIntersectingCpts(pathData = []) {
6
+
7
+ let l = pathData.length;
8
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] }
9
+ let p = p0
10
+
11
+
12
+ for (let i = 1; i < l; i++) {
13
+ let com = pathData[i]
14
+ let comPrev = pathData[i - 1] || null
15
+ let { type, values } = com
16
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
17
+ let adjust = false;
18
+
19
+ //comN && comN.type==='C' &&
20
+ if (type === 'C') {
21
+ let tx = 1.2;
22
+ let cp1 = { x: values[0], y: values[1] }
23
+ p = { x: values[4], y: values[5] }
24
+ let cp2 = { x: values[2], y: values[3] }
25
+
26
+ let cp1_ex = interpolate(p0, cp1, tx )
27
+ let cp2_ex = interpolate( p, cp2, tx)
28
+
29
+ let valuesL = comPrev.values.slice(-2)
30
+ //let p0 = { x: valuesL[0], y: valuesL[1] }
31
+
32
+ // extend tangents
33
+ //let ptI = checkLineIntersection(p0, cp1_ex, p, cp2_ex, false, true)
34
+ let ptI = checkLineIntersection(p0, cp1, p, cp2_ex, true, true)
35
+
36
+
37
+ /*
38
+ renderPoly(markers, [p0, cp1], 'orange', '0.75')
39
+ renderPoly(markers, [p, cp2], 'blue', '0.75')
40
+ renderPoly(markers, [p, cp2_ex], 'magenta', '0.75')
41
+
42
+ renderPoint(markers, p0, 'orange', '0.75')
43
+ renderPoint(markers, cp1, 'magenta', '0.75')
44
+ renderPoint(markers, cp1_ex, 'blue', '0.5')
45
+
46
+ renderPoint(markers, cp2, 'cyan', '0.75')
47
+ */
48
+ //renderPoint(markers, cp2_ex, 'purple', '0.75')
49
+
50
+ //renderPoint(markers, cp1, 'purple', '0.75')
51
+ //renderPoint(markers, cp1_ex, 'blue', '0.75')
52
+
53
+ //renderPoint(markers, cp2, 'cyan')
54
+ /*
55
+ renderPoint(markers, p0, 'orange')
56
+ renderPoint(markers, p, 'magenta', '0.5')
57
+ */
58
+
59
+
60
+ if (ptI) {
61
+ let t = 0.666
62
+ cp1 = interpolate(p0, ptI, t)
63
+ cp2 = interpolate(p, ptI, t)
64
+ com.values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y]
65
+ //renderPoint(markers, ptI)
66
+ }
67
+
68
+ }
69
+
70
+ if (values.length) {
71
+ p0 = p
72
+ }
73
+ }
74
+
75
+
76
+ return pathData;
77
+ }
78
+
3
79
 
4
80
 
5
81