react-native-nano-icons 0.1.2 → 0.1.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 (91) hide show
  1. package/README.md +20 -164
  2. package/android/build.gradle +28 -0
  3. package/android/src/main/java/com/nanoicons/NanoIconView.kt +78 -0
  4. package/android/src/main/java/com/nanoicons/NanoIconViewManager.kt +84 -0
  5. package/android/src/main/java/com/nanoicons/NanoIconsPackage.kt +22 -0
  6. package/ios/NanoIconView.h +4 -0
  7. package/ios/NanoIconView.mm +286 -0
  8. package/lib/commonjs/cli/build.js +1 -1
  9. package/lib/commonjs/cli/config.d.ts +2 -2
  10. package/lib/commonjs/cli/config.js +7 -6
  11. package/lib/commonjs/scripts/cli.js +15 -5
  12. package/lib/commonjs/src/core/font/compile.d.ts +13 -2
  13. package/lib/commonjs/src/core/font/compile.js +49 -46
  14. package/lib/commonjs/src/core/pipeline/managers.js +19 -3
  15. package/lib/commonjs/src/core/pipeline/run.js +121 -32
  16. package/lib/commonjs/src/core/svg/layers.d.ts +16 -0
  17. package/lib/commonjs/src/core/svg/layers.js +27 -0
  18. package/lib/commonjs/src/core/svg/svg_dom.d.ts +29 -1
  19. package/lib/commonjs/src/core/svg/svg_dom.js +78 -2
  20. package/lib/commonjs/src/core/svg/svg_pathops.d.ts +11 -0
  21. package/lib/commonjs/src/core/svg/svg_pathops.js +209 -19
  22. package/lib/commonjs/src/core/types.d.ts +30 -15
  23. package/lib/module/core/font/compile.js +52 -41
  24. package/lib/module/core/font/compile.js.map +1 -1
  25. package/lib/module/core/pipeline/managers.js +17 -3
  26. package/lib/module/core/pipeline/managers.js.map +1 -1
  27. package/lib/module/core/pipeline/run.js +131 -44
  28. package/lib/module/core/pipeline/run.js.map +1 -1
  29. package/lib/module/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
  30. package/lib/module/core/svg/layers.js +34 -0
  31. package/lib/module/core/svg/layers.js.map +1 -1
  32. package/lib/module/core/svg/svg_dom.js +91 -4
  33. package/lib/module/core/svg/svg_dom.js.map +1 -1
  34. package/lib/module/core/svg/svg_pathops.js +203 -19
  35. package/lib/module/core/svg/svg_pathops.js.map +1 -1
  36. package/lib/module/createNanoIconsSet.js +3 -79
  37. package/lib/module/createNanoIconsSet.js.map +1 -1
  38. package/lib/module/createNanoIconsSet.native.js +108 -0
  39. package/lib/module/createNanoIconsSet.native.js.map +1 -0
  40. package/lib/module/createNanoIconsSet.shared.js +91 -0
  41. package/lib/module/createNanoIconsSet.shared.js.map +1 -0
  42. package/lib/module/index.js +1 -2
  43. package/lib/module/index.js.map +1 -1
  44. package/lib/module/specs/NanoIconViewNativeComponent.ts +15 -0
  45. package/lib/module/types.js +4 -0
  46. package/lib/module/types.js.map +1 -0
  47. package/lib/module/utils/shallowEqualColor.js +15 -0
  48. package/lib/module/utils/shallowEqualColor.js.map +1 -0
  49. package/lib/typescript/src/core/font/compile.d.ts +13 -2
  50. package/lib/typescript/src/core/font/compile.d.ts.map +1 -1
  51. package/lib/typescript/src/core/pipeline/managers.d.ts.map +1 -1
  52. package/lib/typescript/src/core/pipeline/run.d.ts.map +1 -1
  53. package/lib/typescript/src/core/svg/layers.d.ts +16 -0
  54. package/lib/typescript/src/core/svg/layers.d.ts.map +1 -1
  55. package/lib/typescript/src/core/svg/svg_dom.d.ts +29 -1
  56. package/lib/typescript/src/core/svg/svg_dom.d.ts.map +1 -1
  57. package/lib/typescript/src/core/svg/svg_pathops.d.ts +11 -0
  58. package/lib/typescript/src/core/svg/svg_pathops.d.ts.map +1 -1
  59. package/lib/typescript/src/core/types.d.ts +30 -15
  60. package/lib/typescript/src/core/types.d.ts.map +1 -1
  61. package/lib/typescript/src/createNanoIconsSet.d.ts +5 -18
  62. package/lib/typescript/src/createNanoIconsSet.d.ts.map +1 -1
  63. package/lib/typescript/src/createNanoIconsSet.native.d.ts +7 -0
  64. package/lib/typescript/src/createNanoIconsSet.native.d.ts.map +1 -0
  65. package/lib/typescript/src/createNanoIconsSet.shared.d.ts +11 -0
  66. package/lib/typescript/src/createNanoIconsSet.shared.d.ts.map +1 -0
  67. package/lib/typescript/src/index.d.ts.map +1 -1
  68. package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts +14 -0
  69. package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts.map +1 -0
  70. package/lib/typescript/src/types.d.ts +19 -0
  71. package/lib/typescript/src/types.d.ts.map +1 -0
  72. package/lib/typescript/src/utils/shallowEqualColor.d.ts +4 -0
  73. package/lib/typescript/src/utils/shallowEqualColor.d.ts.map +1 -0
  74. package/package.json +22 -5
  75. package/react-native-nano-icons.podspec +18 -0
  76. package/scripts/cli.ts +14 -5
  77. package/src/core/font/compile.ts +65 -61
  78. package/src/core/pipeline/managers.ts +26 -3
  79. package/src/core/pipeline/run.ts +156 -38
  80. package/src/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
  81. package/src/core/svg/layers.ts +44 -0
  82. package/src/core/svg/svg_dom.ts +96 -4
  83. package/src/core/svg/svg_pathops.ts +245 -27
  84. package/src/core/types.ts +21 -10
  85. package/src/createNanoIconsSet.native.tsx +140 -0
  86. package/src/createNanoIconsSet.shared.tsx +121 -0
  87. package/src/createNanoIconsSet.tsx +7 -126
  88. package/src/index.ts +1 -2
  89. package/src/specs/NanoIconViewNativeComponent.ts +15 -0
  90. package/src/types.ts +27 -0
  91. package/src/utils/shallowEqualColor.ts +17 -0
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runPipeline = runPipeline;
7
- const node_fs_1 = __importDefault(require("node:fs"));
8
7
  const promises_1 = __importDefault(require("node:fs/promises"));
