Skip to content

Instantly share code, notes, and snippets.

@PhoenixIllusion
Created October 22, 2025 14:14
Show Gist options
  • Save PhoenixIllusion/e5c31d416443b336a5a5bba73dcdd660 to your computer and use it in GitHub Desktop.
Save PhoenixIllusion/e5c31d416443b336a5a5bba73dcdd660 to your computer and use it in GitHub Desktop.
Parsing of a HEIC file. Not exhaustive, just worked on my test image.
export interface BlobLike {
size: number;
slice(start_offset: number, end_offset: number): BlobLike | Promise<BlobLike>;
arrayBuffer(): Promise<ArrayBuffer>;
}
export const enum LogLevel {
OFF,
ERROR,
WARN,
VERBOSE
}
let LOG_LEVEL = LogLevel.OFF;
export function setLogLevel(level: LogLevel) {
LOG_LEVEL = level;
}
function bigIntToInt(bigInt: bigint): number {
if (bigInt >= Number.MIN_SAFE_INTEGER && bigInt <= Number.MAX_SAFE_INTEGER) {
return Number(bigInt);
} else {
throw new Error("BigInt value is outside the safe integer range.");
}
}
export function ERR_FAIL_V_MSG(label: string, log: string) {
throw new Error(`${label}: ${log}`)
}
export function ERR_FAIL_COND_V_MSG(test: boolean, check: boolean, log: string) {
if (test != check) {
throw new Error(log);
}
}
export function WARN_PRINT(log: string) {
if (LOG_LEVEL >= LogLevel.WARN)
console.warn(log);
}
export function ERR_FAIL_V(log: string) {
new Error(log);
}
export function ERR_PRINT(log: string) {
if (LOG_LEVEL >= LogLevel.ERROR)
console.error(log);
}
export const decoder = new TextDecoder();
export class DataReader {
private index = 0;
constructor(private dataView: DataView, private littleEndian = false) {
this.index = 0;
}
U8(): number {
this.index++;
return this.dataView.getUint8(this.index - 1);
}
U16(endian?: boolean): number {
endian = endian === undefined ? this.littleEndian : endian;
this.index += 2;
return this.dataView.getUint16(this.index - 2, endian);
}
S32(endian?: boolean): number {
endian = endian === undefined ? this.littleEndian : endian;
this.index += 4;
return this.dataView.getInt32(this.index - 4, endian);
}
U32(endian?: boolean): number {
endian = endian === undefined ? this.littleEndian : endian;
this.index += 4;
return this.dataView.getUint32(this.index - 4, endian);
}
U64(endian?: boolean): number {
endian = endian === undefined ? this.littleEndian : endian;
this.index += 8;
return bigIntToInt(this.dataView.getBigUint64(this.index - 8, endian));
}
S64(endian?: boolean): number {
endian = endian === undefined ? this.littleEndian : endian;
this.index += 8;
return bigIntToInt(this.dataView.getBigInt64(this.index - 8, endian));
}
U64_BigInt(endian?: boolean): bigint {
endian = endian === undefined ? this.littleEndian : endian;
this.index += 8;
return this.dataView.getBigUint64(this.index - 8, endian);
}
S64_BigInt(endian?: boolean): bigint {
endian = endian === undefined ? this.littleEndian : endian;
this.index += 8;
return this.dataView.getBigInt64(this.index - 8, endian);
}
F32(endian?: boolean): number {
endian = endian === undefined ? this.littleEndian : endian;
this.index += 4;
return this.dataView.getFloat32(this.index - 4, endian);
}
F64(endian?: boolean): number {
endian = endian === undefined ? this.littleEndian : endian;
this.index += 8;
return this.dataView.getFloat64(this.index - 8, endian);
}
CHUNK(len: number): Uint8Array {
this.index += len;
return new Uint8Array(this.dataView.buffer, this.index - len, len)
}
SKIP(len: number): void {
this.index += len;
}
SEEK(offset: number) {
this.index = offset;
}
INDEX() {
return this.index;
}
}
import { DataReader } from './data-reader';
function readString(reader: DataReader) {
const start = reader.INDEX();
while(reader.U8() != 0) {}
const end = reader.INDEX();
reader.SEEK(start);
const res = decoder.decode(reader.CHUNK(end - start - 1));
reader.U8();
return res;
}
const img_ab = await fetch('IMG.HEIC').then(a => a.arrayBuffer());
interface Box {
start: number;
end: number;
boxtype: string;
payload: Uint8Array;
}
interface IFullBox {
version: number;
flags: Uint8Array;
}
interface FullBox extends Box, IFullBox {}
interface Container extends Box {
children: Box[]
}
interface FTYP extends Box {
brand: string;
minor_version: number;
compatible_brands: string[]
}
interface META extends Container, FullBox {}
const BOX: Record<string, (box: Box) => Box> = {}
function parseFTYP(box: Box): FTYP {
const reader = new DataReader(new DataView(box.payload.buffer));
const LEN = box.payload.byteOffset + box.payload.length;
reader.SEEK(box.payload.byteOffset);
const brand = decoder.decode(reader.CHUNK(4));
const minor_version = reader.U32();
const compatible_brands: string[] = [];
while(reader.INDEX() < LEN) {
compatible_brands.push(decoder.decode(reader.CHUNK(4)))
}
return { ... box, brand, minor_version, compatible_brands }
}
BOX['ftyp'] = parseFTYP;
function getReader(box: Box): DataReader {
const reader = new DataReader(new DataView(box.payload.buffer));
reader.SEEK(box.payload.byteOffset);
return reader;
}
function parseFullBox(reader: DataReader): IFullBox {
const version = reader.U8();
const flags = reader.CHUNK(3);
return { version, flags };
}
function parseChildren(reader: DataReader, box: Box): Box[] {
const children = parseBoxes(reader.CHUNK(box.payload.length - 4))
return children;
}
BOX['meta'] = (box: Box): META => {
const reader = getReader(box);
const fullbox = parseFullBox(reader);
const children = parseChildren(reader, box);
return { ... box, ... fullbox, children};
}
BOX['dinf'] = (box: Box): Container => {
const reader = getReader(box);
const children = parseChildren(reader, box);
return { ... box, children};
}
BOX['dref'] = (box: Box): FullBox & Container & { entry_count: number } => {
const reader = getReader(box);
const fullbox = parseFullBox(reader);
const entry_count = reader.U32();
const children = parseChildren(reader, box);
return { ... box, ... fullbox, entry_count, children};
}
BOX['auxC'] = (box: Box): FullBox & { aux_type: string, sub_type: Uint8Array } => {
const reader = getReader(box);
const fullbox = parseFullBox(reader);
const aux_type = readString(reader);
const sub_type = reader.CHUNK(box.end - reader.INDEX());
return { ... box, ... fullbox, aux_type, sub_type};
}
function parseContainer(box: Box): Container {
const reader = getReader(box);
const children = parseChildren(reader, box);
return { ... box, children};
}
BOX['iprp'] = parseContainer
BOX['ipco'] = parseContainer
interface ReferenceBox extends Box {
from_item_ID: number;
reference_count: number;
to_item_ID: number[];
}
BOX['hdlr'] = (box: Box): FullBox & { handler_type: string, name: string} => {
const reader = getReader(box);
const fullbox = parseFullBox(reader);
reader.SKIP(4);
const handler_type = decoder.decode(reader.CHUNK(4));
reader.SKIP(4);
reader.SKIP(4);
reader.SKIP(4);
const name = readString(reader);
return { ... box, ... fullbox, handler_type, name};
}
BOX['iref'] = (box: Box): Container & FullBox => {
const reader = getReader(box);
const fullbox = parseFullBox(reader);
const children = parseChildren(reader, box);
children.forEach(x => {
const ref = x as ReferenceBox;
const reader = getReader(ref);
ref.from_item_ID = fullbox.version > 0 ? reader.U32() : reader.U16();
ref.reference_count = reader.U16();
ref.to_item_ID = [];
for(let i=0 ; i < ref.reference_count; i++) {
ref.to_item_ID.push(fullbox.version > 0 ? reader.U32() : reader.U16());
}
})
return { ... box, ... fullbox, children};
}
BOX['ispe'] = (box: Box): FullBox & { width: number, height: number} => {
const reader = getReader(box);
const fullbox = parseFullBox(reader);
const width = reader.U32();
const height = reader.U32();
return { ... box, ... fullbox, width, height};
}
BOX['pitm'] = (box: Box): FullBox & { item_ID: number} => {
const reader = getReader(box);
const fullbox = parseFullBox(reader);
const item_ID = fullbox.version == 0 ? reader.U16() : reader.U32();
return { ... box, ... fullbox, item_ID};
}
BOX['pixi'] = (box: Box): FullBox & { channels: number, bitPerPixel: number[]} => {
const reader = getReader(box);
const fullbox = parseFullBox(reader);
const channels = reader.U8();
const bitPerPixel: number[] = [];
for(let i=0; i< channels; i++) {
bitPerPixel.push(reader.U8())
}
return { ... box, ... fullbox, channels, bitPerPixel};
}
BOX['colr'] = (box: Box): Box & { colour_type: string, colour_primaries: number, transfer_characteristics: number, matrix_coefficients: number, full_range_flag: boolean, ICC_profile?: Uint8Array} => {
const reader = getReader(box);
const colour_type = decoder.decode(reader.CHUNK(4));
if(colour_type == 'nclx') {
const colour_primaries = reader.U16();
const transfer_characteristics = reader.U16();
const matrix_coefficients = reader.U16();
const val = reader.U8();
const full_range_flag = (val & 0x80) > 0;
return { ... box, colour_type, colour_primaries, transfer_characteristics, matrix_coefficients, full_range_flag};
}
else if ((colour_type == 'rICC') || (colour_type == 'prof'))
{
const colour_primaries = 0;
const transfer_characteristics = 0;
const matrix_coefficients = 0;
const full_range_flag = false;
const ICC_profile = reader.CHUNK(box.payload.length - 4);
return { ... box, colour_type, colour_primaries, transfer_characteristics, matrix_coefficients, full_range_flag, ICC_profile};
}
throw new Error('Uknown COLR type: '+colour_type);
}
interface ILocItemExtent { item_reference_index?: number, extent_offset: number, extent_length: number }
interface ILocItem { item_ID: number, construction_method: number, data_reference_index: number, base_offset: number, extent_count: number, extents: ILocItemExtent[]}
interface ILOC { offset_size: number, length_size: number, base_offset_size: number, index_size: number, item_count: number, items: ILocItem[]};
BOX['iloc'] = (box: Box): FullBox & ILOC => {
const reader = getReader(box);
const fullbox = parseFullBox(reader);
const { version } = fullbox;
const iloc = {} as ILOC;
let v = reader.U8();
iloc.offset_size = v >> 4;
iloc.length_size = v & 0x0F;
v = reader.U8();
iloc.base_offset_size = v >> 4;
iloc.index_size = v & 0x0F;
iloc.item_count = version < 2 ? reader.U16() : reader.U32();
iloc.items = [];
const IDX_SZ = 'U'+(8 * iloc.index_size);
const OFF_SZ = 'U'+(8 * iloc.offset_size);
const LEN_SZ = 'U'+(8 * iloc.length_size);
const BOFF_SZ = 'U'+(8 * iloc.base_offset_size);
for(let i=0; i< iloc.item_count; i++) {
const item = {} as ILocItem;
item.item_ID = version < 2 ? reader.U16() : reader.U32();
if(version == 1 || version == 2) {
v = reader.U16()
item.construction_method = v & 0x0F;
}
item.data_reference_index = reader.U16();
item.base_offset = iloc.base_offset_size > 0 ? (<any>reader)[BOFF_SZ](): -1;
item.extent_count = reader.U16();
item.extents = [];
for(let j=0;j<item.extent_count;j++) {
const extent = {} as ILocItemExtent;
if (((version == 1) || (version == 2)) && (iloc.index_size > 0)) {
extent.item_reference_index = (<any>reader)[IDX_SZ]();
}
extent.extent_offset = (<any>reader)[OFF_SZ]();
extent.extent_length = (<any>reader)[LEN_SZ]();
item.extents.push(extent);
}
iloc.items.push(item);
}
return { ... box, ... fullbox, ... iloc};
}
BOX['irot'] = (box: Box): Box & { angle: number} => {
const reader = getReader(box);
const val = reader.U8();
const angle = (val & 0x3) * 90;
return { ... box, angle};
}
interface IMPA_Assoc { essential: boolean, property_index: number}
interface IPMA_Entry {item_ID: number, association_count: number, associations: IMPA_Assoc[]}
interface IPMA extends Box, FullBox { entry_count: number, entries: IPMA_Entry[] }
BOX['ipma'] = (box: Box): IPMA => {
const reader = getReader(box);
const fullbox = parseFullBox(reader);
const entry_count = reader.U32();
const entries: IPMA_Entry[] = [];
for(let i = 0; i < entry_count; i++) {
const item_ID = fullbox.version == 0 ? reader.U16() : reader.U32();
const association_count = reader.U8();
const associations: IMPA_Assoc[] = [];
for(let j = 0; j < association_count; j++) {
const FLAG = (fullbox.flags[0] & 0x1);
const mask = FLAG ? 0x7FFF : 0x7F;
const e_mask = FLAG ? 0x8000 : 0x80;
const val = FLAG ? reader.U16() : reader.U8();
const essential = (val & e_mask) > 0;
const property_index = val & mask;
associations.push({essential, property_index})
}
entries.push({ item_ID, association_count, associations })
}
return {... box, ... fullbox, entry_count, entries}
}
interface IINF extends Container, FullBox { entry_count: number }
BOX['iinf'] = (box: Box): IINF => {
const reader = getReader(box);
const fullbox = parseFullBox(reader);
const entry_count = fullbox.version == 0 ? reader.U16() : reader.U32();
const children = parseChildren(reader, box);
return { ... box, ... fullbox, entry_count, children};
}
interface INFE extends Box, FullBox { item_ID: number, item_protection_index: number, item_type: string, item_name: string }
BOX['infe'] = (box: Box): INFE => {
const reader = getReader(box);
const fullbox = parseFullBox(reader);
if(fullbox.version < 2)
throw new Error('unable to parse infe type '+fullbox.version);
const item_ID = fullbox.version == 2 ? reader.U16(): reader.U32();
const item_protection_index = reader.U16();
const item_type = decoder.decode(reader.CHUNK(4));
const item_name = readString(reader);
return { ... box, ... fullbox, item_ID, item_protection_index, item_type, item_name};
}
const decoder = new TextDecoder();
function parseBoxes(data: Uint8Array) {
const reader = new DataReader(new DataView(data.buffer));
const LEN = data.byteOffset + data.length;
reader.SEEK(data.byteOffset);
function REMAINING() {
return LEN - reader.INDEX();
}
const boxes: Box[] = []
while(reader.INDEX() < LEN) {
const start = reader.INDEX();
let size = reader.U32();
if(REMAINING() < 4) {
if(size == 0) {
boxes.push({ start, end: LEN, boxtype: 'None', payload: new Uint8Array()})
}
break;
}
const boxtype = decoder.decode(reader.CHUNK(4));
if(size == 0) size = LEN - start;
else if(size == 1) size = reader.U64();
const payload_size = size - (reader.INDEX() - start);
const end = start + size;
let box: Box = { start, end, boxtype, payload: reader.CHUNK(payload_size)};
if(BOX[boxtype]) {
box = BOX[boxtype](box);
}
boxes.push(box)
}
return boxes;
}
const boxes = parseBoxes(new Uint8Array(img_ab));
debugger;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment