mod pnm;
use std::borrow::Cow;
use pnm::{PnmDecoderConfig, PnmEncoderConfig};
use zencodec::decode::{Decode, DecodeJob, DecoderConfig, DynDecoderConfig};
use zencodec::encode::{DynEncoderConfig, EncodeJob, Encoder, EncoderConfig};
use zencodec::{ImageFormat, ResourceLimits, UnsupportedOperation};
use zenpixels::{PixelBuffer, PixelDescriptor, PixelSlice};
fn test_rgb8_pixels() -> PixelBuffer {
#[rustfmt::skip]
let data: Vec<u8> = vec![
255, 0, 0,
0, 255, 0,
0, 0, 255,
255, 255, 255,
0, 0, 0,
255, 255, 0,
0, 255, 255,
255, 0, 255,
];
PixelBuffer::from_vec(data, 4, 2, PixelDescriptor::RGB8_SRGB).expect("valid test buffer")
}
fn test_gray8_pixels() -> PixelBuffer {
let data: Vec<u8> = vec![0, 128, 255, 64, 192, 32];
PixelBuffer::from_vec(data, 3, 2, PixelDescriptor::GRAY8_SRGB).expect("valid test buffer")
}
#[test]
fn concrete_encode_decode_rgb8_roundtrip() {
let pixels = test_rgb8_pixels();
let config = PnmEncoderConfig::new();
let job = config.job();
let encoder = job.encoder().expect("encoder creation");
let output = encoder.encode(pixels.as_slice()).expect("encode");
assert_eq!(output.format(), ImageFormat::Pnm);
assert!(!output.is_empty());
assert_eq!(output.mime_type(), "image/x-portable-anymap");
assert_eq!(output.extension(), "pnm");
let encoded = output.data();
assert!(encoded.starts_with(b"P6\n4 2\n255\n"));
let dec_config = PnmDecoderConfig::new();
let dec_job = dec_config.job();
let info = dec_job.probe(encoded).expect("probe");
assert_eq!(info.width, 4);
assert_eq!(info.height, 2);
assert_eq!(info.format, ImageFormat::Pnm);
let decoder = dec_job
.decoder(Cow::Borrowed(encoded), &[])
.expect("decoder creation");
let decoded = decoder.decode().expect("decode");
let orig = pixels.as_slice();
let result = decoded.pixels();
assert_eq!(orig.width(), result.width());
assert_eq!(orig.rows(), result.rows());
assert_eq!(orig.descriptor(), result.descriptor());
for y in 0..orig.rows() {
assert_eq!(orig.row(y), result.row(y), "row {y} mismatch");
}
}
#[test]
fn concrete_encode_decode_gray8_roundtrip() {
let pixels = test_gray8_pixels();
let config = PnmEncoderConfig::new();
let encoder = config.job().encoder().expect("encoder");
let output = encoder.encode(pixels.as_slice()).expect("encode");
let encoded = output.data();
assert!(encoded.starts_with(b"P5\n3 2\n255\n"));
let dec_config = PnmDecoderConfig::new();
let decoder = dec_config
.job()
.decoder(Cow::Borrowed(encoded), &[])
.expect("decoder");
let decoded = decoder.decode().expect("decode");
let orig = pixels.as_slice();
let result = decoded.pixels();
assert_eq!(orig.descriptor(), result.descriptor());
for y in 0..orig.rows() {
assert_eq!(orig.row(y), result.row(y), "row {y} mismatch");
}
}
#[test]
fn dyn_encode_decode_rgb8_roundtrip() {
let pixels = test_rgb8_pixels();
let config = PnmEncoderConfig::new();
let enc_config: &dyn DynEncoderConfig = &config;
assert_eq!(enc_config.format(), ImageFormat::Pnm);
assert!(!enc_config.supported_descriptors().is_empty());
let enc_job = enc_config.dyn_job();
let encoder = enc_job.into_encoder().expect("dyn encoder");
let output = encoder.encode(pixels.as_slice()).expect("dyn encode");
let encoded = output.into_vec();
let dec_config = PnmDecoderConfig::new();
let dyn_dec: &dyn DynDecoderConfig = &dec_config;
assert_eq!(dyn_dec.formats(), &[ImageFormat::Pnm]);
let dec_job = dyn_dec.dyn_job();
let info = dec_job.probe(&encoded).expect("dyn probe");
assert_eq!(info.width, 4);
assert_eq!(info.height, 2);
let decoder = dec_job
.into_decoder(Cow::Borrowed(&encoded), &[])
.expect("dyn decoder");
let decoded = decoder.decode().expect("dyn decode");
let orig = test_rgb8_pixels();
let result = decoded.pixels();
assert_eq!(orig.as_slice().width(), result.width());
assert_eq!(orig.as_slice().rows(), result.rows());
for y in 0..result.rows() {
assert_eq!(orig.as_slice().row(y), result.row(y), "row {y} mismatch");
}
}
#[test]
fn dyn_encode_decode_gray8_roundtrip() {
let pixels = test_gray8_pixels();
let enc_config = PnmEncoderConfig::new();
let enc: &dyn DynEncoderConfig = &enc_config;
let output = enc
.dyn_job()
.into_encoder()
.expect("dyn encoder")
.encode(pixels.as_slice())
.expect("dyn encode");
let encoded = output.into_vec();
let dec_config = PnmDecoderConfig::new();
let dec: &dyn DynDecoderConfig = &dec_config;
let decoded = dec
.dyn_job()
.into_decoder(Cow::Borrowed(&encoded), &[])
.expect("dyn decoder")
.decode()
.expect("dyn decode");
let orig = pixels.as_slice();
let result = decoded.pixels();
assert_eq!(orig.descriptor(), result.descriptor());
for y in 0..orig.rows() {
assert_eq!(orig.row(y), result.row(y), "row {y} mismatch");
}
}
fn encode_with_any_codec(
config: &dyn DynEncoderConfig,
pixels: PixelSlice<'_>,
) -> Result<Vec<u8>, zencodec::encode::BoxedError> {
let job = config.dyn_job();
let encoder = job.into_encoder()?;
Ok(encoder.encode(pixels)?.into_vec())
}
fn decode_with_any_codec(
config: &dyn DynDecoderConfig,
data: &[u8],
) -> Result<PixelBuffer, zencodec::decode::BoxedError> {
let job = config.dyn_job();
let decoder = job.into_decoder(Cow::Borrowed(data), &[])?;
Ok(decoder.decode()?.into_buffer())
}
#[test]
fn codec_agnostic_roundtrip() {
let pixels = test_rgb8_pixels();
let enc_config = PnmEncoderConfig::new();
let encoded =
encode_with_any_codec(&enc_config, pixels.as_slice()).expect("codec-agnostic encode");
let dec_config = PnmDecoderConfig::new();
let decoded = decode_with_any_codec(&dec_config, &encoded).expect("codec-agnostic decode");
let orig = pixels.as_slice();
let result = decoded.as_slice();
for y in 0..orig.rows() {
assert_eq!(orig.row(y), result.row(y), "row {y} mismatch");
}
}
#[test]
fn encoder_config_static_methods() {
assert_eq!(
<PnmEncoderConfig as EncoderConfig>::format(),
ImageFormat::Pnm
);
let descs = <PnmEncoderConfig as EncoderConfig>::supported_descriptors();
assert!(descs.contains(&PixelDescriptor::RGB8_SRGB));
assert!(descs.contains(&PixelDescriptor::GRAY8_SRGB));
let caps = <PnmEncoderConfig as EncoderConfig>::capabilities();
assert!(caps.lossless());
}
#[test]
fn decoder_config_static_methods() {
assert_eq!(
<PnmDecoderConfig as DecoderConfig>::formats(),
&[ImageFormat::Pnm]
);
let descs = <PnmDecoderConfig as DecoderConfig>::supported_descriptors();
assert!(descs.contains(&PixelDescriptor::RGB8_SRGB));
assert!(descs.contains(&PixelDescriptor::GRAY8_SRGB));
let caps = <PnmDecoderConfig as DecoderConfig>::capabilities();
assert!(caps.cheap_probe());
}
#[test]
fn probe_and_output_info() {
let pixels = test_rgb8_pixels();
let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap()
.into_vec();
let dec_config = PnmDecoderConfig::new();
let job = dec_config.job();
let info = job.probe(&encoded).unwrap();
assert_eq!(info.width, 4);
assert_eq!(info.height, 2);
let out_info = job.output_info(&encoded).unwrap();
assert_eq!(out_info.width, 4);
assert_eq!(out_info.height, 2);
assert_eq!(out_info.native_format, PixelDescriptor::RGB8_SRGB);
}
#[test]
fn decode_respects_dimension_limits() {
let pixels = test_rgb8_pixels();
let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap()
.into_vec();
let limits = ResourceLimits::none().with_max_width(2);
let dec_config = PnmDecoderConfig::new();
let job = dec_config.job().with_limits(limits);
let result = job.decoder(Cow::Borrowed(&encoded), &[]);
assert!(result.is_err(), "should reject image exceeding width limit");
}
#[test]
fn decode_invalid_data() {
let dec_config = PnmDecoderConfig::new();
let job = dec_config.job();
let result = job.probe(b"not a pnm file");
assert!(result.is_err());
}
#[test]
fn decode_truncated() {
let dec_config = PnmDecoderConfig::new();
let result = dec_config.job().probe(b"P6");
assert!(result.is_err());
}
#[test]
fn unsupported_animation_encode() {
let config = PnmEncoderConfig::new();
let job = config.job();
let result = job.animation_frame_encoder();
assert!(result.is_err(), "PNM has no animation support");
}
#[test]
fn unsupported_streaming_decode() {
let pixels = test_rgb8_pixels();
let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap()
.into_vec();
let dec_config = PnmDecoderConfig::new();
let job = dec_config.job();
let result = job.streaming_decoder(Cow::Borrowed(&encoded), &[]);
assert!(result.is_err(), "PNM has no streaming decode");
}
#[test]
fn unsupported_animation_decode() {
let pixels = test_rgb8_pixels();
let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap()
.into_vec();
let dec_config = PnmDecoderConfig::new();
let job = dec_config.job();
let result = job.animation_frame_decoder(Cow::Borrowed(&encoded), &[]);
assert!(result.is_err(), "PNM has no animation decode");
}
#[test]
fn find_cause_limit_exceeded_through_dyn_decode() {
let pixels = test_rgb8_pixels();
let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap()
.into_vec();
let limits = ResourceLimits::none().with_max_width(2);
let dec_config = PnmDecoderConfig::new();
let dyn_dec: &dyn DynDecoderConfig = &dec_config;
let mut job = dyn_dec.dyn_job();
job.set_limits(limits);
let result = job.into_decoder(Cow::Borrowed(&encoded), &[]);
let err = match result {
Err(e) => e,
Ok(_) => panic!("should fail with limit exceeded"),
};
let limit = zencodec::find_cause::<zencodec::LimitExceeded>(&*err);
assert!(
limit.is_some(),
"find_cause should find LimitExceeded through BoxedError → At<PnmError> chain"
);
}
#[test]
fn find_cause_unsupported_through_dyn_decode() {
let pixels = test_rgb8_pixels();
let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap()
.into_vec();
let dec_config = PnmDecoderConfig::new();
let dyn_dec: &dyn DynDecoderConfig = &dec_config;
let job = dyn_dec.dyn_job();
let result = job.into_streaming_decoder(Cow::Borrowed(&encoded), &[]);
let err = match result {
Err(e) => e,
Ok(_) => panic!("streaming decode should fail"),
};
let op = zencodec::find_cause::<UnsupportedOperation>(&*err);
assert!(
op.is_some(),
"find_cause should find UnsupportedOperation through BoxedError → At<PnmError>"
);
assert_eq!(op.unwrap(), &UnsupportedOperation::RowLevelDecode);
}
#[test]
fn find_cause_unsupported_through_dyn_encode() {
let enc_config = PnmEncoderConfig::new();
let dyn_enc: &dyn DynEncoderConfig = &enc_config;
let job = dyn_enc.dyn_job();
let result = job.into_animation_frame_encoder();
let err = match result {
Err(e) => e,
Ok(_) => panic!("animation encode should fail"),
};
let op = zencodec::find_cause::<UnsupportedOperation>(&*err);
assert!(
op.is_some(),
"find_cause should find UnsupportedOperation through BoxedError → At<PnmError>"
);
assert_eq!(op.unwrap(), &UnsupportedOperation::AnimationEncode);
}
#[test]
fn concrete_error_preserves_at_wrapper() {
let pixels = test_rgb8_pixels();
let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap()
.into_vec();
let limits = ResourceLimits::none().with_max_height(1);
let dec_config = PnmDecoderConfig::new();
let dyn_dec: &dyn DynDecoderConfig = &dec_config;
let mut job = dyn_dec.dyn_job();
job.set_limits(limits);
let err = match job.into_decoder(Cow::Borrowed(&encoded), &[]) {
Err(e) => e,
Ok(_) => panic!("should fail with limit exceeded"),
};
let at_err = err.downcast_ref::<whereat::At<pnm::PnmError>>();
assert!(
at_err.is_some(),
"BoxedError should be downcastable to At<PnmError>"
);
let pnm_err = at_err.unwrap().error();
assert!(
matches!(pnm_err, pnm::PnmError::LimitExceeded(_)),
"inner error should be the LimitExceeded variant"
);
}
#[test]
fn whereat_trace_has_location() {
let dec_config = PnmDecoderConfig::new();
let job = dec_config.job();
let err = job.probe(b"not a pnm").expect_err("should fail");
assert!(
err.frame_count() > 0,
"At<PnmError> from start_at() should have at least one location frame"
);
let debug_str = format!("{:?}", err);
assert!(
debug_str.contains("pnm"),
"Debug output should contain source file reference: {debug_str}"
);
}
#[test]
fn find_cause_returns_none_for_absent_type() {
let dec_config = PnmDecoderConfig::new();
let dyn_dec: &dyn DynDecoderConfig = &dec_config;
let job = dyn_dec.dyn_job();
let err = job.probe(b"not a pnm").expect_err("should fail");
assert!(
zencodec::find_cause::<zencodec::LimitExceeded>(&*err).is_none(),
"find_cause should return None when cause type is absent"
);
}
use zencodec::decode::{DecodeRowSink, SinkError};
use zenpixels::PixelSliceMut;
fn gcd(mut a: usize, mut b: usize) -> usize {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
fn lcm(a: usize, b: usize) -> usize {
a / gcd(a, b) * b
}
#[test]
fn sink_preallocated_buffer() {
let pixels = test_rgb8_pixels(); let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap();
let width = 4u32;
let height = 2u32;
let desc = PixelDescriptor::RGB8_SRGB;
let bpp = desc.bytes_per_pixel();
let stride = width as usize * bpp;
let mut buffer = vec![0u8; stride * height as usize];
struct PreallocSink<'a> {
buf: &'a mut [u8],
expected_desc: PixelDescriptor,
expected_width: u32,
total_height: u32,
stride: usize,
}
impl DecodeRowSink for PreallocSink<'_> {
fn provide_next_buffer(
&mut self,
y: u32,
height: u32,
width: u32,
descriptor: PixelDescriptor,
) -> Result<PixelSliceMut<'_>, SinkError> {
if descriptor != self.expected_desc {
return Err(format!(
"format mismatch: expected {:?}, got {:?}",
self.expected_desc, descriptor
)
.into());
}
if width != self.expected_width {
return Err(format!(
"width mismatch: expected {}, got {}",
self.expected_width, width
)
.into());
}
if y + height > self.total_height {
return Err("strip exceeds buffer bounds".into());
}
let start = y as usize * self.stride;
let bpp = descriptor.bytes_per_pixel();
let row_bytes = width as usize * bpp;
let end = start + (height as usize - 1) * self.stride + row_bytes;
Ok(PixelSliceMut::new(
&mut self.buf[start..end],
width,
height,
self.stride,
descriptor,
)
.expect("valid slice"))
}
}
let mut sink = PreallocSink {
buf: &mut buffer,
expected_desc: desc,
expected_width: width,
total_height: height,
stride,
};
let dec_config = PnmDecoderConfig::new();
let job = dec_config.job();
let info = job
.push_decoder(Cow::Borrowed(encoded.data()), &mut sink, &[])
.expect("push_decoder");
assert_eq!(info.width, 4);
assert_eq!(info.height, 2);
assert_eq!(info.native_format, PixelDescriptor::RGB8_SRGB);
let orig = pixels.as_slice();
for y in 0..height {
let orig_row = orig.row(y);
let start = y as usize * stride;
let end = start + stride;
assert_eq!(&buffer[start..end], orig_row, "row {y} mismatch");
}
}
#[test]
fn sink_format_mismatch_rejected_at_begin() {
struct Rgba8OnlySink {
buf: Vec<u8>,
}
impl DecodeRowSink for Rgba8OnlySink {
fn begin(
&mut self,
_width: u32,
_height: u32,
descriptor: PixelDescriptor,
) -> Result<(), SinkError> {
if descriptor != PixelDescriptor::RGBA8_SRGB {
return Err(format!("sink requires RGBA8, got {:?}", descriptor).into());
}
Ok(())
}
fn provide_next_buffer(
&mut self,
_y: u32,
height: u32,
width: u32,
descriptor: PixelDescriptor,
) -> Result<PixelSliceMut<'_>, SinkError> {
let bpp = descriptor.bytes_per_pixel();
let stride = width as usize * bpp;
let needed = height as usize * stride;
self.buf.resize(needed, 0);
Ok(
PixelSliceMut::new(&mut self.buf, width, height, stride, descriptor)
.expect("valid slice"),
)
}
}
let mut sink = Rgba8OnlySink { buf: Vec::new() };
let result = sink.begin(4, 2, PixelDescriptor::RGB8_SRGB);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("RGBA8"), "{err_msg}");
let mut sink2 = Rgba8OnlySink { buf: Vec::new() };
sink2.begin(4, 2, PixelDescriptor::RGBA8_SRGB).unwrap();
}
#[test]
fn sink_row_processing_pipeline() {
let pixels = test_rgb8_pixels(); let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap();
struct ProcessingSink {
strip_buf: Vec<u8>,
row_sums: Vec<u32>,
pending_strip: Option<(u32, u32, u32)>, }
impl ProcessingSink {
fn process_pending(&mut self) {
if let Some((y, height, width)) = self.pending_strip.take() {
let bpp = 3; let stride = width as usize * bpp;
for row_idx in 0..height {
let row_start = row_idx as usize * stride;
let mut sum = 0u32;
for x in 0..width as usize {
sum += self.strip_buf[row_start + x * bpp] as u32;
}
self.row_sums.push(sum);
let _ = y;
}
}
}
}
impl DecodeRowSink for ProcessingSink {
fn provide_next_buffer(
&mut self,
y: u32,
height: u32,
width: u32,
descriptor: PixelDescriptor,
) -> Result<PixelSliceMut<'_>, SinkError> {
self.process_pending();
self.pending_strip = Some((y, height, width));
let bpp = descriptor.bytes_per_pixel();
let stride = width as usize * bpp;
let needed = height as usize * stride;
self.strip_buf.resize(needed, 0);
Ok(
PixelSliceMut::new(&mut self.strip_buf, width, height, stride, descriptor)
.expect("valid slice"),
)
}
fn finish(&mut self) -> Result<(), SinkError> {
self.process_pending();
Ok(())
}
}
let mut sink = ProcessingSink {
strip_buf: Vec::new(),
row_sums: Vec::new(),
pending_strip: None,
};
let dec_config = PnmDecoderConfig::new();
dec_config
.job()
.push_decoder(Cow::Borrowed(encoded.data()), &mut sink, &[])
.expect("push_decoder");
assert_eq!(sink.row_sums.len(), 2, "should have processed 2 rows");
assert_eq!(sink.row_sums[0], 510);
assert_eq!(sink.row_sums[1], 510);
}
#[test]
fn sink_completion_aware() {
let pixels = test_gray8_pixels(); let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap();
struct AccumulatingSink {
output: Option<PixelBuffer>,
rows_received: u32,
finished: bool,
}
impl DecodeRowSink for AccumulatingSink {
fn begin(
&mut self,
width: u32,
height: u32,
descriptor: PixelDescriptor,
) -> Result<(), SinkError> {
self.output = Some(PixelBuffer::new(width, height, descriptor));
Ok(())
}
fn provide_next_buffer(
&mut self,
y: u32,
height: u32,
width: u32,
descriptor: PixelDescriptor,
) -> Result<PixelSliceMut<'_>, SinkError> {
let output = self.output.as_mut().ok_or("begin() not called")?;
if descriptor != output.descriptor() {
return Err("format mismatch".into());
}
if width != output.width() {
return Err("width mismatch".into());
}
self.rows_received = y + height;
Ok(output.rows_mut(y, height))
}
fn finish(&mut self) -> Result<(), SinkError> {
self.finished = true;
Ok(())
}
}
let mut sink = AccumulatingSink {
output: None,
rows_received: 0,
finished: false,
};
let dec_config = PnmDecoderConfig::new();
dec_config
.job()
.push_decoder(Cow::Borrowed(encoded.data()), &mut sink, &[])
.expect("push_decoder");
assert!(sink.finished, "finish() should have been called");
assert_eq!(sink.rows_received, 2);
let output = sink.output.expect("begin() should have allocated");
let orig = pixels.as_slice();
let result = output.as_slice();
for y in 0..orig.rows() {
assert_eq!(orig.row(y), result.row(y), "row {y} mismatch");
}
}
#[test]
fn sink_simd_aligned_decode_into() {
let pixels = test_rgb8_pixels(); let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap();
struct AlignedDecodeIntoSink {
buf: Vec<u8>,
width: u32,
height: u32,
stride: usize,
desc: PixelDescriptor,
}
impl AlignedDecodeIntoSink {
fn new(width: u32, height: u32, desc: PixelDescriptor) -> Self {
let bpp = desc.bytes_per_pixel();
let row_bytes = width as usize * bpp;
let align = lcm(64, bpp);
let stride = row_bytes.div_ceil(align) * align;
let total = if height > 0 {
(height as usize - 1) * stride + row_bytes
} else {
0
};
Self {
buf: vec![0xAA; total], width,
height,
stride,
desc,
}
}
}
impl DecodeRowSink for AlignedDecodeIntoSink {
fn provide_next_buffer(
&mut self,
y: u32,
height: u32,
width: u32,
descriptor: PixelDescriptor,
) -> Result<PixelSliceMut<'_>, SinkError> {
if descriptor != self.desc || width != self.width {
return Err("format/width mismatch".into());
}
if y + height > self.height {
return Err("out of bounds".into());
}
let bpp = descriptor.bytes_per_pixel();
let row_bytes = width as usize * bpp;
let start = y as usize * self.stride;
let end = start + (height as usize - 1) * self.stride + row_bytes;
Ok(PixelSliceMut::new(
&mut self.buf[start..end],
width,
height,
self.stride,
descriptor,
)
.expect("valid"))
}
}
let dec_config = PnmDecoderConfig::new();
let job = dec_config.job();
let out_info = job.output_info(encoded.data()).unwrap();
let mut sink =
AlignedDecodeIntoSink::new(out_info.width, out_info.height, out_info.native_format);
let dec_config2 = PnmDecoderConfig::new();
dec_config2
.job()
.push_decoder(Cow::Borrowed(encoded.data()), &mut sink, &[])
.expect("push_decoder");
assert_eq!(sink.stride % 64, 0, "stride should be 64-byte aligned");
assert_eq!(
sink.stride % 3,
0,
"stride should be pixel-aligned for RGB8"
);
assert_eq!(sink.stride, 192);
let row_bytes = 4 * 3;
assert_eq!(row_bytes, 12);
let orig = pixels.as_slice();
for y in 0..2u32 {
let start = y as usize * sink.stride;
let orig_row = orig.row(y);
assert_eq!(
&sink.buf[start..start + row_bytes],
orig_row,
"row {y} pixel data mismatch"
);
}
}
#[test]
fn sink_through_dyn_dispatch() {
let pixels = test_rgb8_pixels();
let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap();
struct CollectSink {
buf: Vec<u8>,
desc: Option<PixelDescriptor>,
dimensions: Option<(u32, u32)>,
}
impl DecodeRowSink for CollectSink {
fn provide_next_buffer(
&mut self,
y: u32,
height: u32,
width: u32,
descriptor: PixelDescriptor,
) -> Result<PixelSliceMut<'_>, SinkError> {
if self.desc.is_none() {
self.desc = Some(descriptor);
}
self.dimensions = Some((width, y + height));
let bpp = descriptor.bytes_per_pixel();
let stride = width as usize * bpp;
let needed = height as usize * stride;
self.buf.resize(needed, 0);
Ok(
PixelSliceMut::new(&mut self.buf, width, height, stride, descriptor)
.expect("valid"),
)
}
}
let mut sink = CollectSink {
buf: Vec::new(),
desc: None,
dimensions: None,
};
let dec_config = PnmDecoderConfig::new();
dec_config
.job()
.push_decoder(
Cow::Borrowed(encoded.data()),
&mut sink as &mut dyn DecodeRowSink,
&[],
)
.expect("push_decoder");
assert_eq!(sink.desc, Some(PixelDescriptor::RGB8_SRGB));
assert_eq!(sink.dimensions, Some((4, 2)));
}
#[test]
fn sink_deferred_allocation() {
let pixels = test_gray8_pixels(); let config = PnmEncoderConfig::new();
let encoded = config
.job()
.encoder()
.unwrap()
.encode(pixels.as_slice())
.unwrap();
struct DeferredSink {
buf: Option<PixelBuffer>,
}
impl DecodeRowSink for DeferredSink {
fn begin(
&mut self,
width: u32,
height: u32,
descriptor: PixelDescriptor,
) -> Result<(), SinkError> {
self.buf = Some(PixelBuffer::new(width, height, descriptor));
Ok(())
}
fn provide_next_buffer(
&mut self,
y: u32,
height: u32,
_width: u32,
_descriptor: PixelDescriptor,
) -> Result<PixelSliceMut<'_>, SinkError> {
let buf = self.buf.as_mut().ok_or("begin() not called")?;
Ok(buf.rows_mut(y, height))
}
}
let mut sink = DeferredSink { buf: None };
let dec_config = PnmDecoderConfig::new();
dec_config
.job()
.push_decoder(Cow::Borrowed(encoded.data()), &mut sink, &[])
.expect("push_decoder");
let buf = sink.buf.expect("begin() should have allocated");
assert_eq!(buf.width(), 3);
assert_eq!(buf.height(), 2);
assert_eq!(buf.descriptor(), PixelDescriptor::GRAY8_SRGB);
let orig = pixels.as_slice();
let result = buf.as_slice();
for y in 0..orig.rows() {
assert_eq!(orig.row(y), result.row(y), "row {y} mismatch");
}
}
#[test]
fn sink_multi_strip_simulation() {
let width = 8u32;
let height = 24u32;
let desc = PixelDescriptor::RGB8_SRGB;
let bpp = desc.bytes_per_pixel();
let mut data = vec![0u8; width as usize * height as usize * bpp];
for y in 0..height {
for x in 0..width {
let idx = (y as usize * width as usize + x as usize) * bpp;
data[idx] = y as u8; data[idx + 1] = x as u8; data[idx + 2] = 128; }
}
let source = PixelBuffer::from_vec(data, width, height, desc).unwrap();
struct MultiStripSink {
output: PixelBuffer,
strips_received: Vec<(u32, u32)>, }
impl DecodeRowSink for MultiStripSink {
fn provide_next_buffer(
&mut self,
y: u32,
height: u32,
width: u32,
descriptor: PixelDescriptor,
) -> Result<PixelSliceMut<'_>, SinkError> {
if width != self.output.width() || descriptor != self.output.descriptor() {
return Err("mismatch".into());
}
self.strips_received.push((y, height));
Ok(self.output.rows_mut(y, height))
}
}
let mut sink = MultiStripSink {
output: PixelBuffer::new(width, height, desc),
strips_received: Vec::new(),
};
let strip_height = 8u32;
let src = source.as_slice();
for strip_y in (0..height).step_by(strip_height as usize) {
let h = strip_height.min(height - strip_y);
let mut dst = sink.provide_next_buffer(strip_y, h, width, desc).unwrap();
for row in 0..h {
dst.row_mut(row).copy_from_slice(src.row(strip_y + row));
}
}
assert_eq!(sink.strips_received.len(), 3);
assert_eq!(sink.strips_received[0], (0, 8));
assert_eq!(sink.strips_received[1], (8, 8));
assert_eq!(sink.strips_received[2], (16, 8));
let result = sink.output.as_slice();
for y in 0..height {
assert_eq!(src.row(y), result.row(y), "row {y} mismatch");
}
}
#[test]
fn sink_early_abort() {
let width = 4u32;
let height = 16u32;
let desc = PixelDescriptor::RGB8_SRGB;
let data = vec![128u8; width as usize * height as usize * 3];
let source = PixelBuffer::from_vec(data, width, height, desc).unwrap();
struct AbortAfterNSink {
buf: Vec<u8>,
strips_before_abort: u32,
strips_seen: u32,
}
impl DecodeRowSink for AbortAfterNSink {
fn provide_next_buffer(
&mut self,
_y: u32,
height: u32,
width: u32,
descriptor: PixelDescriptor,
) -> Result<PixelSliceMut<'_>, SinkError> {
self.strips_seen += 1;
if self.strips_seen > self.strips_before_abort {
return Err("abort: seen enough".into());
}
let bpp = descriptor.bytes_per_pixel();
let stride = width as usize * bpp;
let needed = height as usize * stride;
self.buf.resize(needed, 0);
Ok(
PixelSliceMut::new(&mut self.buf, width, height, stride, descriptor)
.expect("valid"),
)
}
}
let mut sink = AbortAfterNSink {
buf: Vec::new(),
strips_before_abort: 1,
strips_seen: 0,
};
let strip_h = 8u32;
let src = source.as_slice();
let mut aborted = false;
for strip_y in (0..height).step_by(strip_h as usize) {
let h = strip_h.min(height - strip_y);
match sink.provide_next_buffer(strip_y, h, width, desc) {
Ok(mut dst) => {
for row in 0..h {
dst.row_mut(row).copy_from_slice(src.row(strip_y + row));
}
}
Err(_) => {
aborted = true;
break;
}
}
}
assert!(aborted, "sink should have aborted");
assert_eq!(sink.strips_seen, 2); }