// --------------------------------------------------------------------------- // Pixel pack/unpack helpers // --------------------------------------------------------------------------- /** * Pack 1-byte-per-pixel array into 1bpp. * Excess bits in the last byte are set to 0. * Size = ceil(width * height / 8). */ export function packPixels(pixels: Uint8Array, width: number, height: number): Uint8Array { const total = width * height; const packed = new Uint8Array((total + 7) >> 3); for (let i = 0; i < total; i++) { if (pixels[i]) packed[i >> 3] = (packed[i >> 3] ?? 0) | (1 << (i & 7)); } return packed; } /** * Unpack 1bpp data to 1-byte-per-pixel. Excess bits beyond width*height are ignored. */ export function unpackPixels(packed: Uint8Array, width: number, height: number): Uint8Array { const total = width * height; const pixels = new Uint8Array(total); for (let i = 0; i < total; i++) { pixels[i] = ((packed[i >> 3] ?? 0) >> (i & 7)) & 1; } return pixels; } /** Byte count of packed 1bpp image */ export function packedSize(width: number, height: number): number { return (width * height + 7) >> 3; } /** Padding needed to align `byteCount` to a 4-byte boundary */ export function pad32(byteCount: number): number { return (4 - (byteCount & 3)) & 3; } // --------------------------------------------------------------------------- // BinaryReader — little-endian cursor over an ArrayBuffer // --------------------------------------------------------------------------- export class BinaryReader { private view: DataView; private pos: number = 0; constructor(buffer: ArrayBuffer | ArrayBufferView) { this.view = buffer instanceof ArrayBuffer ? new DataView(buffer) : new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); } get offset(): number { return this.pos; } get byteLength(): number { return this.view.byteLength; } get remaining(): number { return this.view.byteLength - this.pos; } readByte(): number { this.#need(1); return this.view.getUint8(this.pos++); } readUint16(): number { this.#need(2); const v = this.view.getUint16(this.pos, true); this.pos += 2; return v; } readShort(): number { return this.readUint16(); } readInt16(): number { this.#need(2); const v = this.view.getInt16(this.pos, true); this.pos += 2; return v; } readUint24(): number { this.#need(3); const v = this.view.getUint8(this.pos) | (this.view.getUint8(this.pos+1) << 8) | (this.view.getUint8(this.pos+2) << 16); this.pos += 3; return v; } readUint32(): number { this.#need(4); const v = this.view.getUint32(this.pos, true); this.pos += 4; return v; } readUint64(): bigint { this.#need(8); const lo = BigInt(this.view.getUint32(this.pos, true)); const hi = BigInt(this.view.getUint32(this.pos+4, true)); this.pos += 8; return (hi << 32n) | lo; } readBytes(n: number): Uint8Array { this.#need(n); const s = new Uint8Array(this.view.buffer, this.pos, n); this.pos += n; return s; } readUtf8(n: number): string { return new TextDecoder().decode(this.readBytes(n)); } seek(pos: number): void { if (pos < 0 || pos > this.view.byteLength) { throw new RangeError(`seek(${pos}) out of [0, ${this.view.byteLength}]`); } this.pos = pos; } #need(n: number): void { if (this.remaining < n) throw new RangeError( `BinaryReader: need ${n} byte(s) at offset ${this.pos}, ${this.remaining} remain` ); } }