use oxideav_core::frame::VideoPlane;
use oxideav_core::Decoder;
use oxideav_core::{
CodecId, CodecParameters, Error, Frame, Packet, PixelFormat, Result, VideoFrame,
};
use crate::alpha::{decode_scanned_alpha, AlphaChannelType};
use crate::dct::{idct8x8, idct8x8_dc_only, is_dc_only};
use crate::frame::{
compute_slice_sizes, parse_frame, parse_picture_header, parse_slice_header, ChromaFormat,
FrameHeader,
};
use crate::quant::qscale;
use crate::slice::{
blocks_per_mb, chroma_blocks_per_mb, decode_slice_components, LUMA_BLOCKS_PER_MB,
};
const MB_SIDE_PX: usize = 16;
const MAX_DECODED_PIXELS: usize = 32_768 * 32_768;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum BitDepth {
Eight,
Ten,
Twelve,
Sixteen,
}
impl BitDepth {
pub fn bytes_per_sample(self) -> usize {
match self {
Self::Eight => 1,
Self::Ten | Self::Twelve | Self::Sixteen => 2,
}
}
pub fn max_value(self) -> u32 {
match self {
Self::Eight => 255,
Self::Ten => 1023,
Self::Twelve => 4095,
Self::Sixteen => 65535,
}
}
pub fn bits(self) -> u32 {
match self {
Self::Eight => 8,
Self::Ten => 10,
Self::Twelve => 12,
Self::Sixteen => 16,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
pub enum OutputRange {
#[default]
Full,
Video,
}
impl OutputRange {
pub fn bounds(self, bd: BitDepth) -> (u32, u32) {
let max = bd.max_value();
match self {
Self::Full => (0, max),
Self::Video => (1, max - 1),
}
}
}
type OutputSurface = (BitDepth, ChromaFormat, bool);
fn resolve_pixel_format(pf: PixelFormat) -> Result<OutputSurface> {
Ok(match pf {
PixelFormat::Yuv422P => (BitDepth::Eight, ChromaFormat::Y422, false),
PixelFormat::Yuv444P => (BitDepth::Eight, ChromaFormat::Y444, false),
PixelFormat::Yuv422P10Le => (BitDepth::Ten, ChromaFormat::Y422, false),
PixelFormat::Yuv444P10Le => (BitDepth::Ten, ChromaFormat::Y444, false),
PixelFormat::Yuv422P12Le => (BitDepth::Twelve, ChromaFormat::Y422, false),
PixelFormat::Yuv444P12Le => (BitDepth::Twelve, ChromaFormat::Y444, false),
PixelFormat::Yuv422P16Le => (BitDepth::Sixteen, ChromaFormat::Y422, false),
PixelFormat::Yuv444P16Le => (BitDepth::Sixteen, ChromaFormat::Y444, false),
PixelFormat::Yuva422P => (BitDepth::Eight, ChromaFormat::Y422, true),
PixelFormat::Yuva444P => (BitDepth::Eight, ChromaFormat::Y444, true),
PixelFormat::Yuva422P10Le => (BitDepth::Ten, ChromaFormat::Y422, true),
PixelFormat::Yuva444P10Le => (BitDepth::Ten, ChromaFormat::Y444, true),
PixelFormat::Yuva422P12Le => (BitDepth::Twelve, ChromaFormat::Y422, true),
PixelFormat::Yuva444P12Le => (BitDepth::Twelve, ChromaFormat::Y444, true),
PixelFormat::Yuva422P16Le => (BitDepth::Sixteen, ChromaFormat::Y422, true),
PixelFormat::Yuva444P16Le => (BitDepth::Sixteen, ChromaFormat::Y444, true),
other => {
return Err(Error::unsupported(format!(
"prores decoder: requested pixel_format {other:?} not supported \
(expected Yuv4(2|4)4P / Yuva4(2|4)4P, plain or with a 10Le/12Le/16Le \
depth suffix)"
)));
}
})
}
fn pick_output_format(params: &CodecParameters) -> Result<Option<OutputSurface>> {
params.pixel_format.map(resolve_pixel_format).transpose()
}
pub fn make_decoder(params: &CodecParameters) -> Result<Box<dyn Decoder>> {
let (requested, alpha_typed) = match pick_output_format(params)? {
Some((bd, cf, alpha)) => (Some((bd, cf)), alpha),
None => (None, false),
};
Ok(Box::new(ProResDecoder {
codec_id: params.codec_id.clone(),
requested,
alpha_typed,
range: OutputRange::Full,
pending: None,
eof: false,
}))
}
pub struct ProResDecoder {
codec_id: CodecId,
requested: Option<(BitDepth, ChromaFormat)>,
alpha_typed: bool,
range: OutputRange,
pending: Option<Packet>,
eof: bool,
}
impl ProResDecoder {
pub fn set_output_range(&mut self, range: OutputRange) {
self.range = range;
}
pub fn output_range(&self) -> OutputRange {
self.range
}
}
impl Decoder for ProResDecoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> Result<()> {
if self.pending.is_some() {
return Err(Error::other(
"prores decoder: receive_frame must be called before sending another packet",
));
}
self.pending = Some(packet.clone());
Ok(())
}
fn receive_frame(&mut self) -> Result<Frame> {
let Some(pkt) = self.pending.take() else {
return if self.eof {
Err(Error::Eof)
} else {
Err(Error::NeedMore)
};
};
let vf = decode_packet_inner(
&pkt.data,
pkt.pts,
self.requested,
self.range,
self.alpha_typed,
)?;
Ok(Frame::Video(vf))
}
fn flush(&mut self) -> Result<()> {
self.eof = true;
Ok(())
}
}
pub fn decode_packet(data: &[u8], pts: Option<i64>) -> Result<VideoFrame> {
decode_packet_with_depth(data, pts, None)
}
pub fn decode_packet_with_depth(
data: &[u8],
pts: Option<i64>,
requested: Option<(BitDepth, ChromaFormat)>,
) -> Result<VideoFrame> {
decode_packet_with_options(data, pts, requested, OutputRange::Full)
}
pub fn decode_packet_with_options(
data: &[u8],
pts: Option<i64>,
requested: Option<(BitDepth, ChromaFormat)>,
range: OutputRange,
) -> Result<VideoFrame> {
decode_packet_inner(data, pts, requested, range, false)
}
pub fn decode_packet_with_format(
data: &[u8],
pts: Option<i64>,
pixel_format: Option<PixelFormat>,
range: OutputRange,
) -> Result<VideoFrame> {
let (requested, alpha_typed) = match pixel_format.map(resolve_pixel_format).transpose()? {
Some((bd, cf, alpha)) => (Some((bd, cf)), alpha),
None => (None, false),
};
decode_packet_inner(data, pts, requested, range, alpha_typed)
}
fn decode_packet_inner(
data: &[u8],
pts: Option<i64>,
requested: Option<(BitDepth, ChromaFormat)>,
range: OutputRange,
alpha_typed: bool,
) -> Result<VideoFrame> {
let (fh, after_frame) = parse_frame(data)?;
let width = fh.width as usize;
let height = fh.height as usize;
if width == 0 || height == 0 {
return Err(Error::invalid("prores: zero-sized frame"));
}
let pixels = width.saturating_mul(height);
if pixels > MAX_DECODED_PIXELS {
return Err(Error::invalid(format!(
"prores: declared frame size ({width}x{height}) exceeds internal cap"
)));
}
let alpha_kind = AlphaChannelType::from_code(fh.alpha_channel_type)?;
let has_alpha = alpha_kind.is_some();
let interlaced = fh.interlace_mode != 0;
let chroma = fh.chroma_format;
let bit_depth = if let Some((bd, requested_chroma)) = requested {
if requested_chroma != chroma {
return Err(Error::invalid(format!(
"prores: requested pixel_format chroma {:?} does not match \
frame chroma {:?}",
requested_chroma, chroma,
)));
}
bd
} else {
BitDepth::Eight
};
let bps = bit_depth.bytes_per_sample();
let mbs_x = width.div_ceil(MB_SIDE_PX);
let padded_w = mbs_x * MB_SIDE_PX;
let padded_c_w = match chroma {
ChromaFormat::Y422 => padded_w / 2,
ChromaFormat::Y444 => padded_w,
};
let padded_frame_h = if interlaced {
let top_h = height.div_ceil(2);
let bot_h = height / 2;
let top_mb_rows = top_h.div_ceil(MB_SIDE_PX);
let bot_mb_rows = bot_h.div_ceil(MB_SIDE_PX);
top_mb_rows.max(bot_mb_rows) * MB_SIDE_PX * 2
} else {
height.div_ceil(MB_SIDE_PX) * MB_SIDE_PX
};
let y_alloc = padded_w.saturating_mul(padded_frame_h);
let c_alloc = padded_c_w.saturating_mul(padded_frame_h);
if y_alloc > MAX_DECODED_PIXELS || c_alloc > MAX_DECODED_PIXELS {
return Err(Error::invalid("prores: padded plane size exceeds cap"));
}
let y_byte_stride = padded_w * bps;
let c_byte_stride = padded_c_w * bps;
let a_byte_stride = padded_w * bps;
let mut y_plane = vec![0u8; y_byte_stride * padded_frame_h];
let mut cb_plane = vec![0u8; c_byte_stride * padded_frame_h];
let mut cr_plane = vec![0u8; c_byte_stride * padded_frame_h];
let mut a_plane: Vec<u8> = if has_alpha {
vec![0u8; a_byte_stride * padded_frame_h]
} else {
Vec::new()
};
let mut cursor = after_frame;
if interlaced {
let top_h = height.div_ceil(2);
let bot_h = height / 2;
let (first_h, first_field_offset, second_h, second_field_offset) = match fh.interlace_mode {
1 => (top_h, 0usize, bot_h, 1usize),
2 => (bot_h, 1usize, top_h, 0usize),
other => {
return Err(Error::invalid(format!(
"prores: invalid interlace_mode {other}"
)));
}
};
cursor = decode_picture_into_planes(
cursor,
&fh,
mbs_x,
first_h,
chroma,
bit_depth,
range,
alpha_kind,
true,
FieldStride::new(2, first_field_offset),
&mut y_plane,
y_byte_stride,
&mut cb_plane,
c_byte_stride,
&mut cr_plane,
&mut a_plane,
a_byte_stride,
)?;
cursor = decode_picture_into_planes(
cursor,
&fh,
mbs_x,
second_h,
chroma,
bit_depth,
range,
alpha_kind,
true,
FieldStride::new(2, second_field_offset),
&mut y_plane,
y_byte_stride,
&mut cb_plane,
c_byte_stride,
&mut cr_plane,
&mut a_plane,
a_byte_stride,
)?;
} else {
cursor = decode_picture_into_planes(
cursor,
&fh,
mbs_x,
height,
chroma,
bit_depth,
range,
alpha_kind,
false,
FieldStride::progressive(),
&mut y_plane,
y_byte_stride,
&mut cb_plane,
c_byte_stride,
&mut cr_plane,
&mut a_plane,
a_byte_stride,
)?;
}
let _ = cursor;
let c_w = match chroma {
ChromaFormat::Y422 => width.div_ceil(2),
ChromaFormat::Y444 => width,
};
let y_cropped = crop_plane(&y_plane, y_byte_stride, width, height, bps);
let cb_cropped = crop_plane(&cb_plane, c_byte_stride, c_w, height, bps);
let cr_cropped = crop_plane(&cr_plane, c_byte_stride, c_w, height, bps);
let mut planes = vec![
VideoPlane {
stride: width * bps,
data: y_cropped,
},
VideoPlane {
stride: c_w * bps,
data: cb_cropped,
},
VideoPlane {
stride: c_w * bps,
data: cr_cropped,
},
];
if has_alpha {
let a_cropped = crop_plane(&a_plane, a_byte_stride, width, height, bps);
planes.push(VideoPlane {
stride: width * bps,
data: a_cropped,
});
} else if alpha_typed {
planes.push(opaque_alpha_plane(width, height, bit_depth));
}
Ok(VideoFrame { pts, planes })
}
fn opaque_alpha_plane(width: usize, height: usize, bit_depth: BitDepth) -> VideoPlane {
let bps = bit_depth.bytes_per_sample();
let max = bit_depth.max_value();
let mut data = vec![0u8; width * height * bps];
if bps == 1 {
data.fill(max as u8);
} else {
let le = (max as u16).to_le_bytes();
for s in data.chunks_exact_mut(2) {
s.copy_from_slice(&le);
}
}
VideoPlane {
stride: width * bps,
data,
}
}
#[derive(Copy, Clone, Debug)]
struct FieldStride {
step: usize,
offset: usize,
}
impl FieldStride {
fn new(step: usize, offset: usize) -> Self {
Self { step, offset }
}
fn progressive() -> Self {
Self { step: 1, offset: 0 }
}
fn map(self, picture_row: usize) -> usize {
self.step * picture_row + self.offset
}
}
#[allow(clippy::too_many_arguments)]
fn decode_picture_into_planes<'a>(
data: &'a [u8],
fh: &FrameHeader,
mbs_x: usize,
picture_height: usize,
chroma: ChromaFormat,
bit_depth: BitDepth,
range: OutputRange,
alpha_kind: Option<AlphaChannelType>,
interlaced: bool,
field: FieldStride,
y_plane: &mut [u8],
y_byte_stride: usize,
cb_plane: &mut [u8],
c_byte_stride: usize,
cr_plane: &mut [u8],
a_plane: &mut [u8],
a_byte_stride: usize,
) -> Result<&'a [u8]> {
let has_alpha = alpha_kind.is_some();
let mbs_y = picture_height.div_ceil(MB_SIDE_PX);
let (ph, after_pic) = parse_picture_header(data)?;
let slice_sizes_template = compute_slice_sizes(mbs_x, ph.log2_desired_slice_size_in_mb);
let slices_per_row = slice_sizes_template.len();
let expected_slice_count = slices_per_row * mbs_y;
let slice_table_bytes = expected_slice_count
.checked_mul(2)
.ok_or_else(|| Error::invalid("prores: slice count overflow"))?;
if after_pic.len() < slice_table_bytes {
return Err(Error::invalid("prores: slice-size table truncated"));
}
let mut slice_sizes = Vec::with_capacity(expected_slice_count);
for i in 0..expected_slice_count {
let off = i * 2;
slice_sizes.push(u16::from_be_bytes(after_pic[off..off + 2].try_into().unwrap()) as usize);
}
let mut cursor = &after_pic[slice_table_bytes..];
let consumed_picture_bytes: usize =
ph.picture_header_size as usize + slice_table_bytes + slice_sizes.iter().sum::<usize>();
let declared_picture_bytes = ph.picture_size as usize;
if consumed_picture_bytes > declared_picture_bytes {
return Err(Error::invalid(
"prores: header + slice table + payloads exceed declared picture_size",
));
}
if data.len() < declared_picture_bytes {
return Err(Error::invalid("prores: picture overruns buffer"));
}
const LUMA_OFFSETS: [(usize, usize); 4] = [(0, 0), (1, 0), (0, 1), (1, 1)];
let chroma_offsets: &[(usize, usize)] = match chroma {
ChromaFormat::Y422 => &[(0, 0), (0, 1)],
ChromaFormat::Y444 => &LUMA_OFFSETS,
};
let cb_per_mb = chroma_blocks_per_mb(chroma);
let per_mb = blocks_per_mb(chroma);
let mut slice_idx = 0usize;
for my in 0..mbs_y {
let mut mx = 0usize;
for &slice_size_in_mb_template in &slice_sizes_template {
let mbs_this_slice = slice_size_in_mb_template.min(mbs_x - mx);
if mbs_this_slice == 0 {
break;
}
let coded_size = slice_sizes[slice_idx];
if cursor.len() < coded_size {
return Err(Error::invalid("prores: slice payload truncated"));
}
let slice_data = &cursor[..coded_size];
cursor = &cursor[coded_size..];
slice_idx += 1;
let (sh, after_sh) = parse_slice_header(slice_data, has_alpha)?;
let coded_y = sh.coded_size_of_y_data as usize;
let coded_cb = sh.coded_size_of_cb_data as usize;
let cr_data_size = if let Some(sz) = sh.coded_size_of_cr_data {
sz as usize
} else {
slice_data
.len()
.checked_sub(sh.slice_header_size as usize + coded_y + coded_cb)
.ok_or_else(|| Error::invalid("prores: cr_data size underflow"))?
};
if after_sh.len() < coded_y + coded_cb + cr_data_size {
return Err(Error::invalid("prores: slice components truncated"));
}
let y_data = &after_sh[..coded_y];
let cb_data = &after_sh[coded_y..coded_y + coded_cb];
let cr_data = &after_sh[coded_y + coded_cb..coded_y + coded_cb + cr_data_size];
let alpha_data: &[u8] = if has_alpha {
&after_sh[coded_y + coded_cb + cr_data_size..]
} else {
&[]
};
let blocks = decode_slice_components(
y_data,
cb_data,
cr_data,
mbs_this_slice,
chroma,
interlaced,
)?;
if blocks.len() != mbs_this_slice * per_mb {
return Err(Error::invalid(
"prores: decoded block count mismatch in slice",
));
}
for mb_within in 0..mbs_this_slice {
let mb_x = mx + mb_within;
let base = mb_within * per_mb;
for (i, (bx, by)) in LUMA_OFFSETS.iter().enumerate() {
let mut blk_f =
dequant_to_f32(&blocks[base + i], &fh.luma_qmat, sh.quantization_index);
if is_dc_only(&blk_f) {
idct8x8_dc_only(&mut blk_f);
} else {
idct8x8(&mut blk_f);
}
paste_block(
y_plane,
y_byte_stride,
mb_x * MB_SIDE_PX + bx * 8,
my * MB_SIDE_PX + by * 8,
&blk_f,
bit_depth,
range,
field,
);
}
for (i, (bx, by)) in chroma_offsets.iter().enumerate() {
let mut blk_f = dequant_to_f32(
&blocks[base + LUMA_BLOCKS_PER_MB + i],
&fh.chroma_qmat,
sh.quantization_index,
);
if is_dc_only(&blk_f) {
idct8x8_dc_only(&mut blk_f);
} else {
idct8x8(&mut blk_f);
}
let (x0, y0) = match chroma {
ChromaFormat::Y422 => (mb_x * 8, my * MB_SIDE_PX + by * 8),
ChromaFormat::Y444 => {
(mb_x * MB_SIDE_PX + bx * 8, my * MB_SIDE_PX + by * 8)
}
};
paste_block(
cb_plane,
c_byte_stride,
x0,
y0,
&blk_f,
bit_depth,
range,
field,
);
}
for (i, (bx, by)) in chroma_offsets.iter().enumerate() {
let mut blk_f = dequant_to_f32(
&blocks[base + LUMA_BLOCKS_PER_MB + cb_per_mb + i],
&fh.chroma_qmat,
sh.quantization_index,
);
if is_dc_only(&blk_f) {
idct8x8_dc_only(&mut blk_f);
} else {
idct8x8(&mut blk_f);
}
let (x0, y0) = match chroma {
ChromaFormat::Y422 => (mb_x * 8, my * MB_SIDE_PX + by * 8),
ChromaFormat::Y444 => {
(mb_x * MB_SIDE_PX + bx * 8, my * MB_SIDE_PX + by * 8)
}
};
paste_block(
cr_plane,
c_byte_stride,
x0,
y0,
&blk_f,
bit_depth,
range,
field,
);
}
}
if let Some(act) = alpha_kind {
let slice_vertical_size = MB_SIDE_PX;
let cols = MB_SIDE_PX * mbs_this_slice;
let num_alpha_values = cols * slice_vertical_size;
let alpha_values = decode_scanned_alpha(alpha_data, num_alpha_values, act)?;
let plane_rows = a_plane.len() / a_byte_stride;
let dst_y = my * MB_SIDE_PX;
let usable_rows = plane_rows
.saturating_sub(field.map(dst_y))
.div_ceil(field.step.max(1))
.min(MB_SIDE_PX);
paste_alpha(
a_plane,
a_byte_stride,
mx * MB_SIDE_PX,
dst_y,
cols,
usable_rows,
&alpha_values,
act,
bit_depth,
field,
);
}
mx += mbs_this_slice;
}
}
Ok(&data[declared_picture_bytes..])
}
fn dequant_to_f32(blk: &[i32; 64], qmat: &[u8; 64], quantization_index: u8) -> [f32; 64] {
let qs = qscale(quantization_index) as f32;
let mut out = [0.0f32; 64];
for k in 0..64 {
out[k] = (blk[k] as f32 * qmat[k] as f32 * qs) / 8.0;
}
out
}
fn color_to_sample(v: f32, bit_depth: BitDepth, range: OutputRange) -> u32 {
let scale = match bit_depth {
BitDepth::Eight => 0.5,
BitDepth::Ten => 2.0,
BitDepth::Twelve => 8.0,
BitDepth::Sixteen => 128.0,
};
let (nmin, nmax) = range.bounds(bit_depth);
let s = (v + 256.0) * scale;
if s <= nmin as f32 {
nmin
} else if s >= nmax as f32 {
nmax
} else {
s.round() as u32
}
}
#[allow(clippy::too_many_arguments)]
fn paste_block(
plane: &mut [u8],
byte_stride: usize,
x0: usize,
y0: usize,
blk: &[f32; 64],
bit_depth: BitDepth,
range: OutputRange,
field: FieldStride,
) {
match bit_depth {
BitDepth::Eight => {
for j in 0..8 {
let row = field.map(y0 + j);
for i in 0..8 {
let px = color_to_sample(blk[j * 8 + i], bit_depth, range) as u8;
plane[row * byte_stride + x0 + i] = px;
}
}
}
BitDepth::Ten | BitDepth::Twelve | BitDepth::Sixteen => {
for j in 0..8 {
let row = field.map(y0 + j);
for i in 0..8 {
let px = color_to_sample(blk[j * 8 + i], bit_depth, range) as u16;
let off = row * byte_stride + (x0 + i) * 2;
plane[off] = (px & 0xFF) as u8;
plane[off + 1] = (px >> 8) as u8;
}
}
}
}
}
fn alpha_to_sample(alpha: u16, act: AlphaChannelType, out_depth: BitDepth) -> u16 {
let max_out = out_depth.max_value();
let mask = act.mask();
let num = max_out as u64 * alpha as u64;
let denom = mask as u64;
((num + denom / 2) / denom) as u16
}
#[allow(clippy::too_many_arguments)]
fn paste_alpha(
plane: &mut [u8],
byte_stride: usize,
x0: usize,
y0: usize,
cols: usize,
rows: usize,
values: &[u16],
act: AlphaChannelType,
out_depth: BitDepth,
field: FieldStride,
) {
debug_assert!(values.len() >= cols * rows);
let bps = out_depth.bytes_per_sample();
for r in 0..rows {
let row = field.map(y0 + r);
for c in 0..cols {
let s = alpha_to_sample(values[r * cols + c], act, out_depth);
let off = row * byte_stride + (x0 + c) * bps;
match out_depth {
BitDepth::Eight => {
plane[off] = s as u8;
}
BitDepth::Ten | BitDepth::Twelve | BitDepth::Sixteen => {
plane[off] = (s & 0xFF) as u8;
plane[off + 1] = (s >> 8) as u8;
}
}
}
}
}
fn crop_plane(
src: &[u8],
src_byte_stride: usize,
dst_w: usize,
dst_h: usize,
bps: usize,
) -> Vec<u8> {
let dst_byte_stride = dst_w * bps;
let mut out = vec![0u8; dst_byte_stride * dst_h];
for y in 0..dst_h {
out[y * dst_byte_stride..y * dst_byte_stride + dst_byte_stride]
.copy_from_slice(&src[y * src_byte_stride..y * src_byte_stride + dst_byte_stride]);
}
out
}
#[cfg(test)]
mod alpha_sample_tests {
use super::{alpha_to_sample, AlphaChannelType, BitDepth};
#[test]
fn eight_bit_alpha_to_eight_bit_is_identity() {
for a in 0u16..=255 {
assert_eq!(
alpha_to_sample(a, AlphaChannelType::Eight, BitDepth::Eight),
a,
"8-bit alpha {a} must map to itself at 8-bit output"
);
}
}
#[test]
fn endpoints_map_to_full_opacity_range() {
for act in [AlphaChannelType::Eight, AlphaChannelType::Sixteen] {
let max_in = act.mask() as u16;
for depth in [
BitDepth::Eight,
BitDepth::Ten,
BitDepth::Twelve,
BitDepth::Sixteen,
] {
assert_eq!(
alpha_to_sample(0, act, depth),
0,
"alpha 0 (opacity 0.0) must map to sample 0 ({act:?} -> {depth:?})"
);
assert_eq!(
u32::from(alpha_to_sample(max_in, act, depth)),
depth.max_value(),
"alpha {max_in} (opacity 1.0) must map to sample 2^b-1 ({act:?} -> {depth:?})"
);
}
}
}
#[test]
fn eight_bit_alpha_promotes_to_higher_depth() {
fn expect(alpha: u32, max_out: u32, mask: u32) -> u16 {
((max_out * alpha * 2 + mask) / (mask * 2)) as u16
}
for &alpha in &[0u32, 1, 64, 127, 128, 200, 254, 255] {
assert_eq!(
alpha_to_sample(alpha as u16, AlphaChannelType::Eight, BitDepth::Ten),
expect(alpha, 1023, 255),
"8->10 promotion mismatch at alpha {alpha}"
);
assert_eq!(
alpha_to_sample(alpha as u16, AlphaChannelType::Eight, BitDepth::Twelve),
expect(alpha, 4095, 255),
"8->12 promotion mismatch at alpha {alpha}"
);
assert_eq!(
alpha_to_sample(alpha as u16, AlphaChannelType::Eight, BitDepth::Sixteen),
(alpha * 257) as u16,
"8->16 promotion mismatch at alpha {alpha}"
);
}
}
#[test]
fn sixteen_bit_alpha_to_sixteen_bit_is_identity() {
for a in (0u32..=65535).step_by(97).chain([65535]) {
assert_eq!(
alpha_to_sample(a as u16, AlphaChannelType::Sixteen, BitDepth::Sixteen),
a as u16,
"16-bit alpha {a} must map to itself at 16-bit output"
);
}
}
#[test]
fn sixteen_bit_alpha_demotes_monotonically() {
fn expect(alpha: u32, max_out: u32, mask: u32) -> u16 {
((max_out as u64 * alpha as u64 * 2 + mask as u64) / (mask as u64 * 2)) as u16
}
for depth in [BitDepth::Eight, BitDepth::Ten, BitDepth::Twelve] {
let max_out = depth.max_value();
let mut prev = 0u16;
for alpha in (0u32..=65535).step_by(257) {
let got = alpha_to_sample(alpha as u16, AlphaChannelType::Sixteen, depth);
assert_eq!(
got,
expect(alpha, max_out, 65535),
"16->{}b demotion mismatch at alpha {alpha}",
depth.bits()
);
assert!(
u32::from(got) <= max_out,
"demoted sample {got} exceeds 2^b-1 = {max_out}"
);
assert!(got >= prev, "demotion must be monotonic non-decreasing");
prev = got;
}
}
}
#[test]
fn rounds_half_up_at_a_known_midpoint() {
for act in [AlphaChannelType::Eight, AlphaChannelType::Sixteen] {
let mask = act.mask();
for depth in [
BitDepth::Eight,
BitDepth::Ten,
BitDepth::Twelve,
BitDepth::Sixteen,
] {
let max_out = depth.max_value();
for alpha in [1u32, 7, 19, 99, 100, mask / 2, mask / 2 + 1] {
let alpha = alpha.min(mask);
let exact = (max_out as f64 * alpha as f64) / mask as f64;
let want = exact.round() as u16; assert_eq!(
alpha_to_sample(alpha as u16, act, depth),
want,
"round mismatch: {act:?} alpha {alpha} -> {depth:?} (exact {exact})"
);
}
}
}
}
}
#[cfg(test)]
mod color_sample_tests {
use super::{color_to_sample, BitDepth, OutputRange};
#[test]
fn midpoint_v_zero_maps_to_half_scale() {
for range in [OutputRange::Full, OutputRange::Video] {
assert_eq!(color_to_sample(0.0, BitDepth::Eight, range), 128);
assert_eq!(color_to_sample(0.0, BitDepth::Ten, range), 512);
assert_eq!(color_to_sample(0.0, BitDepth::Twelve, range), 2048);
assert_eq!(color_to_sample(0.0, BitDepth::Sixteen, range), 32768);
}
}
#[test]
fn clamp_floor_and_ceiling_track_the_range() {
assert_eq!(
color_to_sample(-256.0, BitDepth::Eight, OutputRange::Full),
0
);
assert_eq!(
color_to_sample(-256.0, BitDepth::Eight, OutputRange::Video),
1
);
assert_eq!(color_to_sample(-256.0, BitDepth::Ten, OutputRange::Full), 0);
assert_eq!(
color_to_sample(-256.0, BitDepth::Twelve, OutputRange::Video),
1
);
assert_eq!(
color_to_sample(-256.0, BitDepth::Sixteen, OutputRange::Full),
0
);
assert_eq!(
color_to_sample(-256.0, BitDepth::Sixteen, OutputRange::Video),
1
);
assert_eq!(
color_to_sample(1000.0, BitDepth::Eight, OutputRange::Full),
255
);
assert_eq!(
color_to_sample(1000.0, BitDepth::Eight, OutputRange::Video),
254
);
assert_eq!(
color_to_sample(1000.0, BitDepth::Ten, OutputRange::Full),
1023
);
assert_eq!(
color_to_sample(1000.0, BitDepth::Ten, OutputRange::Video),
1022
);
assert_eq!(
color_to_sample(1000.0, BitDepth::Twelve, OutputRange::Full),
4095
);
assert_eq!(
color_to_sample(1000.0, BitDepth::Twelve, OutputRange::Video),
4094
);
assert_eq!(
color_to_sample(1000.0, BitDepth::Sixteen, OutputRange::Full),
65535
);
assert_eq!(
color_to_sample(1000.0, BitDepth::Sixteen, OutputRange::Video),
65534
);
}
#[test]
fn round_to_nearest_at_eight_bit() {
assert_eq!(
color_to_sample(1.0, BitDepth::Eight, OutputRange::Full),
129
);
assert_eq!(
color_to_sample(-1.0, BitDepth::Eight, OutputRange::Full),
128
);
assert_eq!(
color_to_sample(2.0, BitDepth::Eight, OutputRange::Full),
129
);
}
#[test]
fn video_equals_full_away_from_the_extremes() {
for &v in &[-200.0f32, -64.0, -1.0, 0.0, 1.0, 64.0, 200.0] {
for depth in [
BitDepth::Eight,
BitDepth::Ten,
BitDepth::Twelve,
BitDepth::Sixteen,
] {
assert_eq!(
color_to_sample(v, depth, OutputRange::Full),
color_to_sample(v, depth, OutputRange::Video),
"interior v={v} must match across ranges at {depth:?}"
);
}
}
}
}