diff --git a/Cargo.toml b/Cargo.toml index 7aa286c2..99d0e4bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,3 +51,9 @@ path = "benches/unfilter.rs" name = "unfilter" harness = false required-features = ["benchmarks"] + +[[bench]] +path = "benches/expand_paletted.rs" +name = "expand_paletted" +harness = false +required-features = ["benchmarks"] \ No newline at end of file diff --git a/benches/expand_paletted.rs b/benches/expand_paletted.rs new file mode 100644 index 00000000..06294368 --- /dev/null +++ b/benches/expand_paletted.rs @@ -0,0 +1,155 @@ +//! Usage example: +//! +//! ``` +//! $ alias bench="rustup run nightly cargo bench" +//! $ bench --bench=expand_paletted --features=benchmarks -- --save-baseline my_baseline +//! ... tweak something ... +//! $ bench --bench=expand_paletted --features=benchmarks -- --baseline my_baseline +//! ``` + +use criterion::{criterion_group, criterion_main, Criterion, Throughput}; +use png::benchable_apis::{create_info_from_plte_trns_bitdepth, create_transform_fn, TransformFn}; +use png::{Info, Transformations}; +use rand::Rng; +use std::fmt::{self, Display}; + +#[derive(Clone, Copy)] +enum TrnsPresence { + Present, + Absent, +} + +impl Display for TrnsPresence { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TrnsPresence::Present => write!(f, "trns=yes"), + TrnsPresence::Absent => write!(f, "trns=no"), + } + } +} + +fn expand_paletted_all(c: &mut Criterion) { + let trns_options = [TrnsPresence::Absent, TrnsPresence::Present]; + let bit_depths = [4, 8]; + + let input_size = { + let typical_l1_cache_size = 32 * 1024; + let mut factor = 1; // input + factor += 4; // RGBA output + factor += 1; // other data + typical_l1_cache_size / factor + }; + + for trns in trns_options.iter().copied() { + for bit_depth in bit_depths.iter().copied() { + bench_expand_palette(c, trns, bit_depth, input_size); + } + } + + bench_create_fn(c, 256, 256); // Full PLTE and trNS + bench_create_fn(c, 224, 32); // Partial PLTE and trNS + bench_create_fn(c, 16, 1); // Guess: typical for small images? +} + +criterion_group!(benches, expand_paletted_all); +criterion_main!(benches); + +fn get_random_bytes(rng: &mut R, n: usize) -> Vec { + use rand::Fill; + let mut result = vec![0u8; n]; + result.as_mut_slice().try_fill(rng).unwrap(); + result +} + +struct Input { + palette: Vec, + trns: Option>, + src: Vec, + src_bit_depth: u8, +} + +impl Input { + fn new(trns: TrnsPresence, src_bit_depth: u8, input_size_in_bytes: usize) -> Self { + let mut rng = rand::thread_rng(); + + // We provide RGB entries for 192 out of 256 possible indices and Alpha/Transparency + // entries for 32 out of 256 possible indices. Rationale for these numbers: + // * Oftentimes only a handful of colors at the edges of an icon need transparency + // * In general, code needs to handle out-of-bounds indices, so it seems desirable + // to explicitly test this. + let palette = get_random_bytes(&mut rng, 192.min(input_size_in_bytes) * 3); + let trns = match trns { + TrnsPresence::Absent => None, + TrnsPresence::Present => Some(get_random_bytes(&mut rng, 32.min(input_size_in_bytes))), + }; + let src = get_random_bytes(&mut rng, input_size_in_bytes); + + Self { + palette, + trns, + src, + src_bit_depth, + } + } + + fn output_size_in_bytes(&self) -> usize { + let output_bytes_per_input_sample = match self.trns { + None => 3, + Some(_) => 4, + }; + let samples_count_per_byte = (8 / self.src_bit_depth) as usize; + let samples_count = self.src.len() * samples_count_per_byte; + samples_count * output_bytes_per_input_sample + } + + fn to_info(&self) -> Info { + create_info_from_plte_trns_bitdepth(&self.palette, self.trns.as_deref(), self.src_bit_depth) + } +} + +#[inline(always)] +fn create_expand_palette_fn(info: &Info) -> TransformFn { + create_transform_fn(info, Transformations::EXPAND).unwrap() +} + +fn bench_create_fn(c: &mut Criterion, plte_size: usize, trns_size: usize) { + let mut group = c.benchmark_group("expand_paletted(ctor)"); + group.sample_size(10000); + + let mut rng = rand::thread_rng(); + let plte = get_random_bytes(&mut rng, plte_size as usize); + let trns = get_random_bytes(&mut rng, trns_size as usize); + let info = create_info_from_plte_trns_bitdepth(&plte, Some(&trns), 8); + group.bench_with_input( + format!("plte={plte_size}/trns={trns_size:?}"), + &info, + |b, info| { + b.iter(|| create_expand_palette_fn(info)); + }, + ); +} + +fn bench_expand_palette( + c: &mut Criterion, + trns: TrnsPresence, + src_bit_depth: u8, + input_size_in_bytes: usize, +) { + let mut group = c.benchmark_group("expand_paletted(exec)"); + + let input = Input::new(trns, src_bit_depth, input_size_in_bytes); + let transform_fn = create_expand_palette_fn(&input.to_info()); + group.throughput(Throughput::Bytes(input.output_size_in_bytes() as u64)); + group.sample_size(500); + group.bench_with_input( + format!("{trns}/src_bits={src_bit_depth}/src_size={input_size_in_bytes}"), + &input, + |b, input| { + let mut output = vec![0; input.output_size_in_bytes()]; + let info = input.to_info(); + b.iter(|| { + transform_fn(input.src.as_slice(), output.as_mut_slice(), &info); + }); + }, + ); +} diff --git a/src/utils.rs b/src/adam7.rs similarity index 77% rename from src/utils.rs rename to src/adam7.rs index 0d2f0476..5bf7bb3f 100644 --- a/src/utils.rs +++ b/src/adam7.rs @@ -2,97 +2,6 @@ use std::iter::StepBy; use std::ops::Range; -#[inline(always)] -pub fn unpack_bits(input: &[u8], output: &mut [u8], channels: usize, bit_depth: u8, func: F) -where - F: Fn(u8, &mut [u8]), -{ - // Only [1, 2, 4, 8] are valid bit depths - assert!(matches!(bit_depth, 1 | 2 | 4 | 8)); - // Check that `input` is capable of producing a buffer as long as `output`: - // number of shift lookups per bit depth * channels * input length - assert!((8 / bit_depth as usize * channels).saturating_mul(input.len()) >= output.len()); - - let mut buf_chunks = output.chunks_exact_mut(channels); - let mut iter = input.iter(); - - // `shift` iterates through the corresponding bit depth sequence: - // 1 => &[7, 6, 5, 4, 3, 2, 1, 0], - // 2 => &[6, 4, 2, 0], - // 4 => &[4, 0], - // 8 => &[0], - // - // `(0..8).step_by(bit_depth.into()).rev()` doesn't always optimize well so - // shifts are calculated instead. (2023-08, Rust 1.71) - - if bit_depth == 8 { - for (&curr, chunk) in iter.zip(&mut buf_chunks) { - func(curr, chunk); - } - } else { - let mask = ((1u16 << bit_depth) - 1) as u8; - - // These variables are initialized in the loop - let mut shift = -1; - let mut curr = 0; - - for chunk in buf_chunks { - if shift < 0 { - shift = 8 - bit_depth as i32; - curr = *iter.next().expect("input for unpack bits is not empty"); - } - - let pixel = (curr >> shift) & mask; - func(pixel, chunk); - - shift -= bit_depth as i32; - } - } -} - -pub fn expand_trns_line(input: &[u8], output: &mut [u8], trns: Option<&[u8]>, channels: usize) { - for (input, output) in input - .chunks_exact(channels) - .zip(output.chunks_exact_mut(channels + 1)) - { - output[..channels].copy_from_slice(input); - output[channels] = if Some(input) == trns { 0 } else { 0xFF }; - } -} - -pub fn expand_trns_line16(input: &[u8], output: &mut [u8], trns: Option<&[u8]>, channels: usize) { - for (input, output) in input - .chunks_exact(channels * 2) - .zip(output.chunks_exact_mut(channels * 2 + 2)) - { - output[..channels * 2].copy_from_slice(input); - if Some(input) == trns { - output[channels * 2] = 0; - output[channels * 2 + 1] = 0 - } else { - output[channels * 2] = 0xFF; - output[channels * 2 + 1] = 0xFF - }; - } -} - -pub fn expand_trns_and_strip_line16( - input: &[u8], - output: &mut [u8], - trns: Option<&[u8]>, - channels: usize, -) { - for (input, output) in input - .chunks_exact(channels * 2) - .zip(output.chunks_exact_mut(channels + 1)) - { - for i in 0..channels { - output[i] = input[i * 2]; - } - output[channels] = if Some(input) == trns { 0 } else { 0xFF }; - } -} - /// This iterator iterates over the different passes of an image Adam7 encoded /// PNG image /// The pattern is: diff --git a/src/benchable_apis.rs b/src/benchable_apis.rs index 0be8134f..17b0b0d6 100644 --- a/src/benchable_apis.rs +++ b/src/benchable_apis.rs @@ -3,6 +3,7 @@ use crate::common::BytesPerPixel; use crate::filter::FilterType; +use crate::{BitDepth, ColorType, Info}; /// Re-exporting `unfilter` to make it easier to benchmark, despite some items being only /// `pub(crate)`: `fn unfilter`, `enum BytesPerPixel`. @@ -10,3 +11,19 @@ pub fn unfilter(filter: FilterType, tbpp: u8, previous: &[u8], current: &mut [u8 let tbpp = BytesPerPixel::from_usize(tbpp as usize); crate::filter::unfilter(filter, tbpp, previous, current) } + +pub use crate::decoder::transform::{create_transform_fn, TransformFn}; + +pub fn create_info_from_plte_trns_bitdepth<'a>( + plte: &'a [u8], + trns: Option<&'a [u8]>, + bit_depth: u8, +) -> Info<'a> { + Info { + color_type: ColorType::Indexed, + bit_depth: BitDepth::from_u8(bit_depth).unwrap(), + palette: Some(plte.into()), + trns: trns.map(Into::into), + ..Info::default() + } +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 3d7c89c0..21d51a66 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -1,19 +1,21 @@ mod stream; +pub(crate) mod transform; mod zlib; pub use self::stream::{DecodeOptions, Decoded, DecodingError, StreamingDecoder}; use self::stream::{FormatErrorInner, CHUNCK_BUFFER_SIZE}; +use self::transform::{create_transform_fn, TransformFn}; use std::io::{BufRead, BufReader, Read}; use std::mem; use std::ops::Range; +use crate::adam7; use crate::chunk; use crate::common::{ BitDepth, BytesPerPixel, ColorType, Info, ParameterErrorKind, Transformations, }; use crate::filter::{unfilter, FilterType}; -use crate::utils; /* pub enum InterlaceHandling { @@ -227,6 +229,7 @@ impl Decoder { prev_start: 0, current_start: 0, transform: self.transform, + transform_fn: None, scratch_buffer: Vec::new(), }; @@ -367,6 +370,9 @@ pub struct Reader { current_start: usize, /// Output transformations transform: Transformations, + /// Function that can transform decompressed, unfiltered rows into final output. + /// See the `transform.rs` module for more details. + transform_fn: Option, /// This buffer is only used so that `next_row` and `next_interlaced_row` can return reference /// to a byte slice. In a future version of this library, this buffer will be removed and /// `next_row` and `next_interlaced_row` will write directly into a user provided output buffer. @@ -390,7 +396,7 @@ struct SubframeInfo { #[derive(Clone)] enum InterlaceIter { None(Range), - Adam7(utils::Adam7Iterator), + Adam7(adam7::Adam7Iterator), } /// Denote a frame as given by sequence numbers. @@ -527,7 +533,7 @@ impl Reader { InterlaceInfo::Null => unreachable!("expected interlace information"), }; let samples = color_type.samples() as u8; - utils::expand_pass(buf, width, row, pass, line, samples * (bit_depth as u8)); + adam7::expand_pass(buf, width, row, pass, line, samples * (bit_depth as u8)); } } else { for row in buf @@ -628,51 +634,13 @@ impl Reader { let row = &self.data_stream[self.prev_start..self.current_start]; // Apply transformations and write resulting data to buffer. - let (color_type, bit_depth, trns) = { - let info = self.info(); - ( - info.color_type, - info.bit_depth as u8, - info.trns.is_some() || self.transform.contains(Transformations::ALPHA), - ) - }; - let expand = self.transform.contains(Transformations::EXPAND) - || self.transform.contains(Transformations::ALPHA); - let strip16 = bit_depth == 16 && self.transform.contains(Transformations::STRIP_16); - let info = self.decoder.info().unwrap(); - let trns = if trns { - Some(info.trns.as_deref()) - } else { - None - }; - match (color_type, trns) { - (ColorType::Indexed, _) if expand => { - expand_paletted(row, output_buffer, info, trns)?; - } - (ColorType::Grayscale | ColorType::GrayscaleAlpha, _) if bit_depth < 8 && expand => { - expand_gray_u8(row, output_buffer, info, trns) - } - (ColorType::Grayscale | ColorType::Rgb, Some(trns)) if expand => { - let channels = color_type.samples(); - if bit_depth == 8 { - utils::expand_trns_line(row, output_buffer, trns, channels); - } else if strip16 { - utils::expand_trns_and_strip_line16(row, output_buffer, trns, channels); - } else { - assert_eq!(bit_depth, 16); - utils::expand_trns_line16(row, output_buffer, trns, channels); - } - } - ( - ColorType::Grayscale | ColorType::GrayscaleAlpha | ColorType::Rgb | ColorType::Rgba, - _, - ) if strip16 => { - for i in 0..row.len() / 2 { - output_buffer[i] = row[2 * i]; - } + let transform_fn = { + if self.transform_fn.is_none() { + self.transform_fn = Some(create_transform_fn(self.info(), self.transform)?); } - _ => output_buffer.copy_from_slice(row), - } + self.transform_fn.unwrap() + }; + transform_fn(row, output_buffer, self.info()); Ok(()) } @@ -827,7 +795,7 @@ impl SubframeInfo { }; let interlace = if info.interlaced { - InterlaceIter::Adam7(utils::Adam7Iterator::new(width, height)) + InterlaceIter::Adam7(adam7::Adam7Iterator::new(width, height)) } else { InterlaceIter::None(0..height) }; @@ -841,94 +809,3 @@ impl SubframeInfo { } } } - -fn expand_paletted( - row: &[u8], - buffer: &mut [u8], - info: &Info, - trns: Option>, -) -> Result<(), DecodingError> { - if let Some(palette) = info.palette.as_ref() { - if let BitDepth::Sixteen = info.bit_depth { - // This should have been caught earlier but let's check again. Can't hurt. - Err(DecodingError::Format( - FormatErrorInner::InvalidColorBitDepth { - color_type: ColorType::Indexed, - bit_depth: BitDepth::Sixteen, - } - .into(), - )) - } else { - let black = [0, 0, 0]; - if let Some(trns) = trns { - let trns = trns.unwrap_or(&[]); - // > The tRNS chunk shall not contain more alpha values than there are palette - // entries, but a tRNS chunk may contain fewer values than there are palette - // entries. In this case, the alpha value for all remaining palette entries is - // assumed to be 255. - // - // It seems, accepted reading is to fully *ignore* an invalid tRNS as if it were - // completely empty / all pixels are non-transparent. - let trns = if trns.len() <= palette.len() / 3 { - trns - } else { - &[] - }; - - utils::unpack_bits(row, buffer, 4, info.bit_depth as u8, |i, chunk| { - let (rgb, a) = ( - palette - .get(3 * i as usize..3 * i as usize + 3) - .unwrap_or(&black), - *trns.get(i as usize).unwrap_or(&0xFF), - ); - chunk[0] = rgb[0]; - chunk[1] = rgb[1]; - chunk[2] = rgb[2]; - chunk[3] = a; - }); - } else { - utils::unpack_bits(row, buffer, 3, info.bit_depth as u8, |i, chunk| { - let rgb = palette - .get(3 * i as usize..3 * i as usize + 3) - .unwrap_or(&black); - chunk[0] = rgb[0]; - chunk[1] = rgb[1]; - chunk[2] = rgb[2]; - }) - } - Ok(()) - } - } else { - Err(DecodingError::Format( - FormatErrorInner::PaletteRequired.into(), - )) - } -} - -fn expand_gray_u8(row: &[u8], buffer: &mut [u8], info: &Info, trns: Option>) { - let rescale = true; - let scaling_factor = if rescale { - (255) / ((1u16 << info.bit_depth as u8) - 1) as u8 - } else { - 1 - }; - if let Some(trns) = trns { - utils::unpack_bits(row, buffer, 2, info.bit_depth as u8, |pixel, chunk| { - chunk[1] = if let Some(trns) = trns { - if pixel == trns[0] { - 0 - } else { - 0xFF - } - } else { - 0xFF - }; - chunk[0] = pixel * scaling_factor - }) - } else { - utils::unpack_bits(row, buffer, 1, info.bit_depth as u8, |val, chunk| { - chunk[0] = val * scaling_factor - }) - } -} diff --git a/src/decoder/transform.rs b/src/decoder/transform.rs new file mode 100644 index 00000000..0cbc0740 --- /dev/null +++ b/src/decoder/transform.rs @@ -0,0 +1,390 @@ +//! Transforming a decompressed, unfiltered row into the final output. + +use crate::{BitDepth, ColorType, DecodingError, Info, Transformations}; + +use super::stream::FormatErrorInner; + +/// Type of a function that can transform a decompressed, unfiltered row (the +/// 1st argument) into the final pixels (the 2nd argument), optionally using +/// image metadata (e.g. PLTE data can be accessed using the 3rd argument). +/// +/// TODO: If some precomputed state is needed (e.g. to make `expand_paletted...` +/// faster) then consider changing this into `Box`. +pub type TransformFn = fn(&[u8], &mut [u8], &Info); + +/// Returns a transformation function that should be applied to image rows based +/// on 1) decoded image metadata (`info`) and 2) the transformations requested +/// by the crate client (`transform`). +pub fn create_transform_fn( + info: &Info, + transform: Transformations, +) -> Result { + let color_type = info.color_type; + let bit_depth = info.bit_depth as u8; + let trns = info.trns.is_some() || transform.contains(Transformations::ALPHA); + let expand = + transform.contains(Transformations::EXPAND) || transform.contains(Transformations::ALPHA); + let strip16 = bit_depth == 16 && transform.contains(Transformations::STRIP_16); + match color_type { + ColorType::Indexed if expand => { + if info.palette.is_none() { + return Err(DecodingError::Format( + FormatErrorInner::PaletteRequired.into(), + )); + } else if let BitDepth::Sixteen = info.bit_depth { + // This should have been caught earlier but let's check again. Can't hurt. + return Err(DecodingError::Format( + FormatErrorInner::InvalidColorBitDepth { + color_type: ColorType::Indexed, + bit_depth: BitDepth::Sixteen, + } + .into(), + )); + } else { + if trns { + Ok(expand_paletted_into_rgba8) + } else { + Ok(expand_paletted_into_rgb8) + } + } + } + ColorType::Grayscale | ColorType::GrayscaleAlpha if bit_depth < 8 && expand => { + if trns { + Ok(expand_gray_u8_with_trns) + } else { + Ok(expand_gray_u8) + } + } + ColorType::Grayscale | ColorType::Rgb if expand && trns => { + if bit_depth == 8 { + Ok(expand_trns_line) + } else if strip16 { + Ok(expand_trns_and_strip_line16) + } else { + assert_eq!(bit_depth, 16); + Ok(expand_trns_line16) + } + } + ColorType::Grayscale | ColorType::GrayscaleAlpha | ColorType::Rgb | ColorType::Rgba + if strip16 => + { + Ok(transform_row_strip16) + } + _ => Ok(copy_row), + } +} + +fn copy_row(row: &[u8], output_buffer: &mut [u8], _: &Info) { + output_buffer.copy_from_slice(row); +} + +fn transform_row_strip16(row: &[u8], output_buffer: &mut [u8], _: &Info) { + for i in 0..row.len() / 2 { + output_buffer[i] = row[2 * i]; + } +} + +#[inline(always)] +fn unpack_bits(input: &[u8], output: &mut [u8], channels: usize, bit_depth: u8, func: F) +where + F: Fn(u8, &mut [u8]), +{ + // Only [1, 2, 4, 8] are valid bit depths + assert!(matches!(bit_depth, 1 | 2 | 4 | 8)); + // Check that `input` is capable of producing a buffer as long as `output`: + // number of shift lookups per bit depth * channels * input length + assert!((8 / bit_depth as usize * channels).saturating_mul(input.len()) >= output.len()); + + let mut buf_chunks = output.chunks_exact_mut(channels); + let mut iter = input.iter(); + + // `shift` iterates through the corresponding bit depth sequence: + // 1 => &[7, 6, 5, 4, 3, 2, 1, 0], + // 2 => &[6, 4, 2, 0], + // 4 => &[4, 0], + // 8 => &[0], + // + // `(0..8).step_by(bit_depth.into()).rev()` doesn't always optimize well so + // shifts are calculated instead. (2023-08, Rust 1.71) + + if bit_depth == 8 { + for (&curr, chunk) in iter.zip(&mut buf_chunks) { + func(curr, chunk); + } + } else { + let mask = ((1u16 << bit_depth) - 1) as u8; + + // These variables are initialized in the loop + let mut shift = -1; + let mut curr = 0; + + for chunk in buf_chunks { + if shift < 0 { + shift = 8 - bit_depth as i32; + curr = *iter.next().expect("input for unpack bits is not empty"); + } + + let pixel = (curr >> shift) & mask; + func(pixel, chunk); + + shift -= bit_depth as i32; + } + } +} + +pub fn expand_trns_line(input: &[u8], output: &mut [u8], info: &Info) { + let channels = info.color_type.samples(); + let trns = info.trns.as_deref(); + for (input, output) in input + .chunks_exact(channels) + .zip(output.chunks_exact_mut(channels + 1)) + { + output[..channels].copy_from_slice(input); + output[channels] = if Some(input) == trns { 0 } else { 0xFF }; + } +} + +pub fn expand_trns_line16(input: &[u8], output: &mut [u8], info: &Info) { + let channels = info.color_type.samples(); + let trns = info.trns.as_deref(); + for (input, output) in input + .chunks_exact(channels * 2) + .zip(output.chunks_exact_mut(channels * 2 + 2)) + { + output[..channels * 2].copy_from_slice(input); + if Some(input) == trns { + output[channels * 2] = 0; + output[channels * 2 + 1] = 0 + } else { + output[channels * 2] = 0xFF; + output[channels * 2 + 1] = 0xFF + }; + } +} + +pub fn expand_trns_and_strip_line16(input: &[u8], output: &mut [u8], info: &Info) { + let channels = info.color_type.samples(); + let trns = info.trns.as_deref(); + for (input, output) in input + .chunks_exact(channels * 2) + .zip(output.chunks_exact_mut(channels + 1)) + { + for i in 0..channels { + output[i] = input[i * 2]; + } + output[channels] = if Some(input) == trns { 0 } else { 0xFF }; + } +} + +pub fn expand_paletted_into_rgb8(row: &[u8], buffer: &mut [u8], info: &Info) { + let palette = info.palette.as_deref().expect("Caller should verify"); + let black = [0, 0, 0]; + + unpack_bits(row, buffer, 3, info.bit_depth as u8, |i, chunk| { + let rgb = palette + .get(3 * i as usize..3 * i as usize + 3) + .unwrap_or(&black); + chunk[0] = rgb[0]; + chunk[1] = rgb[1]; + chunk[2] = rgb[2]; + }) +} + +pub fn expand_paletted_into_rgba8(row: &[u8], buffer: &mut [u8], info: &Info) { + let palette = info.palette.as_deref().expect("Caller should verify"); + let trns = info.trns.as_deref().unwrap_or(&[]); + let black = [0, 0, 0]; + + // > The tRNS chunk shall not contain more alpha values than there are palette + // entries, but a tRNS chunk may contain fewer values than there are palette + // entries. In this case, the alpha value for all remaining palette entries is + // assumed to be 255. + // + // It seems, accepted reading is to fully *ignore* an invalid tRNS as if it were + // completely empty / all pixels are non-transparent. + let trns = if trns.len() <= palette.len() / 3 { + trns + } else { + &[] + }; + + unpack_bits(row, buffer, 4, info.bit_depth as u8, |i, chunk| { + let (rgb, a) = ( + palette + .get(3 * i as usize..3 * i as usize + 3) + .unwrap_or(&black), + *trns.get(i as usize).unwrap_or(&0xFF), + ); + chunk[0] = rgb[0]; + chunk[1] = rgb[1]; + chunk[2] = rgb[2]; + chunk[3] = a; + }); +} + +pub fn expand_gray_u8(row: &[u8], buffer: &mut [u8], info: &Info) { + let scaling_factor = (255) / ((1u16 << info.bit_depth as u8) - 1) as u8; + unpack_bits(row, buffer, 1, info.bit_depth as u8, |val, chunk| { + chunk[0] = val * scaling_factor + }); +} + +pub fn expand_gray_u8_with_trns(row: &[u8], buffer: &mut [u8], info: &Info) { + let scaling_factor = (255) / ((1u16 << info.bit_depth as u8) - 1) as u8; + let trns = info.trns.as_deref(); + unpack_bits(row, buffer, 2, info.bit_depth as u8, |pixel, chunk| { + chunk[1] = if let Some(trns) = trns { + if pixel == trns[0] { + 0 + } else { + 0xFF + } + } else { + 0xFF + }; + chunk[0] = pixel * scaling_factor + }); +} + +#[cfg(test)] +mod test { + use crate::{BitDepth, ColorType, Info, Transformations}; + + fn expand_paletted( + src: &[u8], + src_bit_depth: u8, + palette: &[u8], + trns: Option<&[u8]>, + ) -> Vec { + let info = Info { + color_type: ColorType::Indexed, + bit_depth: BitDepth::from_u8(src_bit_depth).unwrap(), + palette: Some(palette.into()), + trns: trns.map(Into::into), + ..Info::default() + }; + let output_bytes_per_input_sample = match trns { + None => 3, + Some(_) => 4, + }; + let samples_count_per_byte = (8 / src_bit_depth) as usize; + let samples_count = src.len() * samples_count_per_byte; + let mut dst = vec![0; samples_count * output_bytes_per_input_sample]; + let transform_fn = super::create_transform_fn(&info, Transformations::EXPAND).unwrap(); + transform_fn(src, dst.as_mut_slice(), &info); + dst + } + + #[test] + fn test_expand_paletted_rgba_8bit() { + let actual = expand_paletted( + &[0, 1, 2, 3], // src + 8, // src_bit_depth + &[ + // palette + 0, 1, 2, // entry #0 + 4, 5, 6, // entry #1 + 8, 9, 10, // entry #2 + 12, 13, 14, // entry #3 + ], + Some(&[3, 7, 11, 15]), // trns + ); + assert_eq!(actual, (0..16).collect::>()); + } + + #[test] + fn test_expand_paletted_rgb_8bit() { + let actual = expand_paletted( + &[0, 1, 2, 3], // src + 8, // src_bit_depth + &[ + // palette + 0, 1, 2, // entry #0 + 3, 4, 5, // entry #1 + 6, 7, 8, // entry #2 + 9, 10, 11, // entry #3 + ], + None, // trns + ); + assert_eq!(actual, (0..12).collect::>()); + } + + #[test] + fn test_expand_paletted_rgba_4bit() { + let actual = expand_paletted( + &[0x01, 0x23], // src + 4, // src_bit_depth + &[ + // palette + 0, 1, 2, // entry #0 + 4, 5, 6, // entry #1 + 8, 9, 10, // entry #2 + 12, 13, 14, // entry #3 + ], + Some(&[3, 7, 11, 15]), // trns + ); + assert_eq!(actual, (0..16).collect::>()); + } + + #[test] + fn test_expand_paletted_rgb_4bit() { + let actual = expand_paletted( + &[0x01, 0x23], // src + 4, // src_bit_depth + &[ + // palette + 0, 1, 2, // entry #0 + 3, 4, 5, // entry #1 + 6, 7, 8, // entry #2 + 9, 10, 11, // entry #3 + ], + None, // trns + ); + assert_eq!(actual, (0..12).collect::>()); + } + + #[test] + fn test_expand_paletted_rgba_8bit_more_trns_entries_than_palette_entries() { + let actual = expand_paletted( + &[0, 1, 2, 3], // src + 8, // src_bit_depth + &[ + // palette + 0, 1, 2, // entry #0 + 4, 5, 6, // entry #1 + 8, 9, 10, // entry #2 + 12, 13, 14, // entry #3 + ], + Some(&[123; 5]), // trns + ); + + // Invalid (too-long) `trns` means that we'll use 0xFF / opaque alpha everywhere. + assert_eq!( + actual, + vec![0, 1, 2, 0xFF, 4, 5, 6, 0xFF, 8, 9, 10, 0xFF, 12, 13, 14, 0xFF], + ); + } + + #[test] + fn test_expand_paletted_rgba_8bit_less_trns_entries_than_palette_entries() { + let actual = expand_paletted( + &[0, 1, 2, 3], // src + 8, // src_bit_depth + &[ + // palette + 0, 1, 2, // entry #0 + 4, 5, 6, // entry #1 + 8, 9, 10, // entry #2 + 12, 13, 14, // entry #3 + ], + Some(&[3, 7]), // trns + ); + + // Too-short `trns` is treated differently from too-long - only missing entries are + // replaced with 0XFF / opaque. + assert_eq!( + actual, + vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0xFF, 12, 13, 14, 0xFF], + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 26420d99..b7e151a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,6 +64,7 @@ #[macro_use] extern crate bitflags; +mod adam7; pub mod chunk; mod common; mod decoder; @@ -72,7 +73,6 @@ mod filter; mod srgb; pub mod text_metadata; mod traits; -mod utils; pub use crate::common::*; pub use crate::decoder::{