9
8
  const node_path_1 = __importDefault(require("node:path"));
10
9
  const compile_js_1 = require("../font/compile.js");
@@ -12,6 +11,64 @@ const managers_js_1 = require("./managers.js");
12
11
  const config_js_1 = require("./config.js");
13
12
  const svg_dom_js_1 = require("../svg/svg_dom.js");
14
13
  const layers_js_1 = require("../svg/layers.js");
14
+ const svg_pathops_js_1 = require("../svg/svg_pathops.js");
15
+ /**
16
+ * Concatenate multiple SVG path `d` strings into a single compound path.
17
+ * This preserves the exact geometry of each path (no boolean operations)
18
+ * while combining them into one glyph. Under nonzero winding, this renders
19
+ * identically to drawing each path separately with the same color.
20
+ */
21
+ function concatPathDs(ds) {
22
+ if (ds.length === 0)
23
+ return null;
24
+ if (ds.length === 1)
25
+ return ds[0];
26
+ return ds.join(' ');
27
+ }
28
+ /**
29
+ * Merge consecutive same-color paths into single compound paths via boolean UNION.
30
+ * Preserves z-order: only merges runs of adjacent paths with identical fill color.
31
+ */
32
+ function mergeSameColorPaths(paths, logger) {
33
+ if (paths.length <= 1)
34
+ return paths;
35
+ const result = [];
36
+ let i = 0;
37
+ while (i < paths.length) {
38
+ const fill = paths[i].fill;
39
+ // Find consecutive run of same fill that are all mergeable.
40
+ // Paths converted from evenodd have compound hole structure and must not
41
+ // be merged — their CW hole contours would cancel CCW contours from
42
+ // adjacent paths, producing incorrect fill.
43
+ let j = i + 1;
44
+ if (!paths[i].noMerge) {
45
+ while (j < paths.length &&
46
+ paths[j].fill === fill &&
47
+ !paths[j].noMerge) {
48
+ j++;
49
+ }
50
+ }
51
+ if (j - i === 1) {
52
+ result.push(paths[i]);
53
+ }
54
+ else {
55
+ const group = paths.slice(i, j);
56
+ const merged = concatPathDs(group.map((p) => p.d));
57
+ if (merged) {
58
+ logger?.info(` ⊕ Merged ${group.length} same-color paths (fill=${fill})`);
59
+ result.push({ d: merged, fill });
60
+ }
61
+ else {
62
+ result.push(...group);
63
+ }
64
+ }
65
+ i = j;
66
+ }
67
+ return result;
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // Pipeline
71
+ // ---------------------------------------------------------------------------
15
72
  /**
16
73
  * Run the font pipeline with given config and paths.
17
74
  * Uses the singleton Pyodide/PathKit instance (initialized on first call).
@@ -20,19 +77,21 @@ async function runPipeline(config, paths, options) {
20
77
  const startTime = Date.now();
21
78
  const logger = options?.logger;
22
79
  logger?.update(`Building "${config.fontFamily}"…`);
23
- (0, config_js_1.ensureEmptyDir)(paths.tempDir);
24
80
  (0, config_js_1.ensureDir)(paths.outputDir);
25
81
  const files = (await promises_1.default.readdir(paths.inputDir)).filter((f) => f.toLowerCase().endsWith('.svg'));
26
82
  const glyphMap = {
27
- meta: {
28
- fontFamily: config.fontFamily,
29
- upm: config.upm,
30
- safeZone: config.safeZone,
31
- startUnicode: config.startUnicode,
83
+ m: {
84
+ f: config.fontFamily,
85
+ u: config.upm,
86
+ z: config.safeZone,
87
+ s: config.startUnicode,
32
88
  },
33
- icons: {},
89
+ i: {},
34
90
  };
35
91
  let currentUnicode = config.startUnicode;
92
+ const codepointToIcon = new Map();
93
+ const allGlyphs = [];
94
+ const PathKit = await managers_js_1.PathKitManager.getInstance();
36
95
  for (const file of files) {
37
96
  const iconName = node_path_1.default.parse(file).name;
38
97
  const filePath = node_path_1.default.join(paths.inputDir, file);
@@ -44,55 +103,85 @@ async function runPipeline(config, paths, options) {
44
103
  continue;
45
104
  }
46
105
  const preprocessed = (0, svg_dom_js_1.preprocessSvg)(rawContent);
106
+ // Preserve original evenodd `d` strings BEFORE picosvg processes them.
107
+ // Picosvg's simplify (via our PathKit shim) can drop contours from
108
+ // multi-subpath evenodd paths — we restore the originals after.
109
+ const originalEvenoddDs = (0, svg_dom_js_1.extractOriginalEvenoddDs)(preprocessed);
47
110
  const flattenedSvg = await (0, managers_js_1.picoFromFile)(filePath, preprocessed);
48
- const parsed = (0, svg_dom_js_1.parseFlattenedSvg)(flattenedSvg);
111
+ const parsed = (0, svg_dom_js_1.parseFlattenedSvg)(flattenedSvg, {
112
+ onSanitize: (original) => {
113
+ logger?.info(` ⚠ Sanitized path in "${file}": path was missing initial moveto (prepended M from endpoint)`);
114
+ logger?.info(` Original: ${original.slice(0, 80)}…`);
115
+ },
116
+ });
117
+ // Restore original evenodd path data (undamaged by picosvg's simplify),
118
+ // then convert to nonzero winding with our containment-based algorithm.
119
+ // Mark as noMerge — compound paths with holes must stay separate so their
120
+ // CW hole contours don't cancel adjacent paths' CCW contours.
121
+ if (originalEvenoddDs.length > 0) {
122
+ (0, svg_dom_js_1.restoreOriginalEvenoddDs)(parsed.paths, originalEvenoddDs);
123
+ }
124
+ for (const p of parsed.paths) {
125
+ if (p.fillRule === 'evenodd') {
126
+ logger?.info(` ↻ Converting evenodd path to nonzero winding in "${file}"`);
127
+ p.d = (0, svg_pathops_js_1.convertEvenoddToWinding)(PathKit, p.d);
128
+ delete p.fillRule;
129
+ p.noMerge = true;
130
+ }
131
+ }
132
+ // Merge consecutive same-color paths into single compound glyphs
133
+ const mergedPaths = mergeSameColorPaths(parsed.paths, logger);
49
134
  const { vx, vy, scale, xOff, yOff, adv } = (0, layers_js_1.computePlacement)({
50
135
  upm: config.upm,
51
136
  safeZone: config.safeZone,
52
137
  viewBox: parsed.viewBox,
53
138
  });
54
- const entry = { adv, layers: [] };
55
- for (const p of parsed.paths) {
139
+ const layers = [];
140
+ for (const p of mergedPaths) {
56
141
  if ((0, svg_dom_js_1.shouldSkipPath)(p.d, p.fill))
57
142
  continue;
58
143
  const cp = currentUnicode++;
59
- await (0, layers_js_1.writeLayerSvg)({
60
- tempDir: paths.tempDir,
61
- upm: config.upm,
62
- adv,
144
+ codepointToIcon.set(cp, iconName);
145
+ const fontD = (0, layers_js_1.transformPathForFont)(PathKit, p.d, {
63
146
  vx,
64
147
  vy,
65
148
  scale,
66
149
  xOff,
67
150
  yOff,
68
- d: p.d,
151
+ upm: config.upm,
152
+ });
153
+ allGlyphs.push({
69
154
  codepoint: cp,
155
+ advanceWidth: adv,
156
+ d: fontD,
70
157
  });
71
- entry.layers.push({ codepoint: cp, color: p.fill || 'black' });
158
+ layers.push([cp, p.fill || 'black']);
72
159
  }
73
- if (entry.layers.length > 0) {
74
- glyphMap.icons[iconName] = entry;
160
+ if (layers.length > 0) {
161
+ glyphMap.i[iconName] = [adv, layers];
75
162
  }
76
163
  }
77
164
  const glyphmapPath = node_path_1.default.join(paths.outputDir, `${config.fontFamily}.glyphmap.json`);
78
165
  if (options?.inputHash) {
79
- glyphMap.meta.hash = options.inputHash;
166
+ glyphMap.m.h = options.inputHash;
80
167
  }
81
- await promises_1.default.writeFile(glyphmapPath, JSON.stringify(glyphMap, null, 2), 'utf8');
168
+ await promises_1.default.writeFile(glyphmapPath, JSON.stringify(glyphMap), 'utf8');
82
169
  logger?.info(`Compiling TTF…`);
83
170
  const ttfPath = node_path_1.default.join(paths.outputDir, `${config.fontFamily}.ttf`);
84
- await (0, compile_js_1.compileTtfFromGlyphSVGs)({
85
- glyphDir: paths.tempDir,
86
- outTtfPath: ttfPath,
87
- fontName: config.fontFamily,
88
- upm: config.upm,
89
- ascent: config.upm,
90
- descent: 0,
91
- });
92
- if (node_fs_1.default.existsSync(paths.tempDir)) {
93
- node_fs_1.default.rmSync(paths.tempDir, { recursive: true, force: true });
171
+ try {
172
+ await (0, compile_js_1.compileTtfFromGlyphs)({
173
+ glyphs: allGlyphs,
174
+ outTtfPath: ttfPath,
175
+ fontName: config.fontFamily,
176
+ upm: config.upm,
177
+ ascent: config.upm,
178
+ descent: 0,
179
+ });
180
+ }
181
+ catch (err) {
182
+ (0, compile_js_1.parseCompileTtfFromGlyphsError)(err, codepointToIcon);
94
183
  }
95
- const iconCount = Object.keys(glyphMap.icons).length;
184
+ const iconCount = Object.keys(glyphMap.i).length;
96
185
  const elapsed = Date.now() - startTime;
97
186
  logger?.succeed(`Built ${config.fontFamily}.ttf [${iconCount} icon${iconCount === 1 ? '' : 's'} in ${elapsed}ms]`);
98
187
  return { ttfPath, glyphmapPath };
@@ -1,3 +1,4 @@
1
+ import type { PathKitModule } from '../types.js';
1
2
  export declare function computePlacement(opts: {
2
3
  upm: number;
3
4
  safeZone: number;
@@ -22,3 +23,18 @@ export declare function writeLayerSvg(opts: {
22
23
  d: string;
23
24
  codepoint: number;
24
25
  }): Promise<void>;
26
+ /**
27
+ * Transform an SVG path `d` string from source SVG coordinates into font
28
+ * glyph coordinates (Y-up, with placement scaling and centering applied).
29
+ *
30
+ * Combines placement transform (translate + scale + translate) with the
31
+ * SVG→font Y-axis flip into a single affine transform applied via PathKit.
32
+ */
33
+ export declare function transformPathForFont(PathKit: PathKitModule, d: string, opts: {
34
+ vx: number;
35
+ vy: number;
36
+ scale: number;
37
+ xOff: number;
38
+ yOff: number;
39
+ upm: number;
40
+ }): string;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.computePlacement = computePlacement;
7
7
  exports.writeLayerSvg = writeLayerSvg;
8
+ exports.transformPathForFont = transformPathForFont;
8
9
  const promises_1 = __importDefault(require("node:fs/promises"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
10
11
  function roundInt(n) {
@@ -40,3 +41,29 @@ async function writeLayerSvg(opts) {
40
41
  </svg>`.trim();
41
42
  await promises_1.default.writeFile(node_path_1.default.join(opts.tempDir, `u${hex}.svg`), layerSvg, 'utf8');
42
43
  }
44
+ /**
45
+ * Transform an SVG path `d` string from source SVG coordinates into font
46
+ * glyph coordinates (Y-up, with placement scaling and centering applied).
47
+ *
48
+ * Combines placement transform (translate + scale + translate) with the
49
+ * SVG→font Y-axis flip into a single affine transform applied via PathKit.
50
+ */
51
+ function transformPathForFont(PathKit, d, opts) {
52
+ const { vx, vy, scale, xOff, yOff, upm } = opts;
53
+ const p = PathKit.FromSVGString(d);
54
+ if (!p)
55
+ return d;
56
+ // Combined affine: placement + Y-flip for font coordinates.
57
+ // x' = scale * (x - vx) + xOff
58
+ // y' = upm - (scale * (y - vy) + yOff)
59
+ //
60
+ // SkMatrix row-major: [scaleX, skewX, transX, skewY, scaleY, transY, 0,0,1]
61
+ const scaleX = scale;
62
+ const scaleY = -scale;
63
+ const transX = xOff - vx * scale;
64
+ const transY = upm - yOff + vy * scale;
65
+ p.transform(scaleX, 0, transX, 0, scaleY, transY, 0, 0, 1);
66
+ const result = p.toSVGString();
67
+ p.delete?.();
68
+ return result;
69
+ }
@@ -3,14 +3,27 @@ export type ParsedFlatSvg = {
3
3
  paths: Array<{
4
4
  d: string;
5
5
  fill: string | null;
6
+ fillRule?: 'evenodd';
6
7
  }>;
7
8
  };
8
9
  export declare function calculateOpColor(fill: string | null, opacity: number, el: Element): `rgba(${number},${number},${number},${number})`;
10
+ /**
11
+ * If a flattened path lost its initial moveto (e.g. picosvg dropped an empty
12
+ * `Mx y z` subpath), prepend `M` using the path's last coordinate pair.
13
+ * For closed icon shapes the endpoint equals the start point.
14
+ */
15
+ export declare function sanitizePathData(d: string): {
16
+ d: string;
17
+ sanitized: boolean;
18
+ };
9
19
  export declare const parsePath: (p: Element) => {
10
20
  d: string;
11
21
  fill: string | null;
22
+ fillRule?: "evenodd";
12
23
  };
13
- export declare function parseFlattenedSvg(flattenedSvg: string): ParsedFlatSvg;
24
+ export declare function parseFlattenedSvg(flattenedSvg: string, options?: {
25
+ onSanitize?: (original: string) => void;
26
+ }): ParsedFlatSvg;
14
27
  export declare function shouldSkipPath(d: string, fill: string | null): boolean;
15
28
  export type SvgValidation = {
16
29
  valid: true;
@@ -19,4 +32,19 @@ export type SvgValidation = {
19
32
  reason: string;
20
33
  };
21
34
  export declare function validateSvg(content: string): SvgValidation;
35
+ /**
36
+ * Extract the original `d` strings of evenodd paths from the raw SVG
37
+ * BEFORE picosvg processes it. Picosvg's simplify (via our PathKit shim)
38
+ * can drop contours from multi-subpath evenodd paths, so we preserve
39
+ * the originals and apply our own winding conversion later.
40
+ *
41
+ * Returns one `d` string per evenodd path, in document order.
42
+ */
43
+ export declare function extractOriginalEvenoddDs(svgContent: string): string[];
44
+ /**
45
+ * Replace picosvg's (potentially damaged) evenodd path data with the
46
+ * preserved originals. Matches by position: the Nth evenodd path in
47
+ * the parsed output gets the Nth original `d` string.
48
+ */
49
+ export declare function restoreOriginalEvenoddDs(paths: ParsedFlatSvg['paths'], originalDs: string[]): void;
22
50
  export declare function preprocessSvg(content: string): string;
@@ -2,9 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parsePath = void 0;
4
4
  exports.calculateOpColor = calculateOpColor;
5
+ exports.sanitizePathData = sanitizePathData;
5
6
  exports.parseFlattenedSvg = parseFlattenedSvg;
6
7
  exports.shouldSkipPath = shouldSkipPath;
7
8
  exports.validateSvg = validateSvg;
9
+ exports.extractOriginalEvenoddDs = extractOriginalEvenoddDs;
10
+ exports.restoreOriginalEvenoddDs = restoreOriginalEvenoddDs;
8
11
  exports.preprocessSvg = preprocessSvg;
9
12
  const jsdom_1 = require("jsdom");
10
13
  const parse_1 = require("../../utils/parse");
@@ -26,11 +29,36 @@ function calculateOpColor(fill, opacity, el) {
26
29
  const finalAlpha = +(a * opacity).toFixed(4);
27
30
  return `rgba(${r},${g},${b},${finalAlpha})`;
28
31
  }
32
+ /**
33
+ * If a flattened path lost its initial moveto (e.g. picosvg dropped an empty
34
+ * `Mx y z` subpath), prepend `M` using the path's last coordinate pair.
35
+ * For closed icon shapes the endpoint equals the start point.
36
+ */
37
+ function sanitizePathData(d) {
38
+ const trimmed = d.trim();
39
+ if (!trimmed || /^[Mm]/.test(trimmed)) {
40
+ return { d: trimmed, sanitized: false };
41
+ }
42
+ // Strip trailing close commands, then grab the last two numbers as x,y
43
+ const withoutClose = trimmed.replace(/[Zz]\s*$/, '');
44
+ const nums = withoutClose.match(/-?\d+(?:\.\d+)?/g);
45
+ if (!nums || nums.length < 2) {
46
+ return { d: trimmed, sanitized: false };
47
+ }
48
+ const x = nums[nums.length - 2];
49
+ const y = nums[nums.length - 1];
50
+ return { d: `M${x},${y} ${trimmed}`, sanitized: true };
51
+ }
29
52
  const parsePath = (p) => {
30
53
  const d = p.getAttribute('d') ?? '';
31
54
  const op = p.getAttribute('opacity');
32
55
  const fillOp = p.getAttribute('fill-opacity');
33
56
  const fill = p.getAttribute('fill');
57
+ // picosvg may drop fill-rule but preserve clip-rule; treat either as evenodd
58
+ const fillRule = p.getAttribute('fill-rule') === 'evenodd' ||
59
+ p.getAttribute('clip-rule') === 'evenodd'
60
+ ? 'evenodd'
61
+ : undefined;
34
62
  if (op !== null || fillOp !== null) {
35
63
  const opVal = op !== null ? parseFloat(op) : 1;
36
64
  const fillOpVal = fillOp !== null ? parseFloat(fillOp) : 1;
@@ -38,15 +66,17 @@ const parsePath = (p) => {
38
66
  return {
39
67
  d,
40
68
  fill: calculateOpColor(fill, combinedOpacity, p),
69
+ fillRule,
41
70
  };
42
71
  }
43
72
  return {
44
73
  d,
45
74
  fill,
75
+ fillRule,
46
76
  };
47
77
  };
48
78
  exports.parsePath = parsePath;
49
- function parseFlattenedSvg(flattenedSvg) {
79
+ function parseFlattenedSvg(flattenedSvg, options) {
50
80
  const dom = new jsdom_1.JSDOM(flattenedSvg);
51
81
  const doc = dom.window.document;
52
82
  const svgEl = doc.querySelector('svg');
@@ -58,7 +88,16 @@ function parseFlattenedSvg(flattenedSvg) {
58
88
  ? [viewBoxRaw[0], viewBoxRaw[1], viewBoxRaw[2], viewBoxRaw[3]]
59
89
  : [0, 0, 100, 100];
60
90
  const pathEls = Array.from(doc.querySelectorAll('path'));
61
- const paths = pathEls.map(exports.parsePath).filter((p) => p.d.trim() !== '');
91
+ const paths = pathEls
92
+ .map(exports.parsePath)
93
+ .filter((p) => p.d.trim() !== '')
94
+ .map((p) => {
95
+ const { d, sanitized } = sanitizePathData(p.d);
96
+ if (sanitized) {
97
+ options?.onSanitize?.(p.d);
98
+ }
99
+ return { ...p, d };
100
+ });
62
101
  return { viewBox, paths };
63
102
  }
64
103
  function shouldSkipPath(d, fill) {
@@ -76,6 +115,43 @@ function validateSvg(content) {
76
115
  }
77
116
  return { valid: true };
78
117
  }
118
+ /**
119
+ * Extract the original `d` strings of evenodd paths from the raw SVG
120
+ * BEFORE picosvg processes it. Picosvg's simplify (via our PathKit shim)
121
+ * can drop contours from multi-subpath evenodd paths, so we preserve
122
+ * the originals and apply our own winding conversion later.
123
+ *
124
+ * Returns one `d` string per evenodd path, in document order.
125
+ */
126
+ function extractOriginalEvenoddDs(svgContent) {
127
+ if (!/<[^>]*fill-rule\s*=\s*["']evenodd/i.test(svgContent)) {
128
+ return [];
129
+ }
130
+ const dom = new jsdom_1.JSDOM(svgContent, { contentType: 'image/svg+xml' });
131
+ const doc = dom.window.document;
132
+ const results = [];
133
+ const pathEls = doc.querySelectorAll('path[fill-rule="evenodd"], path[clip-rule="evenodd"]');
134
+ for (const el of pathEls) {
135
+ const d = el.getAttribute('d');
136
+ if (d)
137
+ results.push(d);
138
+ }
139
+ return results;
140
+ }
141
+ /**
142
+ * Replace picosvg's (potentially damaged) evenodd path data with the
143
+ * preserved originals. Matches by position: the Nth evenodd path in
144
+ * the parsed output gets the Nth original `d` string.
145
+ */
146
+ function restoreOriginalEvenoddDs(paths, originalDs) {
147
+ let oi = 0;
148
+ for (const p of paths) {
149
+ if (p.fillRule === 'evenodd' && oi < originalDs.length) {
150
+ p.d = originalDs[oi];
151
+ oi++;
152
+ }
153
+ }
154
+ }
79
155
  // ensure the svg has a xmlns attribute
80
156
  function preprocessSvg(content) {
81
157
  if (/xmlns\s*=/.test(content))
@@ -1,4 +1,15 @@
1
1
  import type { PathKitModule, WrappedPath, Point } from '../types.js';
2
+ /**
3
+ * Convert a path `d` string with evenodd fill semantics to an equivalent
4
+ * path that renders identically under nonzero winding.
5
+ *
6
+ * Steps:
7
+ * 1. Parse via PathKit, set fill type to EVENODD, simplify (resolve topology)
8
+ * 2. Split into contours, compute containment depths
9
+ * 3. Fix winding: even depth = CCW (outer), odd depth = CW (hole)
10
+ * 4. Reconstruct d string
11
+ */
12
+ export declare function convertEvenoddToWinding(PathKit: PathKitModule, d: string): string;
2
13
  export declare function buildPathopsBackend(PathKit: PathKitModule): {
3
14
  create_path(fillTypeInt: number): WrappedPath;
4
15
  clone_path(h: WrappedPath): WrappedPath;