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
@@ -0,0 +1,286 @@
1
+ #import "NanoIconView.h"
2
+ #import <CoreText/CoreText.h>
3
+ #import <React/RCTConversions.h>
4
+ #import <React/RCTFabricComponentsPlugins.h>
5
+ #import <react/renderer/components/RNNanoIconsSpec/ComponentDescriptors.h>
6
+ #import <react/renderer/components/RNNanoIconsSpec/Props.h>
7
+
8
+ using namespace facebook::react;
9
+
10
+ // Drawing canvas that can be shifted outside the Yoga frame.
11
+ @interface NanoIconDrawingView : UIView
12
+ @property (nonatomic, copy) void (^drawBlock)(CGContextRef, CGRect);
13
+ @end
14
+
15
+ @implementation NanoIconDrawingView
16
+ - (instancetype)initWithFrame:(CGRect)frame {
17
+ if (self = [super initWithFrame:frame]) {
18
+ self.opaque = NO;
19
+ self.backgroundColor = [UIColor clearColor];
20
+ self.userInteractionEnabled = NO;
21
+ }
22
+ return self;
23
+ }
24
+ - (void)drawRect:(CGRect)rect {
25
+ if (self.drawBlock) {
26
+ CGContextRef ctx = UIGraphicsGetCurrentContext();
27
+ if (ctx) self.drawBlock(ctx, self.bounds);
28
+ }
29
+ }
30
+ @end
31
+
32
+ @implementation NanoIconView {
33
+ CTFontRef _font;
34
+ NSString *_fontFamily;
35
+ CGFloat _fontSize;
36
+ std::vector<CGGlyph> _glyphs;
37
+ std::vector<uint32_t> _colors;
38
+ std::vector<CGColorRef> _cachedCGColors;
39
+ CGFloat _fitScale;
40
+ CGPoint _baselinePosition;
41
+ BOOL _metricsValid;
42
+ NanoIconDrawingView *_drawingView;
43
+ }
44
+
45
+ - (instancetype)initWithFrame:(CGRect)frame {
46
+ if (self = [super initWithFrame:frame]) {
47
+ static const auto defaultProps = std::make_shared<const NanoIconViewProps>();
48
+ _props = defaultProps;
49
+ self.opaque = NO;
50
+ self.backgroundColor = [UIColor clearColor];
51
+ self.clipsToBounds = NO;
52
+
53
+ _fitScale = 1.0;
54
+ _baselinePosition = CGPointZero;
55
+
56
+ _drawingView = [[NanoIconDrawingView alloc] initWithFrame:self.bounds];
57
+ __weak __typeof(self) weakSelf = self;
58
+ _drawingView.drawBlock = ^(CGContextRef context, CGRect bounds) {
59
+ [weakSelf _drawIconInContext:context bounds:bounds];
60
+ };
61
+ [self addSubview:_drawingView];
62
+ }
63
+ return self;
64
+ }
65
+
66
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
67
+ return concreteComponentDescriptorProvider<NanoIconViewComponentDescriptor>();
68
+ }
69
+
70
+ - (void)updateClippedSubviewsWithClipRect:(__unused CGRect)clipRect
71
+ relativeToView:(__unused UIView *)clipView {}
72
+
73
+ #pragma mark - Metrics
74
+
75
+ // Scale factor to fit the icon font's em square into the view height,
76
+ // and the CoreText baseline origin used for all glyph draws.
77
+ - (void)_updateMetrics {
78
+ if (!_font) {
79
+ _metricsValid = NO;
80
+ return;
81
+ }
82
+ CGFloat ascent = CTFontGetAscent(_font);
83
+ CGFloat descent = CTFontGetDescent(_font);
84
+ CGFloat totalHeight = ascent + descent;
85
+ _fitScale = (totalHeight > 0) ? (self.bounds.size.height / totalHeight) : 1.0;
86
+ _baselinePosition = CGPointMake(0, descent);
87
+ _metricsValid = YES;
88
+ }
89
+
90
+ // Distance from this view's bottom edge to the parent text baseline.
91
+ // Returns 0 when standalone or when the icon is taller than the text line.
92
+ - (CGFloat)_inlineBaselineOffset {
93
+ UIView *current = self.superview;
94
+ NSAttributedString *attrStr = nil;
95
+ while (current) {
96
+ if ([current respondsToSelector:@selector(attributedText)]) {
97
+ attrStr = [current performSelector:@selector(attributedText)];
98
+ if (attrStr.length > 0) break;
99
+ }
100
+ current = current.superview;
101
+ }
102
+ if (!attrStr) return 0;
103
+
104
+ UIFont *f = [attrStr attribute:NSFontAttributeName atIndex:0 effectiveRange:nil];
105
+ if (!f) return 0;
106
+
107
+ NSParagraphStyle *style = [attrStr attribute:NSParagraphStyleAttributeName
108
+ atIndex:0 effectiveRange:nil];
109
+ CGFloat lineHeight = (style && style.maximumLineHeight > 0)
110
+ ? style.maximumLineHeight : f.lineHeight;
111
+
112
+ NSNumber *bOff = [attrStr attribute:NSBaselineOffsetAttributeName
113
+ atIndex:0 effectiveRange:nil];
114
+ CGFloat baselineFromLineTop = f.ascender - (bOff ? bOff.doubleValue : 0);
115
+
116
+ CGFloat frameBottom = self.frame.origin.y + self.frame.size.height;
117
+ CGFloat posInLine = fmod(frameBottom, lineHeight);
118
+ if (posInLine < 0.01) posInLine = lineHeight;
119
+
120
+ return MAX(0, posInLine - baselineFromLineTop);
121
+ }
122
+
123
+ #pragma mark - Layout
124
+
125
+ - (void)layoutSubviews {
126
+ [super layoutSubviews];
127
+ if (!_metricsValid) [self _updateMetrics];
128
+
129
+ CGFloat offset = [self _inlineBaselineOffset];
130
+ _drawingView.frame = CGRectMake(0, -offset,
131
+ self.bounds.size.width, self.bounds.size.height);
132
+ }
133
+
134
+ #pragma mark - Drawing
135
+
136
+ // Render multi-color icons by drawing each color layer glyph at the same
137
+ // position. Layers stack via painter's order to compose the final icon.
138
+ - (void)_drawIconInContext:(CGContextRef)context bounds:(CGRect)bounds {
139
+ if (!_font || _glyphs.empty()) return;
140
+ if (!_metricsValid) [self _updateMetrics];
141
+
142
+ CGContextSaveGState(context);
143
+ // Flip to CoreText coordinates (Y-up) and apply fit scale.
144
+ CGContextTranslateCTM(context, 0, bounds.size.height);
145
+ CGContextScaleCTM(context, 1.0, -1.0);
146
+ CGContextScaleCTM(context, _fitScale, _fitScale);
147
+
148
+ size_t i = 0;
149
+ while (i < _glyphs.size()) {
150
+ if (_glyphs[i] == 0) { i++; continue; }
151
+
152
+ CGColorRef color = (i < _cachedCGColors.size()) ? _cachedCGColors[i] : NULL;
153
+ if (!color) {
154
+ static CGColorRef sBlack = CGColorCreateSRGB(0, 0, 0, 1);
155
+ color = sBlack;
156
+ }
157
+ CGContextSetFillColorWithColor(context, color);
158
+
159
+ // Batch consecutive same-color glyphs.
160
+ size_t batchStart = i;
161
+ size_t batchCount = 0;
162
+ CGPoint posBuf[16];
163
+ CGGlyph glyphBuf[16];
164
+
165
+ while (i < _glyphs.size()) {
166
+ if (_glyphs[i] == 0) { i++; continue; }
167
+ CGColorRef next = (i < _cachedCGColors.size()) ? _cachedCGColors[i] : NULL;
168
+ if (i > batchStart && next != color) break;
169
+ if (batchCount < 16) {
170
+ posBuf[batchCount] = _baselinePosition;
171
+ glyphBuf[batchCount] = _glyphs[i];
172
+ }
173
+ batchCount++;
174
+ i++;
175
+ }
176
+
177
+ CGPoint *positions = posBuf;
178
+ CGGlyph *glyphs = glyphBuf;
179
+ if (batchCount > 16) {
180
+ positions = (CGPoint *)malloc(batchCount * sizeof(CGPoint));
181
+ glyphs = (CGGlyph *)malloc(batchCount * sizeof(CGGlyph));
182
+ size_t idx = 0;
183
+ for (size_t j = batchStart; j < i; j++) {
184
+ if (_glyphs[j] == 0) continue;
185
+ positions[idx] = _baselinePosition;
186
+ glyphs[idx] = _glyphs[j];
187
+ idx++;
188
+ }
189
+ }
190
+
191
+ CTFontDrawGlyphs(_font, glyphs, positions, batchCount, context);
192
+
193
+ if (batchCount > 16) {
194
+ free(positions);
195
+ free(glyphs);
196
+ }
197
+ }
198
+
199
+ CGContextRestoreGState(context);
200
+ }
201
+
202
+ #pragma mark - Props
203
+
204
+ - (void)_releaseCachedColors {
205
+ for (CGColorRef c : _cachedCGColors) CGColorRelease(c);
206
+ _cachedCGColors.clear();
207
+ }
208
+
209
+ // Convert ARGB uint32 color values into cached CGColorRefs.
210
+ - (void)_rebuildCachedColors {
211
+ [self _releaseCachedColors];
212
+ _cachedCGColors.resize(_colors.size());
213
+ for (size_t i = 0; i < _colors.size(); i++) {
214
+ uint32_t ci = _colors[i];
215
+ _cachedCGColors[i] = CGColorCreateSRGB(
216
+ ((ci >> 16) & 0xFF) / 255.0,
217
+ ((ci >> 8) & 0xFF) / 255.0,
218
+ ( ci & 0xFF) / 255.0,
219
+ ((ci >> 24) & 0xFF) / 255.0);
220
+ }
221
+ }
222
+
223
+ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps {
224
+ const auto &oldViewProps = static_cast<const NanoIconViewProps &>(*_props);
225
+ const auto &newViewProps = static_cast<const NanoIconViewProps &>(*props);
226
+
227
+ BOOL fontChanged = NO;
228
+ BOOL needsRedraw = NO;
229
+
230
+ if (oldViewProps.fontFamily != newViewProps.fontFamily ||
231
+ oldViewProps.fontSize != newViewProps.fontSize) {
232
+ if (_font) { CFRelease(_font); _font = NULL; }
233
+ _fontFamily = [NSString stringWithUTF8String:newViewProps.fontFamily.c_str()];
234
+ _fontSize = newViewProps.fontSize;
235
+ _font = CTFontCreateWithName((__bridge CFStringRef)_fontFamily, _fontSize, NULL);
236
+ _metricsValid = NO;
237
+ fontChanged = YES;
238
+ needsRedraw = YES;
239
+ }
240
+
241
+ // Map Unicode codepoints to font glyph IDs, handling surrogate pairs for codepoints > 0xFFFF.
242
+ if (fontChanged || oldViewProps.codepoints != newViewProps.codepoints) {
243
+ const auto &codepoints = newViewProps.codepoints;
244
+ _glyphs.resize(codepoints.size());
245
+ for (size_t i = 0; i < codepoints.size(); i++) {
246
+ int32_t cp = codepoints[i];
247
+ if (cp <= 0xFFFF) {
248
+ UniChar ch = (UniChar)cp;
249
+ CTFontGetGlyphsForCharacters(_font, &ch, &_glyphs[i], 1);
250
+ } else {
251
+ UniChar surr[2] = {
252
+ (UniChar)(0xD800 + ((cp - 0x10000) >> 10)),
253
+ (UniChar)(0xDC00 + ((cp - 0x10000) & 0x3FF))
254
+ };
255
+ CGGlyph pair[2] = {0, 0};
256
+ CTFontGetGlyphsForCharacters(_font, surr, pair, 2);
257
+ _glyphs[i] = pair[0];
258
+ }
259
+ }
260
+ needsRedraw = YES;
261
+ }
262
+
263
+ if (oldViewProps.colors != newViewProps.colors) {
264
+ const auto &colors = newViewProps.colors;
265
+ _colors.resize(colors.size());
266
+ for (size_t i = 0; i < colors.size(); i++) {
267
+ _colors[i] = (uint32_t)colors[i];
268
+ }
269
+ [self _rebuildCachedColors];
270
+ needsRedraw = YES;
271
+ }
272
+
273
+ [super updateProps:props oldProps:oldProps];
274
+ if (needsRedraw) [_drawingView setNeedsDisplay];
275
+ }
276
+
277
+ - (void)dealloc {
278
+ if (_font) CFRelease(_font);
279
+ [self _releaseCachedColors];
280
+ }
281
+
282
+ @end
283
+
284
+ Class<RCTComponentViewProtocol> NanoIconViewCls(void) {
285
+ return NanoIconView.class;
286
+ }
@@ -20,7 +20,7 @@ function shouldSkipGeneration(inputHash, outputDir, fontFamily, logger) {
20
20
  return false;
21
21
  }
