diff --git a/entries/all-boxes.ts b/entries/all-boxes.ts index 297aa468..cf94f0be 100644 --- a/entries/all-boxes.ts +++ b/entries/all-boxes.ts @@ -1,9 +1,11 @@ export * from '#/boxes/a1lx'; export * from '#/boxes/a1op'; +export * from '#/boxes/amve'; export * from '#/boxes/auxC'; export * from '#/boxes/av1C'; export * from '#/boxes/avcC'; export * from '#/boxes/btrt'; +export * from '#/boxes/cclv'; export * from '#/boxes/ccst'; export * from '#/boxes/cdef'; export * from '#/boxes/clap'; @@ -59,8 +61,10 @@ export * from '#/boxes/mehd'; export * from '#/boxes/meta'; export * from '#/boxes/mfhd'; export * from '#/boxes/mfro'; +export * from '#/boxes/mini'; export * from '#/boxes/mskC'; export * from '#/boxes/mvhd'; +export * from '#/boxes/ndwt'; export * from '#/boxes/npck'; export * from '#/boxes/nump'; export * from '#/boxes/padb'; @@ -83,6 +87,7 @@ export * from '#/boxes/qt/keys'; export * from '#/boxes/qt/prof'; export * from '#/boxes/qt/tapt'; export * from '#/boxes/qt/wave'; +export * from '#/boxes/reve'; export * from '#/boxes/rtp'; export * from '#/boxes/saio'; export * from '#/boxes/saiz'; diff --git a/src/bitstream.ts b/src/bitstream.ts new file mode 100644 index 00000000..2ba1e019 --- /dev/null +++ b/src/bitstream.ts @@ -0,0 +1,105 @@ +import type { MultiBufferStream } from '#/buffer'; + +/** + * BitStream is a class that acts as a MultiBufferStream wrapper for parsing individual bits + */ +export class BitStream { + // The last byte read from the stream. + last_byte: number; + // If not zero, number of most significant bits already read in last_byte. + num_bits_read_in_last_byte: number; + + constructor(readonly stream: MultiBufferStream) { + this.last_byte = 0; + this.num_bits_read_in_last_byte = 0; + } + + /** + * Reads bits with or without byte alignment and potentially across multiple bytes. + * The bits are read in the earliest byte first, and most significant bits in a byte first. + * @param num_bits Number of bits to read. + * @return An unsigned integer whose binary representation is the big-endian read bits. + */ + read(num_bits: number) { + if (num_bits > 32) { + throw new Error('BitStream.read: Unsupported number of bits.'); + } + let remaining_num_bits = num_bits; + let value = 0; + while (remaining_num_bits !== 0) { + if (this.num_bits_read_in_last_byte === 0) { + this.last_byte = this.stream.readUint8(); + } + // Number of bits among remaining_num_bits that can be read in the last_byte. + const num_read_bits = Math.min(8 - this.num_bits_read_in_last_byte, remaining_num_bits); + // Read the most significant bits first. + const read_bits = + (this.last_byte >> (8 - this.num_bits_read_in_last_byte - num_read_bits)) & + ((1 << num_read_bits) - 1); + value = (value << num_read_bits) | read_bits; + this.num_bits_read_in_last_byte = (this.num_bits_read_in_last_byte + num_read_bits) % 8; + remaining_num_bits -= num_read_bits; + } + return value; + } + + /** + * Reads one bit. + * @return The read bit. + */ + bool() { + return this.read(1) === 1; + } + + /** + * Reads a potentially unaligned four-character code. + * @return The read big-endian 4CC. + */ + four_cc() { + return String.fromCharCode(this.read(8), this.read(8), this.read(8), this.read(8)); + } + + /** + * Reads up to seven bits until byte alignment. The read bits must be zeros. + */ + pad_with_zeros() { + if ( + this.num_bits_read_in_last_byte !== 0 && + this.read(8 - this.num_bits_read_in_last_byte) !== 0 + ) { + throw new Error('BitStream.padding: Bits were not all set to zero.'); + } + } + + /** + * Reads a potentially unaligned 8-bit unsigned int from the underlying MultiBufferStream. + * @return The read big-endian number. + */ + readUint8() { + return this.read(8); + } + + /** + * Reads a potentially unaligned 16-bit unsigned int from the underlying MultiBufferStream. + * @return The read big-endian number. + */ + readUint16() { + return this.read(16); + } + + /** + * Reads a potentially unaligned 32-bit unsigned int from the underlying MultiBufferStream. + * @return The read big-endian number. + */ + readUint32() { + return this.read(32); + } + + /** + * Reads a potentially unaligned 32-bit signed int from the underlying MultiBufferStream. + * @return The read big-endian number. + */ + readInt32() { + return this.read(1) ? this.read(31) - Math.pow(2, 31) : this.read(31); + } +} diff --git a/src/boxes/amve.ts b/src/boxes/amve.ts new file mode 100644 index 00000000..731c0e90 --- /dev/null +++ b/src/boxes/amve.ts @@ -0,0 +1,18 @@ +import { Box } from '#/box'; +import type { MultiBufferStream } from '#/buffer'; +import type { BitStream } from '#/bitstream'; + +export class amveBox extends Box { + static override readonly fourcc = 'amve' as const; + box_name = 'AmbientViewingEnvironmentBox' as const; + + ambient_illuminance: number; + ambient_light_x: number; + ambient_light_y: number; + + parse(stream: MultiBufferStream | BitStream) { + this.ambient_illuminance = stream.readUint32(); + this.ambient_light_x = stream.readUint16(); + this.ambient_light_y = stream.readUint16(); + } +} diff --git a/src/boxes/cclv.ts b/src/boxes/cclv.ts new file mode 100644 index 00000000..b7478169 --- /dev/null +++ b/src/boxes/cclv.ts @@ -0,0 +1,40 @@ +import { Box } from '#/box'; +import type { MultiBufferStream } from '#/buffer'; +import type { BitStream } from '#/bitstream'; + +export class cclvBox extends Box { + static override readonly fourcc = 'cclv' as const; + box_name = 'ContentColourVolumeBox' as const; + + ccv_primaries_x: Array; + ccv_primaries_y: Array; + ccv_min_luminance_value: number; + ccv_max_luminance_value: number; + ccv_avg_luminance_value: number; + + parse(stream: MultiBufferStream | BitStream) { + const flags = stream.readUint8(); + const ccv_primaries_present_flag = flags & 0x10; + const ccv_min_luminance_value_present_flag = flags & 0x08; + const ccv_max_luminance_value_present_flag = flags & 0x04; + const ccv_avg_luminance_value_present_flag = flags & 0x02; + + if (ccv_primaries_present_flag) { + this.ccv_primaries_x = new Array(3); + this.ccv_primaries_y = new Array(3); + for (let c = 0; c < 3; c++) { + this.ccv_primaries_x[c] = stream.readInt32(); + this.ccv_primaries_y[c] = stream.readInt32(); + } + } + if (ccv_min_luminance_value_present_flag) { + this.ccv_min_luminance_value = stream.readUint32(); + } + if (ccv_max_luminance_value_present_flag) { + this.ccv_max_luminance_value = stream.readUint32(); + } + if (ccv_avg_luminance_value_present_flag) { + this.ccv_avg_luminance_value = stream.readUint32(); + } + } +} diff --git a/src/boxes/clli.ts b/src/boxes/clli.ts index e319efcb..e6f40b24 100644 --- a/src/boxes/clli.ts +++ b/src/boxes/clli.ts @@ -1,5 +1,6 @@ import { Box } from '#/box'; import type { MultiBufferStream } from '#/buffer'; +import type { BitStream } from '#/bitstream'; export class clliBox extends Box { static override readonly fourcc = 'clli' as const; @@ -8,7 +9,7 @@ export class clliBox extends Box { max_content_light_level: number; max_pic_average_light_level: number; - parse(stream: MultiBufferStream) { + parse(stream: MultiBufferStream | BitStream) { this.max_content_light_level = stream.readUint16(); this.max_pic_average_light_level = stream.readUint16(); } diff --git a/src/boxes/mdcv.ts b/src/boxes/mdcv.ts index 40815e07..e89d192c 100644 --- a/src/boxes/mdcv.ts +++ b/src/boxes/mdcv.ts @@ -1,6 +1,7 @@ import { Box } from '#/box'; import type { MultiBufferStream } from '#/buffer'; import { ColorPoint } from './displays/colorPoint'; +import type { BitStream } from '#/bitstream'; export class mdcvBox extends Box { static override readonly fourcc = 'mdcv' as const; @@ -11,7 +12,7 @@ export class mdcvBox extends Box { max_display_mastering_luminance: number; min_display_mastering_luminance: number; - parse(stream: MultiBufferStream) { + parse(stream: MultiBufferStream | BitStream) { this.display_primaries = []; this.display_primaries[0] = new ColorPoint(stream.readUint16(), stream.readUint16()); this.display_primaries[1] = new ColorPoint(stream.readUint16(), stream.readUint16()); diff --git a/src/boxes/mini.ts b/src/boxes/mini.ts new file mode 100644 index 00000000..3eada261 --- /dev/null +++ b/src/boxes/mini.ts @@ -0,0 +1,310 @@ +import { Box } from '#/box'; +import { BitStream } from '#/bitstream'; +import type { MultiBufferStream } from '#/buffer'; +import { amveBox } from './amve'; +import { cclvBox } from './cclv'; +import { clliBox } from './clli'; +import { mdcvBox } from './mdcv'; +import { ndwtBox } from './ndwt'; +import { reveBox } from './reve'; + +export class miniBox extends Box { + static override readonly fourcc = 'mini' as const; + box_name = 'MinimizedImageBox' as const; + + version: number; + + explicit_codec_types_flag: boolean; + float_flag: boolean; + full_range_flag: boolean; + alpha_flag: boolean; + explicit_cicp_flag: boolean; + hdr_flag: boolean; + icc_flag: boolean; + exif_flag: boolean; + xmp_flag: boolean; + + chroma_subsampling: number; + orientation_minus1: number; + + large_dimensions_flag: boolean; + width_minus1: number; + height_minus1: number; + + chroma_is_horizontally_centered: boolean; + chroma_is_vertically_centered: boolean; + + bit_depth: number; + + alpha_is_premultiplied: boolean; + + colour_primaries: number; + transfer_characteristics: number; + matrix_coefficients: number; + + infe_type: string; + codec_config_type: string; + + gainmap_flag: boolean; + gainmap_width_minus1: number; + gainmap_height_minus1: number; + gainmap_matrix_coefficients: number; + gainmap_full_range_flag: boolean; + gainmap_chroma_subsampling: number; + gainmap_chroma_is_horizontally_centered: boolean; + gainmap_chroma_is_vertically_centered: boolean; + gainmap_float_flag: boolean; + gainmap_bit_depth: number; + tmap_icc_flag: boolean; + tmap_explicit_cicp_flag: boolean; + tmap_colour_primaries: number; + tmap_transfer_characteristics: number; + tmap_matrix_coefficients: number; + tmap_full_range_flag: boolean; + + large_metadata_flag: boolean; + large_codec_config_flag: boolean; + large_item_data_flag: boolean; + + icc_data_size_minus1: number; + tmap_icc_data_size_minus1: number; + gainmap_metadata_size: number; + gainmap_item_data_size: number; + gainmap_item_codec_config_size: number; + main_item_codec_config_size: number; + main_item_data_size_minus1: number; + alpha_item_data_size: number; + alpha_item_codec_config_size: number; + exif_xmp_compressed_flag: boolean; + exif_data_size_minus1: number; + xmp_data_size_minus1: number; + + parse(stream: MultiBufferStream) { + const bits = new BitStream(stream); + this.version = bits.read(2); + + this.explicit_codec_types_flag = bits.bool(); + this.float_flag = bits.bool(); + this.full_range_flag = bits.bool(); + this.alpha_flag = bits.bool(); + this.explicit_cicp_flag = bits.bool(); + this.hdr_flag = bits.bool(); + this.icc_flag = bits.bool(); + this.exif_flag = bits.bool(); + this.xmp_flag = bits.bool(); + + this.chroma_subsampling = bits.read(2); + this.orientation_minus1 = bits.read(3); + + this.large_dimensions_flag = bits.bool(); + this.width_minus1 = bits.read(this.large_dimensions_flag ? 15 : 7); + this.height_minus1 = bits.read(this.large_dimensions_flag ? 15 : 7); + + // Pixel information + if (this.chroma_subsampling === 1 || this.chroma_subsampling === 2) { + this.chroma_is_horizontally_centered = bits.bool(); + } + if (this.chroma_subsampling === 1) { + this.chroma_is_vertically_centered = bits.bool(); + } + if (this.float_flag) { + this.bit_depth = 1 << (bits.read(2) + 4); + } else { + this.bit_depth = bits.bool() ? bits.read(3) + 9 : 8; + } + if (this.alpha_flag) { + this.alpha_is_premultiplied = bits.bool(); + } + // Colour properties + if (this.explicit_cicp_flag) { + this.colour_primaries = bits.read(8); + this.transfer_characteristics = bits.read(8); + this.matrix_coefficients = bits.read(8); + } else { + this.colour_primaries = this.icc_flag ? 2 : 1; + this.transfer_characteristics = this.icc_flag ? 2 : 13; + this.matrix_coefficients = this.chroma_subsampling === 0 ? 2 : 6; + } + if (this.explicit_codec_types_flag) { + this.infe_type = bits.four_cc(); + this.codec_config_type = bits.four_cc(); + } else { + this.infe_type = '[deduced from ftyp minor_version]'; + this.codec_config_type = '[deduced from ftyp minor_version]'; + } + + // High Dynamic Range properties + if (this.hdr_flag) { + this.gainmap_flag = bits.bool(); + if (this.gainmap_flag) { + const gainmap_dimension_same_as_main_item_flag = bits.bool(); + if (gainmap_dimension_same_as_main_item_flag) { + this.gainmap_width_minus1 = this.width_minus1; + this.gainmap_height_minus1 = this.height_minus1; + } else { + this.gainmap_width_minus1 = bits.read(this.large_dimensions_flag ? 15 : 7); + this.gainmap_height_minus1 = bits.read(this.large_dimensions_flag ? 15 : 7); + } + this.gainmap_matrix_coefficients = bits.read(8); + this.gainmap_full_range_flag = bits.bool(); + this.gainmap_chroma_subsampling = bits.read(2); + if (this.gainmap_chroma_subsampling === 1 || this.gainmap_chroma_subsampling === 2) { + this.gainmap_chroma_is_horizontally_centered = bits.bool(); + } + if (this.gainmap_chroma_subsampling === 1) { + this.gainmap_chroma_is_vertically_centered = bits.bool(); + } + this.gainmap_float_flag = bits.bool(); + + if (this.gainmap_float_flag) { + this.gainmap_bit_depth = 1 << (bits.read(2) + 4); + } else { + this.gainmap_bit_depth = bits.bool() ? bits.read(3) + 9 : 8; + } + this.tmap_icc_flag = bits.bool(); + this.tmap_explicit_cicp_flag = bits.bool(); + if (this.tmap_explicit_cicp_flag) { + this.tmap_colour_primaries = bits.read(8); + this.tmap_transfer_characteristics = bits.read(8); + this.tmap_matrix_coefficients = bits.read(8); + this.tmap_full_range_flag = bits.bool(); + } else { + this.tmap_colour_primaries = 1; + this.tmap_transfer_characteristics = 13; + this.tmap_matrix_coefficients = 6; + this.tmap_full_range_flag = true; + } + } + + // These are only the inner syntaxes, not the boxes themselves. + // Still create child boxes for prettier display. + const parse_hdr_boxes = (parent: Box, stream: BitStream) => { + const clli_flag = stream.bool(); + const mdcv_flag = stream.bool(); + const cclv_flag = stream.bool(); + const amve_flag = stream.bool(); + const reve_flag = stream.bool(); + const ndwt_flag = stream.bool(); + if (clli_flag) { + const clli = new clliBox(); + clli.parse(stream); + parent.addBox(clli); + } + if (mdcv_flag) { + const mdcv = new mdcvBox(); + mdcv.parse(stream); + parent.addBox(mdcv); + } + if (cclv_flag) { + const cclv = new cclvBox(); + cclv.parse(stream); + parent.addBox(cclv); + } + if (amve_flag) { + const amve = new amveBox(); + amve.parse(stream); + parent.addBox(amve); + } + if (reve_flag) { + const reve = new reveBox(); + reve.parse(stream); + parent.addBox(reve); + } + if (ndwt_flag) { + const ndwt = new ndwtBox(); + ndwt.parse(stream); + parent.addBox(ndwt); + } + }; + parse_hdr_boxes(this, bits); + + if (this.gainmap_flag) { + const tmap_placeholder = new Box(); + tmap_placeholder.box_name = 'tmap'; + parse_hdr_boxes(tmap_placeholder, bits); + if (tmap_placeholder.boxes) { + this.addBox(tmap_placeholder); + } + } + } + + // Chunk sizes + if (this.icc_flag || this.exif_flag || this.xmp_flag || (this.hdr_flag && this.gainmap_flag)) { + this.large_metadata_flag = bits.bool(); + } + this.large_codec_config_flag = bits.bool(); + this.large_item_data_flag = bits.bool(); + if (this.icc_flag) { + this.icc_data_size_minus1 = bits.read(this.large_metadata_flag ? 20 : 10); + } + if (this.hdr_flag && this.gainmap_flag && this.tmap_icc_flag) { + this.tmap_icc_data_size_minus1 = bits.read(this.large_metadata_flag ? 20 : 10); + } + if (this.hdr_flag && this.gainmap_flag) { + this.gainmap_metadata_size = bits.read(this.large_metadata_flag ? 20 : 10); + } + if (this.hdr_flag && this.gainmap_flag) { + this.gainmap_item_data_size = bits.read(this.large_item_data_flag ? 28 : 15); + } + if (this.hdr_flag && this.gainmap_flag && this.gainmap_item_data_size > 0) { + this.gainmap_item_codec_config_size = bits.read(this.large_codec_config_flag ? 12 : 3); + } + this.main_item_codec_config_size = bits.read(this.large_codec_config_flag ? 12 : 3); + this.main_item_data_size_minus1 = bits.read(this.large_item_data_flag ? 28 : 15); + if (this.alpha_flag) { + this.alpha_item_data_size = bits.read(this.large_item_data_flag ? 28 : 15); + } + if (this.alpha_flag && this.alpha_item_data_size > 0) { + this.alpha_item_codec_config_size = bits.read(this.large_codec_config_flag ? 12 : 3); + } + if (this.exif_flag || this.xmp_flag) { + this.exif_xmp_compressed_flag = bits.bool(); + } + if (this.exif_flag) { + this.exif_data_size_minus1 = bits.read(this.large_metadata_flag ? 20 : 10); + } + if (this.xmp_flag) { + this.xmp_data_size_minus1 = bits.read(this.large_metadata_flag ? 20 : 10); + } + bits.pad_with_zeros(); // bit padding till byte alignment + + // The following chunks are raw bytes. No need to parse them as explicit fields. + + // unsigned int(8) main_item_codec_config[main_item_codec_config_size]; + // unsigned int(8) alpha_item_codec_config[]; // non-parsed variable + // if (alpha_flag && alpha_item_data_size > 0) { + // if(alpha_item_codec_config_size == 0) { + // alpha_item_codec_config_size = main_item_codec_config_size; + // alpha_item_codec_config = main_item_codec_config; + // } else { + // unsigned int(8) alpha_item_explicit_codec_config[alpha_item_codec_config_size]; + // alpha_item_codec_config = alpha_item_explicit_codec_config; + // } + // } + // unsigned int(8) gainmap_item_codec_config[]; // non-parsed variable + // if (hdr_flag && gainmap_flag && gainmap_item_data_size > 0) { + // if (gainmap_item_codec_config_size == 0) { + // gainmap_item_codec_config_size = main_item_codec_config_size; + // gainmap_item_codec_config = main_item_codec_config; + // } else { + // unsigned int(8) gainmap_item_explicit_codec_config[gainmap_item_codec_config_size]; + // gainmap_item_codec_config = gainmap_item_explicit_codec_config; + // } + // } + // if (icc_flag) + // unsigned int(8) icc_data[icc_data_size_minus1 + 1]; + // if (hdr_flag && gainmap_flag && tmap_icc_flag) + // unsigned int(8) tmap_icc_data[tmap_icc_data_size_minus1 + 1]; + // if (hdr_flag && gainmap_flag && gainmap_metadata_size > 0) + // unsigned int(8) gainmap_metadata[gainmap_metadata_size]; + // if (alpha_flag && alpha_item_data_size > 0) + // unsigned int(8) alpha_item_data[alpha_item_data_size]; + // if (hdr_flag && gainmap_flag && gainmap_item_data_size > 0) + // unsigned int(8) gainmap_item_data[gainmap_item_data_size]; + // unsigned int(8) main_item_data[main_item_data_size_minus1 + 1]; + // if (exif_flag) + // unsigned int(8) exif_data[exif_data_size_minus1 + 1]; + // if (xmp_flag) + // unsigned int(8) xmp_data[xmp_data_size_minus1 + 1]; + } +} diff --git a/src/boxes/ndwt.ts b/src/boxes/ndwt.ts new file mode 100644 index 00000000..3b5f4136 --- /dev/null +++ b/src/boxes/ndwt.ts @@ -0,0 +1,14 @@ +import { Box } from '#/box'; +import type { MultiBufferStream } from '#/buffer'; +import type { BitStream } from '#/bitstream'; + +export class ndwtBox extends Box { + static override readonly fourcc = 'ndwt' as const; + box_name = 'NominalDiffuseWhite' as const; + + diffuse_white_luminance: number; + + parse(stream: MultiBufferStream | BitStream) { + this.diffuse_white_luminance = stream.readUint32(); + } +} diff --git a/src/boxes/pixi.ts b/src/boxes/pixi.ts index a3b8ab70..3dce0fd6 100644 --- a/src/boxes/pixi.ts +++ b/src/boxes/pixi.ts @@ -1,19 +1,50 @@ -import { FullBox } from '#/box'; +import { Box, FullBox } from '#/box'; +import { BitStream } from '#/bitstream'; import type { MultiBufferStream } from '#/buffer'; +class pixiBoxChannel extends Box { + depth: number; + channel_idc: number; + reserved: number; + component_format: number; + subsampling_type: number; + subsampling_location: number; + channel_label: string; +} + export class pixiBox extends FullBox { static override readonly fourcc = 'pixi' as const; box_name = 'PixelInformationProperty' as const; num_channels: number; - bits_per_channels: Array; parse(stream: MultiBufferStream) { this.parseFullHeader(stream); this.num_channels = stream.readUint8(); - this.bits_per_channels = []; + const channels = []; + for (let i = 0; i < this.num_channels; i++) { + channels[i] = new pixiBoxChannel(); + channels[i].depth = stream.readUint8(); + } + if (this.flags & 0x1) { + const bits = new BitStream(stream); + for (let i = 0; i < this.num_channels; i++) { + channels[i].channel_idc = bits.read(3); + channels[i].reserved = bits.read(1); + channels[i].component_format = bits.read(2); + const subsampling_flag = bits.bool(); + const channel_label_flag = bits.bool(); + if (subsampling_flag) { + channels[i].subsampling_type = bits.read(4); + channels[i].subsampling_location = bits.read(4); + } + if (channel_label_flag) { + channels[i].channel_label = stream.readCString(); + } + } + } for (let i = 0; i < this.num_channels; i++) { - this.bits_per_channels[i] = stream.readUint8(); + this.addEntry(channels[i]); } } } diff --git a/src/boxes/reve.ts b/src/boxes/reve.ts new file mode 100644 index 00000000..1d00a75c --- /dev/null +++ b/src/boxes/reve.ts @@ -0,0 +1,24 @@ +import { Box } from '#/box'; +import type { MultiBufferStream } from '#/buffer'; +import type { BitStream } from '#/bitstream'; + +export class reveBox extends Box { + static override readonly fourcc = 'reve' as const; + box_name = 'ReferenceViewingEnvironment' as const; + + surround_luminance: number; + surround_light_x: number; + surround_light_y: number; + periphery_luminance: number; + periphery_light_x: number; + periphery_light_y: number; + + parse(stream: MultiBufferStream | BitStream) { + this.surround_luminance = stream.readUint32(); + this.surround_light_x = stream.readUint16(); + this.surround_light_y = stream.readUint16(); + this.periphery_luminance = stream.readUint32(); + this.periphery_light_x = stream.readUint16(); + this.periphery_light_y = stream.readUint16(); + } +}