Binary Parser to BinaryReader Migration Guide
February 11, 2026 · View on GitHub
Overview
This document outlines the migration from binary-parser to our custom BinaryReader implementation for better bundle size, typing, and performance.
Size Comparison
Before:
- binary-parser: ~25-30KB minified in bundle
- ui-xcfimage bundle: 104KB minified
After (Estimated):
- BinaryReader: ~2-3KB minified
- ui-xcfimage bundle: ~80-85KB minified (20-25% reduction)
API Comparison
Basic Types
Before (binary-parser):
const parser = new Parser()
.uint8("field1")
.int8("field2")
.uint32("field3")
.int32("field4")
.floatbe("field5")
.floatle("field6");
After (BinaryReader):
const field1 = reader.readUInt8();
const field2 = reader.readInt8();
const field3 = reader.readUInt32BE();
const field4 = reader.readInt32BE();
const field5 = reader.readFloatBE();
const field6 = reader.readFloatLE();
Strings
Before:
// Zero-terminated
.string("name", { zeroTerminated: true })
// Fixed length
.string("magic", { length: 9 })
After:
// Zero-terminated
const name = reader.readZeroTerminatedString();
// Fixed length
const magic = reader.readString(9);
Buffers
Before:
.buffer("data", { length: "fieldLength" })
After:
const data = reader.readBuffer(fieldLength);
Arrays with readUntil
Before:
.array("layerList", {
type: "int32be",
readUntil: (item) => item === 0
})
After:
const layerList = reader.readUInt32ArrayUntil((item) => item === 0);
Complex Arrays
Before:
.array("propertyList", {
type: propertyParser,
readUntil: (item) => item.type === 0
})
After:
const propertyList = reader.readArrayUntil(
(br) => parseProperty(br),
(item) => item.type === 0
);
Migration Examples
Example 1: RGB Parser
Before:
const rgbParser = new Parser().uint8("red").uint8("green").uint8("blue");
After:
interface RGB {
red: number;
green: number;
blue: number;
}
function parseRGB(reader: BinaryReader): RGB {
return {
red: reader.readUInt8(),
green: reader.readUInt8(),
blue: reader.readUInt8(),
};
}
Example 2: Property with Length
Before:
const prop_modeParser = new Parser().uint32("length", { assert: 4 }).uint32("mode");
After:
interface ModeProp {
length: number;
mode: number;
}
function parseModeProp(reader: BinaryReader): ModeProp {
const length = reader.readUInt32BE();
if (length !== 4) {
throw new XCFParseError(`Expected length 4, got ${length}`);
}
const mode = reader.readUInt32BE();
return { length, mode };
}
Example 3: Choice/Switch Parser
Before:
.choice("data", {
tag: "type",
choices: {
[XCF_PropType.MODE]: modParser,
[XCF_PropType.OPACITY]: opacityParser,
},
defaultChoice: defaultParser
})
After:
function parsePropertyData(reader: BinaryReader, type: number): PropertyData {
switch (type) {
case XCF_PropType.MODE:
return parseModeProp(reader);
case XCF_PropType.OPACITY:
return parseOpacityProp(reader);
default:
return parseDefaultProp(reader);
}
}
Example 4: XCF Header V10
Before:
const gimpHeaderV10 = new Parser()
.endianess("big")
.string("magic", { length: 9 })
.string("version", { length: 4 })
.int8("padding", { assert: 0 })
.uint32("width")
.uint32("height")
.uint32("base_type")
.array("propertyList", {
type: propertyListParser,
readUntil: (item) => item.type === 0,
})
.array("layerList", {
type: "int32be",
readUntil: (item) => item === 0,
});
After:
interface GimpHeaderV10 {
magic: string;
version: string;
padding: number;
width: number;
height: number;
base_type: number;
propertyList: ParsedProperty[];
layerList: number[];
}
function parseGimpHeaderV10(reader: BinaryReader): GimpHeaderV10 {
const magic = reader.readString(9);
const version = reader.readString(4);
const padding = reader.readInt8();
if (padding !== 0) {
throw new XCFParseError(`Expected padding 0, got ${padding}`);
}
const width = reader.readUInt32BE();
const height = reader.readUInt32BE();
const base_type = reader.readUInt32BE();
const propertyList = reader.readArrayUntil(
(br) => parseProperty(br),
(item) => item.type === 0
);
const layerList = reader.readUInt32ArrayUntil((item) => item === 0);
return {
magic,
version,
padding,
width,
height,
base_type,
propertyList,
layerList,
};
}
Benefits
1. Better TypeScript Types
// Before: generic parser types
const result = gimpHeaderV10.parse(buffer); // result is 'any'
// After: strongly typed
const result = parseGimpHeaderV10(reader); // result is GimpHeaderV10
2. Smaller Bundle
- Only includes what we use
- No parser construction overhead
- Tree-shakeable functions
3. Better Performance
- No parser object creation
- Direct Buffer API calls
- No intermediate object allocation
- Streaming read pattern
4. Better Error Messages
- Custom error messages per field
- Context-aware validation
- Stack traces point to actual parsing code
Migration Strategy
- ✅ Create
src/lib/binary-reader.ts - Create parser functions in
src/lib/xcf-parsers.ts - Update
gimpparser.tsto use new parsers - Run full test suite
- Measure bundle size reduction
- Remove binary-parser dependency
Estimated Effort
- Initial setup: 1-2 hours
- Migration: 3-4 hours
- Testing: 1 hour
- Total: 5-7 hours
Risk Assessment
Low Risk because:
- ✅ Comprehensive test suite already exists
- ✅ Tests use real XCF files
- ✅ Can migrate incrementally (keep binary-parser during migration)
- ✅ BinaryReader is simple and testable
- ✅ Existing tests will catch any regressions