Skip to content
105 changes: 105 additions & 0 deletions docs/binary-format-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Iconify Binary Format Spec (v1)

The `.iconbin` format is an optimized binary representation of an Iconify icon collection. It is designed for fast random access, minimal memory footprint, and zero-parsing startup.

## Structure

| Section | Description |
|---|---|
| **Header** | Magic bytes, version, and offsets to other sections. |
| **Metadata** | Collection-level information (prefix, name, license, defaults). |
| **Icon Index** | Sorted list of icon names and pointers to their records. |
| **Alias Index** | Sorted list of alias names and pointers to their records. |
| **Icon Records** | Fixed-size records for each icon. |
| **Alias Records** | Fixed-size records for each alias. |
| **String Table** | Index of string offsets followed by raw UTF-8 data. |

---

## Header (28 bytes)

| Offset | Type | Description |
|---|---|---|
| 0 | uint32 | Magic Bytes: `0x49 0x43 0x4F 0x4E` ("ICON") |
| 4 | uint8 | Version: `0x01` |
| 5 | uint8 | Reserved: `0x00` |
| 6 | uint16 | Icon Count |
| 8 | uint16 | Alias Count |
| 10 | uint32 | String Count |
| 14 | uint32 | Metadata Offset |
| 18 | uint32 | Icon Index Offset |
| 22 | uint32 | Alias Index Offset |
| 26 | uint32 | String Table Offset |

---

## Metadata

| Field | Type | Description |
|---|---|---|
| Prefix | uint32 | String index for the collection prefix (e.g., "mdi"). |
| Name | uint32 | String index for the human-readable name. |
| Total Icons | uint32 | Total icons in the upstream set. |
| Author Name | uint32 | String index. |
| Author URL | uint32 | String index. |
| License Title | uint32 | String index. |
| License SPDX | uint32 | String index. |
| License URL | uint32 | String index. |
| Attribution | uint8 | `0x01` if requires attribution, else `0x00`. |
| Default Width | float32 | Default viewbox width. |
| Default Height | float32 | Default viewbox height. |

---

## Index Sections (Icon & Alias)

The index allows binary search for icon/alias names.

| Field | Type | Description |
|---|---|---|
| Name Index | uint32 | String index for the icon/alias name. |
| Record Offset | uint32 | Absolute offset to the corresponding Record section. |

---

## Icon Record (14 bytes)

| Field | Type | Description |
|---|---|---|
| Body | uint32 | String index for the SVG path data. |
| Width | float32 | Viewbox width. |
| Height | float32 | Viewbox height. |
| Flags | uint8 | Bit 0: hidden, Bit 1: hFlip, Bit 2: vFlip. |
| Rotate | uint8 | 0, 1 (90°), 2 (180°), 3 (270°). |

---

## Alias Record (14 bytes)

| Field | Type | Description |
|---|---|---|
| Parent | uint32 | String index for the parent icon name. |
| Width | float32 | Width override (optional). |
| Height | float32 | Height override (optional). |
| Flags | uint8 | See below. |
| Rotate | uint8 | 0, 1, 2, 3. |

**Alias Flags:**
- Bit 0: hasWidth override
- Bit 1: hasHeight override
- Bit 2: hasRotate override
- Bit 3: hasHFlip override
- Bit 4: hasVFlip override
- Bit 5: hFlip value
- Bit 6: vFlip value

---

## String Table

Strings are indexed by their order of appearance (0-based).

| Field | Type | Description |
|---|---|---|
| Offsets | uint32[String Count] | Absolute offsets to the start of each length-prefixed string. |
| Strings | LengthPrefixedString[] | `uint32 length` + `uint8 data[]` |
36 changes: 36 additions & 0 deletions docs/performance-baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Performance Baseline

This document tracks performance benchmarks for the Iconify SDK v2.

## Test Environment
- **Device**: Linux Workstation
- **Dart Version**: 3.5.0
- **Data Set**: Material Design Icons (MDI) full collection (~7,600 icons, 3.3 MB JSON)

## Parse Performance (Cold Start)

| Format | Full Collection Parse | Single Icon Lookup | Size on Disk |
|---|---|---|---|
| **JSON** | 73ms | ~2ms (est) | 3.32 MB |
| **Binary (.iconbin)** | 24ms | 0.005ms (4.8μs) | 2.91 MB |
| **Improvement** | **3.0x** | **~400x** | **12% smaller** |

