117 lines
3.6 KiB
TypeScript
117 lines
3.6 KiB
TypeScript
import { EXIF_CODES, ExifError } from "../../errors/exif-error.js";
|
|
import type { ByteOrder, Rational } from "../../types.js";
|
|
|
|
/**
|
|
* Low-level binary reader for TIFF/EXIF byte streams.
|
|
*
|
|
* Supports both big-endian and little-endian byte orders with runtime switching.
|
|
* All read operations are bounds-checked and throw ExifError on overflow.
|
|
*/
|
|
export class ByteReader {
|
|
protected byteOrder: ByteOrder;
|
|
protected view: DataView;
|
|
protected data: Uint8Array;
|
|
|
|
constructor(data: Uint8Array, byteOrder: ByteOrder = "BE") {
|
|
this.data = data;
|
|
this.byteOrder = byteOrder;
|
|
this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
}
|
|
|
|
setByteOrder(order: ByteOrder): void {
|
|
this.byteOrder = order;
|
|
}
|
|
|
|
get length(): number {
|
|
return this.data.byteLength;
|
|
}
|
|
|
|
protected assertBounds(offset: number, size: number): void {
|
|
if (offset < 0 || offset + size > this.data.byteLength) {
|
|
throw new ExifError(
|
|
`Read out of bounds: offset=${offset}, size=${size}, length=${this.data.byteLength}`,
|
|
EXIF_CODES.PARSE_ERROR
|
|
);
|
|
}
|
|
}
|
|
|
|
readUint8(offset: number): number {
|
|
this.assertBounds(offset, 1);
|
|
return this.view.getUint8(offset);
|
|
}
|
|
|
|
readUint16(offset: number): number {
|
|
this.assertBounds(offset, 2);
|
|
return this.view.getUint16(offset, this.byteOrder === "LE");
|
|
}
|
|
|
|
readUint32(offset: number): number {
|
|
this.assertBounds(offset, 4);
|
|
return this.view.getUint32(offset, this.byteOrder === "LE");
|
|
}
|
|
|
|
readInt16(offset: number): number {
|
|
this.assertBounds(offset, 2);
|
|
return this.view.getInt16(offset, this.byteOrder === "LE");
|
|
}
|
|
|
|
readInt32(offset: number): number {
|
|
this.assertBounds(offset, 4);
|
|
return this.view.getInt32(offset, this.byteOrder === "LE");
|
|
}
|
|
|
|
readFloat32(offset: number): number {
|
|
this.assertBounds(offset, 4);
|
|
return this.view.getFloat32(offset, this.byteOrder === "LE");
|
|
}
|
|
|
|
readFloat64(offset: number): number {
|
|
this.assertBounds(offset, 8);
|
|
return this.view.getFloat64(offset, this.byteOrder === "LE");
|
|
}
|
|
|
|
readBytes(offset: number, length: number): Uint8Array {
|
|
this.assertBounds(offset, length);
|
|
return this.data.slice(offset, offset + length);
|
|
}
|
|
|
|
readString(offset: number, length: number): string {
|
|
this.assertBounds(offset, length);
|
|
const bytes = this.data.slice(offset, offset + length);
|
|
let str = "";
|
|
for (let i = 0; i < bytes.length; i++) {
|
|
const b = bytes[i] as number;
|
|
if (b === 0) break; // null-terminate on first null
|
|
str += String.fromCharCode(b);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
readNullTermString(offset: number): string {
|
|
let end = offset;
|
|
while (end < this.data.byteLength && this.data[end] !== 0) {
|
|
end++;
|
|
}
|
|
return this.readString(offset, end - offset);
|
|
}
|
|
|
|
readRational(offset: number): Rational {
|
|
this.assertBounds(offset, 8);
|
|
const numerator = this.readUint32(offset);
|
|
const denominator = this.readUint32(offset + 4);
|
|
return { numerator, denominator };
|
|
}
|
|
|
|
readSRational(offset: number): Rational {
|
|
this.assertBounds(offset, 8);
|
|
const numerator = this.readInt32(offset);
|
|
const denominator = this.readInt32(offset + 4);
|
|
return { numerator, denominator };
|
|
}
|
|
|
|
slice(start: number, end: number): ByteReader {
|
|
const sliced = this.data.slice(start, end);
|
|
return new ByteReader(sliced, this.byteOrder);
|
|
}
|
|
}
|