use oxideav_core::{Error, Result};
#[derive(Clone, Debug)]
pub struct BitmapInfoHeader {
pub width: u32,
pub height: u32,
pub planes: u16,
pub bit_count: u16,
pub compression: [u8; 4],
pub size_image: u32,
pub x_pels_per_meter: i32,
pub y_pels_per_meter: i32,
pub clr_used: u32,
pub clr_important: u32,
pub extradata: Vec<u8>,
pub top_down: bool,
}
pub fn parse_bitmap_info_header(buf: &[u8]) -> Result<BitmapInfoHeader> {
if buf.len() < 40 {
return Err(Error::invalid("AVI: BITMAPINFOHEADER too short"));
}
let _bi_size = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
let width = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as i32 as u32;
let height_signed = i32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
let top_down = height_signed.is_negative();
let height = height_signed.unsigned_abs();
let planes = u16::from_le_bytes([buf[12], buf[13]]);
let bit_count = u16::from_le_bytes([buf[14], buf[15]]);
let mut compression = [0u8; 4];
compression.copy_from_slice(&buf[16..20]);
let size_image = u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]);
let x_pels_per_meter = i32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]);
let y_pels_per_meter = i32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]);
let clr_used = u32::from_le_bytes([buf[32], buf[33], buf[34], buf[35]]);
let clr_important = u32::from_le_bytes([buf[36], buf[37], buf[38], buf[39]]);
let extradata = if buf.len() > 40 {
buf[40..].to_vec()
} else {
Vec::new()
};
Ok(BitmapInfoHeader {
width,
height,
planes,
bit_count,
compression,
size_image,
x_pels_per_meter,
y_pels_per_meter,
clr_used,
clr_important,
extradata,
top_down,
})
}
pub const BI_BITFIELDS: [u8; 4] = [3, 0, 0, 0];
pub fn parse_bitfields_masks(extradata: &[u8]) -> Option<(u32, u32, u32)> {
if extradata.len() < 12 {
return None;
}
let r = u32::from_le_bytes([extradata[0], extradata[1], extradata[2], extradata[3]]);
let g = u32::from_le_bytes([extradata[4], extradata[5], extradata[6], extradata[7]]);
let b = u32::from_le_bytes([extradata[8], extradata[9], extradata[10], extradata[11]]);
Some((r, g, b))
}
pub fn write_bitmap_info_header(
width: u32,
height: u32,
compression: [u8; 4],
bit_count: u16,
extradata: &[u8],
) -> Vec<u8> {
write_bitmap_info_header_oriented(width, height, compression, bit_count, extradata, false)
}
pub fn write_bitmap_info_header_oriented(
width: u32,
height: u32,
compression: [u8; 4],
bit_count: u16,
extradata: &[u8],
top_down: bool,
) -> Vec<u8> {
let bi_size = 40u32 + extradata.len() as u32;
let mut out = Vec::with_capacity(bi_size as usize);
out.extend_from_slice(&bi_size.to_le_bytes());
out.extend_from_slice(&width.to_le_bytes());
let height_field: i32 = if top_down {
-(height as i32)
} else {
height as i32
};
out.extend_from_slice(&height_field.to_le_bytes());
out.extend_from_slice(&1u16.to_le_bytes()); out.extend_from_slice(&bit_count.to_le_bytes());
out.extend_from_slice(&compression);
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&0i32.to_le_bytes()); out.extend_from_slice(&0i32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(extradata);
out
}
#[derive(Clone, Debug)]
pub struct WaveFormatEx {
pub format_tag: u16,
pub channels: u16,
pub samples_per_sec: u32,
pub avg_bytes_per_sec: u32,
pub block_align: u16,
pub bits_per_sample: u16,
pub extradata: Vec<u8>,
}
pub fn parse_waveformatex(buf: &[u8]) -> Result<WaveFormatEx> {
if buf.len() < 14 {
return Err(Error::invalid("AVI: WAVEFORMAT(EX) too short"));
}
let format_tag = u16::from_le_bytes([buf[0], buf[1]]);
let channels = u16::from_le_bytes([buf[2], buf[3]]);
let samples_per_sec = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
let avg_bytes_per_sec = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
let block_align = u16::from_le_bytes([buf[12], buf[13]]);
let bits_per_sample = if buf.len() >= 16 {
u16::from_le_bytes([buf[14], buf[15]])
} else {
0
};
let extradata = if buf.len() >= 18 {
let cb_size = u16::from_le_bytes([buf[16], buf[17]]) as usize;
if 18 + cb_size <= buf.len() {
buf[18..18 + cb_size].to_vec()
} else {
buf[18..].to_vec()
}
} else {
Vec::new()
};
Ok(WaveFormatEx {
format_tag,
channels,
samples_per_sec,
avg_bytes_per_sec,
block_align,
bits_per_sample,
extradata,
})
}
#[allow(clippy::too_many_arguments)]
pub fn write_waveformatex(
format_tag: u16,
channels: u16,
samples_per_sec: u32,
avg_bytes_per_sec: u32,
block_align: u16,
bits_per_sample: u16,
extradata: &[u8],
) -> Vec<u8> {
let mut out = Vec::with_capacity(18 + extradata.len());
out.extend_from_slice(&format_tag.to_le_bytes());
out.extend_from_slice(&channels.to_le_bytes());
out.extend_from_slice(&samples_per_sec.to_le_bytes());
out.extend_from_slice(&avg_bytes_per_sec.to_le_bytes());
out.extend_from_slice(&block_align.to_le_bytes());
out.extend_from_slice(&bits_per_sample.to_le_bytes());
out.extend_from_slice(&(extradata.len() as u16).to_le_bytes());
out.extend_from_slice(extradata);
out
}
pub const WAVE_FORMAT_EXTENSIBLE: u16 = 0xFFFE;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Guid(pub [u8; 16]);
impl Guid {
pub const fn from_components(data1: u32, data2: u16, data3: u16, data4: [u8; 8]) -> Self {
let d1 = data1.to_le_bytes();
let d2 = data2.to_le_bytes();
let d3 = data3.to_le_bytes();
let bytes = [
d1[0], d1[1], d1[2], d1[3], d2[0], d2[1], d3[0], d3[1], data4[0], data4[1], data4[2],
data4[3], data4[4], data4[5], data4[6], data4[7],
];
Self(bytes)
}
pub fn from_bytes(buf: &[u8]) -> Option<Self> {
if buf.len() < 16 {
return None;
}
let mut out = [0u8; 16];
out.copy_from_slice(&buf[..16]);
Some(Self(out))
}
pub fn display(&self) -> String {
let d1 = u32::from_le_bytes([self.0[0], self.0[1], self.0[2], self.0[3]]);
let d2 = u16::from_le_bytes([self.0[4], self.0[5]]);
let d3 = u16::from_le_bytes([self.0[6], self.0[7]]);
format!(
"{d1:08x}-{d2:04x}-{d3:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
self.0[8],
self.0[9],
self.0[10],
self.0[11],
self.0[12],
self.0[13],
self.0[14],
self.0[15],
)
}
pub fn is_ksdataformat_base(&self) -> bool {
const KS_TAIL: [u8; 12] = [
0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71, ];
self.0[4..16] == KS_TAIL
}
pub fn ksdataformat_tag(&self) -> Option<u16> {
if !self.is_ksdataformat_base() {
return None;
}
let d1 = u32::from_le_bytes([self.0[0], self.0[1], self.0[2], self.0[3]]);
if d1 > u16::MAX as u32 {
return None;
}
Some(d1 as u16)
}
}
pub const KSDATAFORMAT_SUBTYPE_PCM: Guid = Guid::from_components(
0x0000_0001,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const KSDATAFORMAT_SUBTYPE_IEEE_FLOAT: Guid = Guid::from_components(
0x0000_0003,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const KSDATAFORMAT_SUBTYPE_DRM: Guid = Guid::from_components(
0x0000_0009,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const KSDATAFORMAT_SUBTYPE_ALAW: Guid = Guid::from_components(
0x0000_0006,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const KSDATAFORMAT_SUBTYPE_MULAW: Guid = Guid::from_components(
0x0000_0007,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const KSDATAFORMAT_SUBTYPE_ADPCM: Guid = Guid::from_components(
0x0000_0002,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
pub const KSDATAFORMAT_SUBTYPE_MPEG: Guid = Guid::from_components(
0x0000_0050,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
#[derive(Clone, Debug)]
pub struct WaveFormatExtensible {
pub wfx: WaveFormatEx,
pub valid_bits_per_sample: u16,
pub channel_mask: u32,
pub subformat: Guid,
}
pub fn parse_waveformatextensible(buf: &[u8]) -> Result<WaveFormatExtensible> {
let mut wfx = parse_waveformatex(buf)?;
if wfx.format_tag != WAVE_FORMAT_EXTENSIBLE {
return Err(Error::invalid(format!(
"AVI: WAVEFORMATEXTENSIBLE requires wFormatTag = 0x{:04X}, got 0x{:04X}",
WAVE_FORMAT_EXTENSIBLE, wfx.format_tag
)));
}
if wfx.extradata.len() < 22 {
return Err(Error::invalid(format!(
"AVI: WAVEFORMATEXTENSIBLE requires >= 22 bytes of cbSize extension, got {}",
wfx.extradata.len()
)));
}
let valid_bits_per_sample = u16::from_le_bytes([wfx.extradata[0], wfx.extradata[1]]);
let channel_mask = u32::from_le_bytes([
wfx.extradata[2],
wfx.extradata[3],
wfx.extradata[4],
wfx.extradata[5],
]);
let subformat = Guid::from_bytes(&wfx.extradata[6..22]).expect("checked >= 22 bytes above");
wfx.extradata = wfx.extradata[22..].to_vec();
Ok(WaveFormatExtensible {
wfx,
valid_bits_per_sample,
channel_mask,
subformat,
})
}
#[allow(clippy::too_many_arguments)]
pub fn write_waveformatextensible(
channels: u16,
samples_per_sec: u32,
avg_bytes_per_sec: u32,
block_align: u16,
container_bits: u16,
valid_bps: u16,
channel_mask: u32,
subformat: &Guid,
) -> Vec<u8> {
let mut extension = Vec::with_capacity(22);
extension.extend_from_slice(&valid_bps.to_le_bytes());
extension.extend_from_slice(&channel_mask.to_le_bytes());
extension.extend_from_slice(&subformat.0);
write_waveformatex(
WAVE_FORMAT_EXTENSIBLE,
channels,
samples_per_sec,
avg_bytes_per_sec,
block_align,
container_bits,
&extension,
)
}
pub fn subformat_codec_hint(subformat: &Guid, bits_per_sample: u16) -> Option<&'static str> {
if *subformat == KSDATAFORMAT_SUBTYPE_PCM {
return Some(match bits_per_sample {
8 => "pcm_u8",
24 => "pcm_s24le",
32 => "pcm_s32le",
_ => "pcm_s16le",
});
}
if *subformat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT {
return Some(match bits_per_sample {
64 => "pcm_f64le",
_ => "pcm_f32le",
});
}
if *subformat == KSDATAFORMAT_SUBTYPE_ALAW {
return Some("pcm_alaw");
}
if *subformat == KSDATAFORMAT_SUBTYPE_MULAW {
return Some("pcm_mulaw");
}
if *subformat == KSDATAFORMAT_SUBTYPE_ADPCM {
return Some("adpcm_ms");
}
if *subformat == KSDATAFORMAT_SUBTYPE_MPEG {
return Some("mp2");
}
if *subformat == KSDATAFORMAT_SUBTYPE_DRM {
return Some("drm");
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bmih_roundtrip() {
let bytes = write_bitmap_info_header(320, 240, *b"MJPG", 24, &[0xAA, 0xBB]);
assert_eq!(bytes.len(), 42);
let h = parse_bitmap_info_header(&bytes).unwrap();
assert_eq!(h.width, 320);
assert_eq!(h.height, 240);
assert_eq!(&h.compression, b"MJPG");
assert_eq!(h.bit_count, 24);
assert_eq!(h.extradata, vec![0xAA, 0xBB]);
assert!(!h.top_down);
}
#[test]
fn bmih_roundtrip_top_down() {
let bytes = write_bitmap_info_header_oriented(320, 240, [0, 0, 0, 0], 24, &[], true);
let h = parse_bitmap_info_header(&bytes).unwrap();
assert_eq!(h.height, 240, "abs(biHeight) reported as positive pixels");
assert!(h.top_down, "negative biHeight ⇒ top_down flag set");
let on_wire = i32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
assert_eq!(on_wire, -240);
}
#[test]
fn bitfields_masks_rgb565() {
let mut ext = Vec::new();
ext.extend_from_slice(&0x0000_F800u32.to_le_bytes()); ext.extend_from_slice(&0x0000_07E0u32.to_le_bytes()); ext.extend_from_slice(&0x0000_001Fu32.to_le_bytes()); let masks = parse_bitfields_masks(&ext).unwrap();
assert_eq!(masks, (0xF800, 0x07E0, 0x001F));
}
#[test]
fn bitfields_masks_too_short_returns_none() {
assert!(parse_bitfields_masks(&[1u8, 2, 3]).is_none());
assert!(parse_bitfields_masks(&[]).is_none());
}
#[test]
fn wfx_roundtrip() {
let bytes = write_waveformatex(1, 2, 48_000, 192_000, 4, 16, &[]);
assert_eq!(bytes.len(), 18);
let h = parse_waveformatex(&bytes).unwrap();
assert_eq!(h.format_tag, 1);
assert_eq!(h.channels, 2);
assert_eq!(h.samples_per_sec, 48_000);
assert_eq!(h.block_align, 4);
assert_eq!(h.bits_per_sample, 16);
}
#[test]
fn guid_display_canonical_form() {
let s = KSDATAFORMAT_SUBTYPE_PCM.display();
assert_eq!(s, "00000001-0000-0010-8000-00aa00389b71");
}
#[test]
fn guid_ksdataformat_base_pattern_and_tag_recovery() {
for (guid, expected_tag) in [
(KSDATAFORMAT_SUBTYPE_PCM, 0x0001u16),
(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 0x0003),
(KSDATAFORMAT_SUBTYPE_DRM, 0x0009),
(KSDATAFORMAT_SUBTYPE_ALAW, 0x0006),
(KSDATAFORMAT_SUBTYPE_MULAW, 0x0007),
(KSDATAFORMAT_SUBTYPE_ADPCM, 0x0002),
(KSDATAFORMAT_SUBTYPE_MPEG, 0x0050),
] {
assert!(
guid.is_ksdataformat_base(),
"{} not KSDATAFORMAT base",
guid.display()
);
assert_eq!(guid.ksdataformat_tag(), Some(expected_tag));
}
let foreign = Guid::from_components(0xDEAD_BEEF, 0xCAFE, 0xBABE, [1, 2, 3, 4, 5, 6, 7, 8]);
assert!(!foreign.is_ksdataformat_base());
assert_eq!(foreign.ksdataformat_tag(), None);
}
#[test]
fn wfex_roundtrip_pcm_24in32() {
let channels = 6u16;
let sample_rate = 48_000u32;
let block_align = channels * 4; let avg_bps = sample_rate * block_align as u32;
let channel_mask: u32 = 0x0000_003F;
let bytes = write_waveformatextensible(
channels,
sample_rate,
avg_bps,
block_align,
32, 24, channel_mask,
&KSDATAFORMAT_SUBTYPE_PCM,
);
assert_eq!(bytes.len(), 40, "WAVEFORMATEXTENSIBLE is 18 + 2 + 22 bytes");
assert_eq!(u16::from_le_bytes([bytes[16], bytes[17]]), 22);
let parsed = parse_waveformatextensible(&bytes).unwrap();
assert_eq!(parsed.wfx.format_tag, WAVE_FORMAT_EXTENSIBLE);
assert_eq!(parsed.wfx.channels, channels);
assert_eq!(parsed.wfx.samples_per_sec, sample_rate);
assert_eq!(parsed.wfx.avg_bytes_per_sec, avg_bps);
assert_eq!(parsed.wfx.block_align, block_align);
assert_eq!(parsed.wfx.bits_per_sample, 32);
assert_eq!(parsed.valid_bits_per_sample, 24);
assert_eq!(parsed.channel_mask, channel_mask);
assert_eq!(parsed.subformat, KSDATAFORMAT_SUBTYPE_PCM);
assert!(parsed.wfx.extradata.is_empty());
}
#[test]
fn wfex_rejects_non_extensible_format_tag() {
let bytes = write_waveformatex(0x0001, 2, 48_000, 192_000, 4, 16, &[0u8; 22]);
assert!(parse_waveformatextensible(&bytes).is_err());
}
#[test]
fn wfex_rejects_short_extension() {
let bytes = write_waveformatex(
WAVE_FORMAT_EXTENSIBLE,
2,
48_000,
192_000,
4,
16,
&[0u8; 10],
);
assert!(parse_waveformatextensible(&bytes).is_err());
}
#[test]
fn subformat_codec_hint_pcm_depth_aware() {
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_PCM, 8),
Some("pcm_u8")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_PCM, 16),
Some("pcm_s16le")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_PCM, 24),
Some("pcm_s24le")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_PCM, 32),
Some("pcm_s32le")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 32),
Some("pcm_f32le")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 64),
Some("pcm_f64le")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_ALAW, 8),
Some("pcm_alaw")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_MULAW, 8),
Some("pcm_mulaw")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_ADPCM, 4),
Some("adpcm_ms")
);
assert_eq!(
subformat_codec_hint(&KSDATAFORMAT_SUBTYPE_MPEG, 0),
Some("mp2")
);
let foreign = Guid::from_components(0xDEAD, 0xBEEF, 0xCAFE, [0; 8]);
assert!(subformat_codec_hint(&foreign, 16).is_none());
}
#[test]
fn wfx_short_legacy() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&0x0001u16.to_le_bytes());
bytes.extend_from_slice(&2u16.to_le_bytes());
bytes.extend_from_slice(&8_000u32.to_le_bytes());
bytes.extend_from_slice(&16_000u32.to_le_bytes());
bytes.extend_from_slice(&2u16.to_le_bytes());
let h = parse_waveformatex(&bytes).unwrap();
assert_eq!(h.bits_per_sample, 0);
assert!(h.extradata.is_empty());
}
}