use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlphCompression {
None,
Lossless,
Reserved(u8),
}
impl AlphCompression {
fn from_bits(c: u8) -> Self {
match c & 0b11 {
0 => Self::None,
1 => Self::Lossless,
other => Self::Reserved(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlphFiltering {
None,
Horizontal,
Vertical,
Gradient,
}
impl AlphFiltering {
fn from_bits(f: u8) -> Self {
match f & 0b11 {
0 => Self::None,
1 => Self::Horizontal,
2 => Self::Vertical,
3 => Self::Gradient,
_ => unreachable!("masked to 2 bits"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlphPreprocessing {
None,
LevelReduction,
Reserved(u8),
}
impl AlphPreprocessing {
fn from_bits(p: u8) -> Self {
match p & 0b11 {
0 => Self::None,
1 => Self::LevelReduction,
other => Self::Reserved(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AlphError {
EmptyPayload,
DimensionsOverflow {
width: u32,
height: u32,
},
RawLengthMismatch {
expected: usize,
actual: usize,
},
UnsupportedCompression(u8),
Vp8l(crate::vp8l_decode::DecodeError),
}
impl fmt::Display for AlphError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyPayload => {
f.write_str("ALPH payload missing the §2.7.1.2 info byte (payload length 0)")
}
Self::DimensionsOverflow { width, height } => write!(
f,
"ALPH alpha-plane dimensions {width}x{height} overflow the addressable range"
),
Self::RawLengthMismatch { expected, actual } => write!(
f,
"ALPH raw (method 0) bitstream length {actual} != width*height {expected}"
),
Self::UnsupportedCompression(c) => write!(
f,
"ALPH compression method {c} is undefined by §2.7.1.2 (only 0 and 1 exist)"
),
Self::Vp8l(e) => write!(f, "ALPH method-1 VP8L image-stream decode: {e}"),
}
}
}
impl std::error::Error for AlphError {}
impl From<crate::vp8l_decode::DecodeError> for AlphError {
fn from(e: crate::vp8l_decode::DecodeError) -> Self {
Self::Vp8l(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AlphHeader {
pub compression: AlphCompression,
pub filtering: AlphFiltering,
pub preprocessing: AlphPreprocessing,
pub reserved: u8,
pub info_byte: u8,
}
impl AlphHeader {
pub fn parse(payload: &[u8]) -> Result<Self, AlphError> {
let info = *payload.first().ok_or(AlphError::EmptyPayload)?;
let reserved = (info >> 6) & 0b11;
let p_bits = (info >> 4) & 0b11;
let f_bits = (info >> 2) & 0b11;
let c_bits = info & 0b11;
Ok(Self {
compression: AlphCompression::from_bits(c_bits),
filtering: AlphFiltering::from_bits(f_bits),
preprocessing: AlphPreprocessing::from_bits(p_bits),
reserved,
info_byte: info,
})
}
pub const fn bitstream_offset(&self) -> usize {
1
}
}
#[inline]
fn clip(v: i32) -> u8 {
v.clamp(0, 255) as u8
}
pub fn decode_alpha(payload: &[u8], width: u32, height: u32) -> Result<Vec<u8>, AlphError> {
let header = AlphHeader::parse(payload)?;
let count = (width as usize)
.checked_mul(height as usize)
.ok_or(AlphError::DimensionsOverflow { width, height })?;
let bitstream = &payload[header.bitstream_offset()..];
let filtered: Vec<u8> = match header.compression {
AlphCompression::None => {
if bitstream.len() != count {
return Err(AlphError::RawLengthMismatch {
expected: count,
actual: bitstream.len(),
});
}
bitstream.to_vec()
}
AlphCompression::Lossless => {
let image =
crate::vp8l_transform::decode_lossless_headerless(bitstream, width, height)?;
image
.pixels()
.iter()
.map(|argb| (argb >> 8) as u8)
.collect()
}
AlphCompression::Reserved(c) => return Err(AlphError::UnsupportedCompression(c)),
};
if count == 0 {
return Ok(filtered);
}
let w = width as usize;
let h = height as usize;
let mut out = vec![0u8; count];
let idx = |x: usize, y: usize| y * w + x;
for y in 0..h {
for x in 0..w {
let xv = filtered[idx(x, y)] as i32;
let predictor: i32 = match (x, y) {
(0, 0) => 0,
_ => match header.filtering {
AlphFiltering::None => 0,
AlphFiltering::Horizontal => {
if x == 0 {
out[idx(0, y - 1)] as i32
} else {
out[idx(x - 1, y)] as i32
}
}
AlphFiltering::Vertical => {
if y == 0 {
out[idx(x - 1, 0)] as i32
} else {
out[idx(x, y - 1)] as i32
}
}
AlphFiltering::Gradient => {
if x == 0 {
out[idx(0, y - 1)] as i32
} else if y == 0 {
out[idx(x - 1, 0)] as i32
} else {
let a = out[idx(x - 1, y)] as i32;
let b = out[idx(x, y - 1)] as i32;
let c = out[idx(x - 1, y - 1)] as i32;
clip(a + b - c) as i32
}
}
},
};
out[idx(x, y)] = ((predictor + xv) & 0xff) as u8;
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn info(rsv: u8, p: u8, f: u8, c: u8) -> u8 {
((rsv & 0b11) << 6) | ((p & 0b11) << 4) | ((f & 0b11) << 2) | (c & 0b11)
}
#[test]
fn empty_payload_is_rejected_with_named_error() {
assert_eq!(AlphHeader::parse(&[]), Err(AlphError::EmptyPayload));
}
#[test]
fn all_zero_info_decodes_to_none_none_none_zero() {
let h = AlphHeader::parse(&[0x00]).unwrap();
assert_eq!(h.compression, AlphCompression::None);
assert_eq!(h.filtering, AlphFiltering::None);
assert_eq!(h.preprocessing, AlphPreprocessing::None);
assert_eq!(h.reserved, 0);
assert_eq!(h.info_byte, 0);
assert_eq!(h.bitstream_offset(), 1);
}
#[test]
fn compression_field_decodes_all_four_values() {
assert_eq!(
AlphHeader::parse(&[info(0, 0, 0, 0)]).unwrap().compression,
AlphCompression::None
);
assert_eq!(
AlphHeader::parse(&[info(0, 0, 0, 1)]).unwrap().compression,
AlphCompression::Lossless
);
assert_eq!(
AlphHeader::parse(&[info(0, 0, 0, 2)]).unwrap().compression,
AlphCompression::Reserved(2)
);
assert_eq!(
AlphHeader::parse(&[info(0, 0, 0, 3)]).unwrap().compression,
AlphCompression::Reserved(3)
);
}
#[test]
fn filtering_field_decodes_all_four_methods() {
assert_eq!(
AlphHeader::parse(&[info(0, 0, 0, 0)]).unwrap().filtering,
AlphFiltering::None
);
assert_eq!(
AlphHeader::parse(&[info(0, 0, 1, 0)]).unwrap().filtering,
AlphFiltering::Horizontal
);
assert_eq!(
AlphHeader::parse(&[info(0, 0, 2, 0)]).unwrap().filtering,
AlphFiltering::Vertical
);
assert_eq!(
AlphHeader::parse(&[info(0, 0, 3, 0)]).unwrap().filtering,
AlphFiltering::Gradient
);
}
#[test]
fn preprocessing_field_decodes_both_named_values_plus_reserved() {
assert_eq!(
AlphHeader::parse(&[info(0, 0, 0, 0)])
.unwrap()
.preprocessing,
AlphPreprocessing::None
);
assert_eq!(
AlphHeader::parse(&[info(0, 1, 0, 0)])
.unwrap()
.preprocessing,
AlphPreprocessing::LevelReduction
);
assert_eq!(
AlphHeader::parse(&[info(0, 2, 0, 0)])
.unwrap()
.preprocessing,
AlphPreprocessing::Reserved(2)
);
assert_eq!(
AlphHeader::parse(&[info(0, 3, 0, 0)])
.unwrap()
.preprocessing,
AlphPreprocessing::Reserved(3)
);
}
#[test]
fn reserved_field_surfaces_raw_two_bit_value_without_rejection() {
for rsv in 0u8..=3 {
let h = AlphHeader::parse(&[info(rsv, 0, 0, 0)]).unwrap();
assert_eq!(h.reserved, rsv, "Rsv={rsv}");
assert_eq!(h.compression, AlphCompression::None);
assert_eq!(h.filtering, AlphFiltering::None);
assert_eq!(h.preprocessing, AlphPreprocessing::None);
}
}
#[test]
fn fields_decode_independently_across_a_full_combination() {
let h = AlphHeader::parse(&[0xB6]).unwrap();
assert_eq!(h.reserved, 0b10);
assert_eq!(h.preprocessing, AlphPreprocessing::Reserved(0b11));
assert_eq!(h.filtering, AlphFiltering::Horizontal);
assert_eq!(h.compression, AlphCompression::Reserved(0b10));
assert_eq!(h.info_byte, 0xB6);
}
#[test]
fn fixture_lossy_with_alpha_info_byte_decodes_to_lossless_no_filter_no_pre() {
let h = AlphHeader::parse(&[0x01]).unwrap();
assert_eq!(h.compression, AlphCompression::Lossless);
assert_eq!(h.filtering, AlphFiltering::None);
assert_eq!(h.preprocessing, AlphPreprocessing::None);
assert_eq!(h.reserved, 0);
assert_eq!(h.info_byte, 0x01);
}
#[test]
fn bitstream_offset_is_always_one_past_the_info_byte() {
let h = AlphHeader::parse(&[0x01, 0xAA, 0xBB]).unwrap();
assert_eq!(h.bitstream_offset(), 1);
}
#[test]
fn trailing_bytes_are_not_consumed_by_the_info_byte_parse() {
let baseline = AlphHeader::parse(&[0x01]).unwrap();
let with_tail = AlphHeader::parse(&[0x01, 0xFF, 0x00, 0x55, 0xAA]).unwrap();
assert_eq!(baseline, with_tail);
}
fn raw_alph(f: u8, residual: &[u8]) -> Vec<u8> {
let mut v = vec![info(0, 0, f, 0)];
v.extend_from_slice(residual);
v
}
#[test]
fn decode_raw_uncompressed_no_filter_is_identity() {
let residual = [10u8, 5, 250, 3, 100, 200];
let payload = raw_alph(0, &residual);
let plane = decode_alpha(&payload, 3, 2).unwrap();
assert_eq!(plane, residual.to_vec());
}
#[test]
fn decode_raw_length_mismatch_is_rejected() {
let payload = raw_alph(0, &[1, 2, 3]); assert_eq!(
decode_alpha(&payload, 2, 2),
Err(AlphError::RawLengthMismatch {
expected: 4,
actual: 3
})
);
}
#[test]
fn decode_unsupported_compression_method_is_rejected() {
let payload = vec![info(0, 0, 0, 2), 0, 0, 0, 0];
assert_eq!(
decode_alpha(&payload, 2, 2),
Err(AlphError::UnsupportedCompression(2))
);
}
#[test]
fn decode_horizontal_filter_inverse() {
let residual = [10u8, 5, 250, 3, 100, 200];
let payload = raw_alph(1, &residual);
let plane = decode_alpha(&payload, 3, 2).unwrap();
assert_eq!(plane, vec![10, 15, 9, 13, 113, 57]);
}
#[test]
fn decode_vertical_filter_inverse() {
let residual = [10u8, 5, 250, 3, 100, 200];
let payload = raw_alph(2, &residual);
let plane = decode_alpha(&payload, 3, 2).unwrap();
assert_eq!(plane, vec![10, 15, 9, 13, 115, 209]);
}
#[test]
fn decode_gradient_filter_inverse() {
let residual = [10u8, 5, 7, 3, 100, 50, 20, 8, 9];
let payload = raw_alph(3, &residual);
let plane = decode_alpha(&payload, 3, 3).unwrap();
assert_eq!(plane, vec![10, 15, 22, 13, 118, 175, 33, 146, 212]);
}
#[test]
fn decode_modulo_256_wraps_into_0_255() {
let payload = raw_alph(1, &[200, 200]);
let plane = decode_alpha(&payload, 2, 1).unwrap();
assert_eq!(plane, vec![200, 144]);
}
#[test]
fn decode_gradient_clip_clamps_predictor() {
let payload = raw_alph(3, &[255, 0, 0, 5]);
let plane = decode_alpha(&payload, 2, 2).unwrap();
assert_eq!(plane, vec![255, 255, 255, 4]);
}
#[test]
fn decode_zero_area_plane_is_empty() {
let payload = raw_alph(0, &[]);
assert_eq!(decode_alpha(&payload, 0, 4).unwrap(), Vec::<u8>::new());
assert_eq!(decode_alpha(&payload, 4, 0).unwrap(), Vec::<u8>::new());
}
#[test]
fn decode_empty_payload_is_rejected() {
assert_eq!(decode_alpha(&[], 1, 1), Err(AlphError::EmptyPayload));
}
}