Created
October 22, 2025 14:14
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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