22
22
  const glyphmap = JSON.parse(fs_1.default.readFileSync(glyphmapPath, 'utf8'));
23
- const storedHash = glyphmap?.meta?.hash;
23
+ const storedHash = glyphmap?.m?.h;
24
24
  if (storedHash && storedHash === inputHash) {
25
25
  logger?.info(`${fontFamily}: SVG fingerprint unchanged, skipping build.`);
26
26
  return true;
@@ -3,7 +3,7 @@ export type NanoIconsConfig = {
3
3
  iconSets: IconSetConfig[];
4
4
  };
5
5
  /**
6
- * Load .nanoicons.json from the project root.
6
+ * Load .nanoicons.json from the given directory.
7
7
  * Throws with a helpful message if the file is missing or malformed.
8
8
  */
9
- export declare function loadNanoIconsConfig(projectRoot: string): NanoIconsConfig;
9
+ export declare function loadNanoIconsConfig(configRoot: string): NanoIconsConfig;
@@ -7,19 +7,20 @@ exports.loadNanoIconsConfig = loadNanoIconsConfig;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  /**
10
- * Load .nanoicons.json from the project root.
10
+ * Load .nanoicons.json from the given directory.
11
11
  * Throws with a helpful message if the file is missing or malformed.
12
12
  */
13
- function loadNanoIconsConfig(projectRoot) {
14
- const configPath = node_path_1.default.join(projectRoot, '.nanoicons.json');
13
+ function loadNanoIconsConfig(configRoot) {
14
+ const configPath = node_path_1.default.join(configRoot, '.nanoicons.json');
15
15
  if (!node_fs_1.default.existsSync(configPath)) {
16
- throw new Error(`[react-native-nano-icons] No .nanoicons.json found at project root (${projectRoot}).\n` +
17
- `Create one with: { "iconSets": [{ "inputDir": "assets/icons", "fontFamily": "MyIcons" }] }`);
16
+ throw new Error(`🔬❌ [react-native-nano-icons] No .nanoicons.json found at (${configRoot}).\n` +
17
+ `Create one with: { "iconSets": [{ "inputDir": "assets/icons", "fontFamily": "MyIcons" }] } \n` +
18
+ `Or run with --path <dir> to specify a different directory.`);
18
19
  }
19
20
  const raw = node_fs_1.default.readFileSync(configPath, 'utf8');
20
21
  const config = JSON.parse(raw);
21
22
  if (!config?.iconSets?.length) {
22
- throw new Error(`[react-native-nano-icons] .nanoicons.json must contain an "iconSets" array with at least one entry.`);
23
+ throw new Error(`🔬❌ [react-native-nano-icons] .nanoicons.json must contain an "iconSets" array with at least one entry.`);
23
24
  }
24
25
  return config;
25
26
  }
@@ -1,25 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
3
6
  Object.defineProperty(exports, "__esModule", { value: true });
4
7
  /**
5
8
  * Bare React Native workflow: build icon fonts and link them into the native project.
6
9
  *
7
- * Run from your app root: npx react-native-nano-icons [--verbose]
10
+ * Run from your app root: npx react-native-nano-icons [--verbose] [--path <dir>]
8
11
  *
9
12
  * Reads .nanoicons.json (same shape as Expo plugin options) so Expo and bare apps
10
13
  * share one config format.
11
14
  *
12
15
  * Flags:
13
- * --verbose Show per-SVG processing details and pipeline timing
16
+ * --verbose Show per-SVG processing details and pipeline timing
17
+ * --path <dir> Directory containing .nanoicons.json (default: cwd)
14
18
  */
19
+ const node_path_1 = __importDefault(require("node:path"));
15
20
  const index_js_1 = require("../cli/index.js");
16
21
  async function main() {
17
22
  const verbose = process.argv.includes('--verbose');
18
23
  const level = verbose ? 'verbose' : 'normal';
24
+ const pathIdx = process.argv.indexOf('--path');
25
+ const projectRoot = process.cwd();
26
+ const configRoot = pathIdx !== -1 && process.argv[pathIdx + 1]
27
+ ? node_path_1.default.resolve(projectRoot, process.argv[pathIdx + 1])
28
+ : projectRoot;
19
29
  const logger = await (0, index_js_1.createOraLogger)(level);
20
- const config = (0, index_js_1.loadNanoIconsConfig)(process.cwd());
21
- const built = await (0, index_js_1.buildAllFonts)(config.iconSets, process.cwd(), { logger });
22
- await (0, index_js_1.linkBare)(process.cwd(), built, logger);
30
+ const config = (0, index_js_1.loadNanoIconsConfig)(configRoot);
31
+ const built = await (0, index_js_1.buildAllFonts)(config.iconSets, projectRoot, { logger });
32
+ await (0, index_js_1.linkBare)(projectRoot, built, logger);
23
33
  }
24
34
  main().catch((err) => {
25
35
  const message = err instanceof Error ? err.message : String(err);
@@ -1,5 +1,16 @@
1
- export declare function compileTtfFromGlyphSVGs(opts: {
2
- glyphDir: string;
1
+ export type FontGlyph = {
2
+ codepoint: number;
3
+ advanceWidth: number;
4
+ /** Path data already in font coordinates (Y-up, placement applied). */
5
+ d: string;
6
+ };
7
+ export declare function parseCompileTtfFromGlyphsError(err: unknown, codepointToIcon: Map<number, string>): void;
8
+ /**
9
+ * Compile a TTF font from pre-transformed glyph data.
10
+ * Builds SVG font XML directly (no intermediate files), then converts via svg2ttf.
11
+ */
12
+ export declare function compileTtfFromGlyphs(opts: {
13
+ glyphs: FontGlyph[];
3
14
  outTtfPath: string;
4
15
  fontName: string;
5
16
  upm: number;
@@ -3,63 +3,66 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.compileTtfFromGlyphSVGs = compileTtfFromGlyphSVGs;
6
+ exports.parseCompileTtfFromGlyphsError = parseCompileTtfFromGlyphsError;
7
+ exports.compileTtfFromGlyphs = compileTtfFromGlyphs;
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
- const node_events_1 = require("node:events");
10
10
  const metrics_js_1 = require("./metrics.js");
11
11
  const svg2ttf_1 = __importDefault(require("svg2ttf"));
12
- const parse_js_1 = require("../../utils/parse.js");
13
- async function writeGlyphStreamToFont(fontStream, svgPath, filename) {
14
- const codepoint = (0, parse_js_1.parseCodepointFromFilename)(filename);
15
- const name = node_path_1.default.basename(filename, '.svg');
16
- return new Promise((resolve, reject) => {
17
- const glyphStream = node_fs_1.default.createReadStream(svgPath);
18
- glyphStream.metadata = {
19
- name,
20
- unicode: [String.fromCodePoint(codepoint)],
21
- };
22
- glyphStream.on('error', reject);
23
- // Do not add fontStream.on("error", reject) here — one per glyph would exceed
24
- // Node's default MaxListeners (10). Font stream errors are handled once below.
25
- fontStream.write(glyphStream);
26
- glyphStream.on('end', resolve);
12
+ function escapeXml(s) {
13
+ return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
14
+ }
15
+ /**
16
+ * Build an SVG font XML string from pre-transformed glyph data.
17
+ */
18
+ function buildSvgFontXml(opts) {
19
+ const { fontName, glyphs, upm, ascent, descent } = opts;
20
+ const glyphLines = glyphs.map((g) => {
21
+ const hex = g.codepoint.toString(16);
22
+ const name = `u${hex.padStart(4, '0')}`;
23
+ return `<glyph glyph-name="${name}" unicode="&#x${hex};" horiz-adv-x="${g.advanceWidth}" d="${escapeXml(g.d)}"/>`;
27
24
  });
25
+ return `<?xml version="1.0" standalone="no"?>
26
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
27
+ <svg xmlns="http://www.w3.org/2000/svg">
28
+ <defs>
29
+ <font id="${escapeXml(fontName)}" horiz-adv-x="${upm}">
30
+ <font-face font-family="${escapeXml(fontName)}" units-per-em="${upm}" ascent="${ascent}" descent="${-Math.abs(descent)}"/>
31
+ <missing-glyph horiz-adv-x="0"/>
32
+ ${glyphLines.join('\n')}
33
+ </font>
34
+ </defs>
35
+ </svg>`;
36
+ }
37
+ function parseCompileTtfFromGlyphsError(err, codepointToIcon) {
38
+ const msg = err instanceof Error ? err.message : String(err);
39
+ const cpMatch = msg.match(/glyph\s+"u([0-9a-fA-F]+)"/);
40
+ if (cpMatch) {
41
+ const cp = parseInt(cpMatch[1], 16);
42
+ const iconName = codepointToIcon.get(cp);
43
+ const detail = iconName
44
+ ? `icon "${iconName}" (codepoint u${cpMatch[1]})`
45
+ : `codepoint u${cpMatch[1]}`;
46
+ throw new Error(`Font compilation failed for ${detail}: ${msg}`);
47
+ }
48
+ throw err;
28
49
  }
29
- async function compileTtfFromGlyphSVGs(opts) {
30
- const { SVGIcons2SVGFontStream } = await import('svgicons2svgfont');
31
- const { glyphDir, outTtfPath, fontName, upm, ascent, descent } = opts;
50
+ /**
51
+ * Compile a TTF font from pre-transformed glyph data.
52
+ * Builds SVG font XML directly (no intermediate files), then converts via svg2ttf.
53
+ */
54
+ async function compileTtfFromGlyphs(opts) {
55
+ const { glyphs, outTtfPath, fontName, upm, ascent, descent } = opts;
32
56
  const lineGap = opts.lineGap ?? 0;
33
- const files = node_fs_1.default
34
- .readdirSync(glyphDir)
35
- .filter((f) => /^u[0-9a-fA-F]+\.svg$/.test(f))
36
- .sort((a, b) => (0, parse_js_1.parseCodepointFromFilename)(a) - (0, parse_js_1.parseCodepointFromFilename)(b));
37
- if (files.length === 0)
38
- throw new Error(`No glyph SVGs found in: ${glyphDir}`);
39
- const fontStream = new SVGIcons2SVGFontStream({
57
+ if (glyphs.length === 0)
58
+ throw new Error('No glyphs to compile');
59
+ const svgFontString = buildSvgFontXml({
40
60
  fontName,
41
- fontHeight: upm,
42
- normalize: false,
61
+ glyphs,
62
+ upm,
43
63
  ascent,
44
64
  descent,
45
65
  });
46
- const svgFontChunks = [];
47
- fontStream.on('data', (c) => svgFontChunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)));
48
- // Single error listener for the font stream; per-glyph errors are handled in writeGlyphStreamToFont.
49
- let fontStreamReject;
50
- const fontStreamErrorPromise = new Promise((_, rej) => {
51
- fontStreamReject = rej;
52
- });
53
- fontStream.on('error', (err) => fontStreamReject(err));
54
- for (const f of files) {
55
- await Promise.race([
56
- writeGlyphStreamToFont(fontStream, node_path_1.default.join(glyphDir, f), f),
57
- fontStreamErrorPromise,
58
- ]);
59
- }
60
- fontStream.end();
61
- await (0, node_events_1.once)(fontStream, 'end');
62
- const svgFontString = Buffer.concat(svgFontChunks).toString('utf8');
63
66
  const ttfRaw = (0, svg2ttf_1.default)(svgFontString);
64
67
  const rawBuf = Buffer.from(ttfRaw.buffer);
65
68
  const fixedBuf = (0, metrics_js_1.forceTtfMetrics)(rawBuf, upm, ascent, descent, lineGap);
@@ -56,14 +56,30 @@ class PyodideManager {
56
56
  const pathopsPy = await promises_1.default.readFile(pathopsPyPath, 'utf8');
57
57
  py.FS.writeFile('/pathops.py', pathopsPy);
58
58
  await py.loadPackage(['micropip', 'lxml'], { messageCallback: () => { } });
59
+ // Resolve local picosvg wheel path for offline-first installation.
60
+ const picosvgWhlDir = node_path_1.default.join(getPackageRoot(), 'src', 'core', 'shims');
61
+ const picosvgWhl = (await promises_1.default.readdir(picosvgWhlDir))
62
+ .find((f) => f.startsWith('picosvg-') && f.endsWith('.whl'));
63
+ const localWhlUrl = picosvgWhl
64
+ ? `file://${node_path_1.default.join(picosvgWhlDir, picosvgWhl)}`
65
+ : null;
66
+ py.globals.set('_picosvg_local_whl', localWhlUrl);
59
67
  await py.runPythonAsync(`
60
68
  import sys
61
69
  if "/" not in sys.path:
62
70
  sys.path.insert(0, "/")
63
-
71
+
64
72
  import micropip
65
- await micropip.install("picosvg", deps=False)
66
-
73
+
74
+ _whl = _picosvg_local_whl
75
+ if _whl:
76
+ try:
77
+ await micropip.install(_whl, deps=False)
78
+ except Exception:
79
+ await micropip.install("picosvg", deps=False)
80
+ else:
81
+ await micropip.install("picosvg", deps=False)
82
+
67
83
  import pathops
68
84
  import picosvg
69
85
  `);