### Insights
- **Binary Format**: The binary format eliminates JSON tokenization and string escaping overhead. The string table with offset index allows $O(1)$ access to any string.
- **Lazy Decoding**: `BinaryIconifyProvider` uses `decodeIcon` to extract single icons without parsing the rest of the collection, leading to sub-millisecond resolution even for massive sets.
- **Zero Parsing**: Header and index structures are designed for direct `ByteData` reading, making the "parse" time almost entirely limited by memory I/O.

## Benchmarks Log

### 2026-03-16
- **Device**: macOS Workstation (Apple M2)
- **Dart Version**: 3.5.0
- **Full Parse (MDI)**: 29ms (JSON) vs 11ms (Binary) — **2.6x Speedup**
- **Single Icon Lookup (Binary)**: 3.9μs vs 11.8ms (JSON parse+extract) — **~3000x Speedup**
- **PictureCache Hit**: 0.4μs (10k iterations in 4ms)
- **PictureCache Eviction Overhead**: Negligible (<1ms for 100 evictions)
- **Parallel Preloading**: 7ms (Parallel) vs 22ms (Sequential) for 5 collections — **3.1x Speedup**

### 2026-03-15
- Initial benchmark of `BinaryIconFormat` v1.
- Results confirmed 3x faster full parse and extremely fast random access.
110 changes: 88 additions & 22 deletions packages/cli/lib/src/commands/generate_command.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';

import 'package:args/command_runner.dart';
Expand All @@ -23,6 +24,13 @@ class GenerateCommand extends Command<int> {
help: 'Path to generate ICON_ATTRIBUTION.md.',
defaultsTo: 'ICON_ATTRIBUTION.md',
);
argParser.addOption(
'format',
abbr: 'f',
help: 'Output format for icon data.',
allowed: ['dart', 'binary', 'sprite', 'all'],
defaultsTo: 'dart',
);
}

@override
Expand Down Expand Up @@ -125,29 +133,86 @@ class GenerateCommand extends Command<int> {
}
}

progress.update('Generating code...');
final format = argResults?['format'] as String;
final bool generateDart = format == 'dart' || format == 'all';
final bool generateBinary = format == 'binary' || format == 'all';
final bool generateSprite = format == 'sprite' || format == 'all';

if (generateDart) {
progress.update('Generating Dart code...');
final outputContent = IconCodeGenerator.generate(
usedIconNames: usedIcons,
iconDataMap: iconDataMap,
);

if (argResults?['dry-run'] == true) {
_logger.info('\n--- DART PREVIEW ---');
_logger.info(outputContent.split('\n').take(10).join('\n'));
_logger.info('...');
} else {
final outputFile = File(config.output);
if (!outputFile.parent.existsSync()) {
outputFile.parent.createSync(recursive: true);
}
await outputFile.writeAsString(outputContent);
}
}

// 4. Generate Dart code
final outputContent = IconCodeGenerator.generate(
usedIconNames: usedIcons,
iconDataMap: iconDataMap,
);
if (generateBinary) {
progress.update('Generating Binary files...');
for (final entry in collections.entries) {
final prefix = entry.key;
final collection = entry.value;
final encoded = BinaryIconFormat.encode(collection);

if (argResults?['dry-run'] == true) {
progress.complete(
'Dry run: Code generation would produce ${iconDataMap.length} icons.');
_logger.info('\n--- PREVIEW ---');
_logger.info(outputContent.split('\n').take(20).join('\n'));
_logger.info('...');
return ExitCode.success.code;
if (argResults?['dry-run'] == true) {
_logger.info(
'Dry run: Would write $prefix.iconbin (${encoded.length} bytes)');
} else {
final binaryFile = File('${config.dataDir}/$prefix.iconbin');
await binaryFile.writeAsBytes(encoded);
_logger.info('✅ Generated ${binaryFile.path}');
}
}
}

// 5. Write to disk
final outputFile = File(config.output);
if (!outputFile.parent.existsSync()) {
outputFile.parent.createSync(recursive: true);
if (generateSprite) {
progress.update('Generating SVG Sprite Sheet...');
final buffer = StringBuffer();
buffer.writeln(
'<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="display:none;">');

final sortedKeys = iconDataMap.keys.toList()..sort();
for (final fullName in sortedKeys) {
final data = iconDataMap[fullName]!;
final id = fullName.replaceAll(':', '-');
buffer.writeln(
' <symbol id="$id" viewBox="0 0 ${data.width} ${data.height}">');
buffer.writeln(' ${data.body}');
buffer.writeln(' </symbol>');
}
buffer.writeln('</svg>');

if (argResults?['dry-run'] == true) {
_logger.info(
'Dry run: Would write icons.sprite.svg (${buffer.length} bytes)');
} else {
final spriteFile = File('${config.dataDir}/icons.sprite.svg');
await spriteFile.writeAsString(buffer.toString());
_logger.info('✅ Generated ${spriteFile.path}');

// Generate manifest for SpriteIconifyProvider
final manifest = {
'icons': iconDataMap.map((key, value) => MapEntry(key, {
'width': value.width,
'height': value.height,
})),
};
final manifestFile = File('${config.dataDir}/icons.sprite.json');
await manifestFile.writeAsString(jsonEncode(manifest));
_logger.info('✅ Generated ${manifestFile.path}');
}
}
await outputFile.writeAsString(outputContent);

// 6. Generate Attribution File
if (attributionRequired.isNotEmpty) {
Expand All @@ -172,12 +237,13 @@ class GenerateCommand extends Command<int> {
}
buffer.writeln();
}
await attributionFile.writeAsString(buffer.toString());
_logger.info('✅ Generated $attributionPath');
if (argResults?['dry-run'] != true) {
await attributionFile.writeAsString(buffer.toString());
_logger.info('✅ Generated $attributionPath');
}
}

progress.complete(
'Successfully generated ${iconDataMap.length} icons into ${config.output}');
progress.complete('Successfully generated icon data ($format)');
return ExitCode.success.code;
}
}
9 changes: 7 additions & 2 deletions packages/cli/lib/src/commands/sync_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class SyncCommand extends Command<int> {
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
commitRef = json['sha'] as String;
progress.complete('Latest commit: ${commitRef.substring(0, 7)}');
progress.complete('Latest commit: ${_shortRef(commitRef)}');
} else {
progress.complete(
'GitHub API failed (HTTP ${response.statusCode}). Falling back to "master".');
Expand All @@ -115,7 +115,7 @@ class SyncCommand extends Command<int> {
}

_logger.info(
'🔄 Syncing ${prefixes.length} collections (ref: ${commitRef.substring(0, 7)}) to ${config.dataDir}...');
'🔄 Syncing ${prefixes.length} collections (ref: ${_shortRef(commitRef)}) to ${config.dataDir}...');

final dataDir = Directory(config.dataDir);
if (!dataDir.existsSync()) {
Expand Down Expand Up @@ -201,4 +201,9 @@ class SyncCommand extends Command<int> {

return ExitCode.success.code;
}

String _shortRef(String ref) {
if (ref.length > 7) return ref.substring(0, 7);
return ref;
}
}
9 changes: 7 additions & 2 deletions packages/cli/lib/src/commands/verify_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class VerifyCommand extends Command<int> {
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
latestCommit = json['sha'] as String;
progressSha.complete('Latest commit: ${latestCommit.substring(0, 7)}');
progressSha.complete('Latest commit: ${_shortRef(latestCommit)}');
} else {
progressSha.complete(
'GitHub API failed (HTTP ${response.statusCode}). Falling back to "master".');
Expand All @@ -95,7 +95,7 @@ class VerifyCommand extends Command<int> {
}

_logger.info(
'🔍 Verifying ${prefixes.length} collections against upstream (ref: ${latestCommit.substring(0, 7)})...');
'🔍 Verifying ${prefixes.length} collections against upstream (ref: ${_shortRef(latestCommit)})...');

var mismatchCount = 0;
var errorCount = 0;
Expand Down Expand Up @@ -155,4 +155,9 @@ class VerifyCommand extends Command<int> {
return ExitCode.software.code;
}
}

String _shortRef(String ref) {
if (ref.length > 7) return ref.substring(0, 7);
return ref;
}
}
Loading
Loading