diff --git a/crates/pdf-decode/src/indexed.rs b/crates/pdf-decode/src/indexed.rs index b3525afe..c8ea61bf 100644 --- a/crates/pdf-decode/src/indexed.rs +++ b/crates/pdf-decode/src/indexed.rs @@ -2,7 +2,18 @@ use crate::error::DecodeError; -/// Expands indexed palette values into their base component bytes. +/// Expands indexed palette values into their base color component bytes. +/// +/// Each value in `indices` selects one `base_components`-wide entry from +/// `lookup`. Indices above `hival` are clamped to `hival`, as required for PDF +/// indexed color spaces. The returned bytes contain the selected entries in +/// the same order as the input indices. +/// +/// # Errors +/// +/// Returns [`DecodeError::InvalidComponentCount`] when `base_components` is +/// zero. Returns [`DecodeError::PaletteLookupOutOfBounds`] when `lookup` does +/// not contain a complete entry for a selected index. pub fn expand_indexed_values( indices: &[u8], lookup: &[u8], diff --git a/crates/pdf-decode/src/samples.rs b/crates/pdf-decode/src/samples.rs index 42fe97c4..e895ef70 100644 --- a/crates/pdf-decode/src/samples.rs +++ b/crates/pdf-decode/src/samples.rs @@ -1,5 +1,7 @@ //! Sample code unpacking and normalization helpers. +use std::borrow::Cow; + use num_traits::ToPrimitive; use pdf_utils::BitReader; @@ -49,13 +51,14 @@ pub fn decode_sample_codes( /// Decodes packed sample codes into byte-sized sample values. /// -/// Eight-bit samples use a direct copy path. Wider sample codes are decoded -/// normally and return an error when a value cannot fit in a byte. -pub fn decode_sample_bytes( - data: &[u8], +/// Eight-bit samples borrow directly from the input. Other sample sizes are +/// decoded into an owned buffer and return an error when a value cannot fit in +/// a byte. +pub fn decode_sample_bytes<'a>( + data: &'a [u8], bits_per_sample: usize, layout: SampleLayout, -) -> Result, DecodeError> { +) -> Result, DecodeError> { validate_bits_per_sample(bits_per_sample)?; if bits_per_sample == 8 { @@ -75,13 +78,14 @@ pub fn decode_sample_bytes( expected_bytes: sample_count, actual_bytes: data.len(), })?; - return Ok(samples.to_vec()); + return Ok(Cow::Borrowed(samples)); } - decode_sample_codes(data, bits_per_sample, layout)? + let samples = decode_sample_codes(data, bits_per_sample, layout)? .into_iter() .map(|sample| u8::try_from(sample).map_err(|_| DecodeError::InvalidSampleData)) - .collect() + .collect::, _>>()?; + Ok(Cow::Owned(samples)) } /// Decodes packed sample codes and normalizes them to the `0.0..=1.0` range. @@ -368,14 +372,12 @@ mod tests { #[test] fn decode_contiguous_8_bit_sample_bytes() { - let samples = decode_sample_bytes( - &[0x12, 0x34, 0x56, 0x78], - 8, - SampleLayout::Contiguous { sample_count: 3 }, - ) - .unwrap(); + let data = [0x12, 0x34, 0x56, 0x78]; + let samples = + decode_sample_bytes(&data, 8, SampleLayout::Contiguous { sample_count: 3 }).unwrap(); - assert_eq!(samples, vec![0x12, 0x34, 0x56]); + assert!(matches!(samples, Cow::Borrowed(_))); + assert_eq!(samples.as_ref(), &data[..3]); } #[test] @@ -391,7 +393,8 @@ mod tests { ) .unwrap(); - assert_eq!(samples, vec![0x10, 0x20, 0x30, 0x40]); + assert!(matches!(samples, Cow::Borrowed(_))); + assert_eq!(samples.as_ref(), &[0x10, 0x20, 0x30, 0x40]); } #[test] @@ -407,7 +410,8 @@ mod tests { ) .unwrap(); - assert_eq!(samples, vec![1, 0, 1, 0, 1, 1]); + assert!(matches!(samples, Cow::Owned(_))); + assert_eq!(samples.as_ref(), &[1, 0, 1, 0, 1, 1]); } #[test] diff --git a/crates/pdf-image/src/image_xobject.rs b/crates/pdf-image/src/image_xobject.rs index 23ae1b09..da95bc5f 100644 --- a/crates/pdf-image/src/image_xobject.rs +++ b/crates/pdf-image/src/image_xobject.rs @@ -1,7 +1,7 @@ -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; use pdf_color_space::{color_space::ColorSpace, indexed_color_space::IndexedColorSpace}; -use pdf_decode::{DecodeMap, SampleLayout, decode_sample_bytes}; +use pdf_decode::{DecodeMap, SampleLayout, decode_sample_bytes, expand_indexed_values}; use pdf_filter::filter::{Filter, decode_data_with_resolver, decode_with_resolver}; use pdf_graphics::PixelFormat; use pdf_object::{ @@ -11,7 +11,6 @@ use pdf_object::{ use crate::InlineImage; use crate::error::PdfImageError; -use crate::indexed::expand_indexed_values_to_components; /// Represents a PDF Image XObject, which is a self-contained raster image. #[derive(Debug, Clone)] @@ -329,9 +328,10 @@ impl ImageXObject { let sample_codes = Self::decode_image_sample_codes(raw_data, 1, metadata)?; let sample_max = Self::sample_max(metadata.bits_per_component)?; let decode = DecodeMap::from_dictionary(dictionary, objects, 1, metadata.image_mask)?; - let decoded_indices = decode.apply_to_bytes(&sample_codes, sample_max, sample_max); + let decoded_indices = decode.apply_to_bytes(sample_codes.as_ref(), sample_max, sample_max); let base_components = indexed.base.num_color_components(); - let image_data = expand_indexed_values_to_components( + + let image_data = expand_indexed_values( &decoded_indices, &indexed.lookup, indexed.hival, @@ -366,18 +366,18 @@ impl ImageXObject { stored_color_space: metadata.color_space.clone(), num_color_components: num_components, image_data: decode.apply_to_bytes( - &sample_codes, + sample_codes.as_ref(), Self::sample_max(metadata.bits_per_component)?, 255, ), }) } - fn decode_image_sample_codes( - raw_data: &[u8], + fn decode_image_sample_codes<'a>( + raw_data: &'a [u8], samples_per_pixel: usize, metadata: &ImageMetadata, - ) -> Result, PdfImageError> { + ) -> Result, PdfImageError> { Ok(decode_sample_bytes( raw_data, metadata.bits_per_component, diff --git a/crates/pdf-image/src/indexed.rs b/crates/pdf-image/src/indexed.rs deleted file mode 100644 index fd238520..00000000 --- a/crates/pdf-image/src/indexed.rs +++ /dev/null @@ -1,168 +0,0 @@ -//! Utilities for expanding indexed image data. - -use pdf_decode::{SampleLayout, decode_sample_bytes, expand_indexed_values}; - -use crate::PdfImageError; - -/// Number of color components in RGB color space. -const RGB_COMPONENTS: usize = 3; - -fn decode_indices( - indexed_data: &[u8], - width: usize, - height: usize, - bits_per_component: usize, -) -> Result, PdfImageError> { - Ok(decode_sample_bytes( - indexed_data, - bits_per_component, - SampleLayout::RowAligned { - width, - height, - samples_per_pixel: 1, - }, - )?) -} - -/// Expands indexed color values into palette component bytes. -pub(crate) fn expand_indexed_values_to_components( - indexed_values: &[u8], - lookup: &[u8], - hival: u8, - base_components: usize, -) -> Result, PdfImageError> { - Ok(expand_indexed_values( - indexed_values, - lookup, - hival, - base_components, - )?) -} - -/// Expands indexed color image data to an arbitrary component count. -pub fn expand_indexed_to_components( - indexed_data: &[u8], - lookup: &[u8], - width: usize, - height: usize, - bits_per_component: usize, - hival: u8, - base_components: usize, -) -> Result, PdfImageError> { - let indices = decode_indices(indexed_data, width, height, bits_per_component)?; - expand_indexed_values_to_components(&indices, lookup, hival, base_components) -} - -/// Expands indexed color image data to RGB format. -pub fn expand_indexed_to_rgb( - indexed_data: &[u8], - lookup: &[u8], - width: usize, - height: usize, - bits_per_component: usize, - hival: u8, -) -> Result, PdfImageError> { - expand_indexed_to_components( - indexed_data, - lookup, - width, - height, - bits_per_component, - hival, - RGB_COMPONENTS, - ) -} - -#[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used)] -mod tests { - use super::*; - - #[test] - fn test_expand_indexed_8bit() { - let width = 2usize; - let height = 2usize; - let indexed_data = vec![0u8, 1u8, 2u8, 1u8]; - let lookup = vec![10u8, 11u8, 12u8, 20u8, 21u8, 22u8, 30u8, 31u8, 32u8]; - - let out = expand_indexed_to_rgb(&indexed_data, &lookup, width, height, 8, 2).unwrap(); - let expected = vec![10, 11, 12, 20, 21, 22, 30, 31, 32, 20, 21, 22]; - assert_eq!(out, expected); - } - - #[test] - fn test_expand_indexed_4bit() { - let width = 4usize; - let height = 1usize; - let indexed_data = vec![0x01u8, 0x23u8]; - let lookup = vec![ - 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8, 9u8, 10u8, 11u8, 12u8, - ]; - - let out = expand_indexed_to_rgb(&indexed_data, &lookup, width, height, 4, 3).unwrap(); - let expected = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; - assert_eq!(out, expected); - } - - #[test] - fn test_expand_indexed_2bit() { - let width = 4usize; - let height = 1usize; - let indexed_data = vec![0x1Bu8]; - let lookup = vec![ - 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8, 9u8, 10u8, 11u8, 12u8, - ]; - - let out = expand_indexed_to_rgb(&indexed_data, &lookup, width, height, 2, 3).unwrap(); - let expected = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; - assert_eq!(out, expected); - } - - #[test] - fn test_expand_indexed_1bit() { - let width = 4usize; - let height = 2usize; - let indexed_data = vec![0b1010_0000u8, 0b0110_0000u8]; - let lookup = vec![11u8, 22u8, 33u8, 44u8, 55u8, 66u8]; - - let out = - expand_indexed_to_components(&indexed_data, &lookup, width, height, 1, 1, 3).unwrap(); - - assert_eq!( - out, - vec![ - 44, 55, 66, 11, 22, 33, 44, 55, 66, 11, 22, 33, 11, 22, 33, 44, 55, 66, 44, 55, 66, - 11, 22, 33 - ] - ); - } - - #[test] - fn test_expand_indexed_hival_clamp() { - let out = expand_indexed_to_rgb(&[2u8], &[10, 11, 12, 20, 21, 22], 1, 1, 8, 1).unwrap(); - - assert_eq!(out, vec![20, 21, 22]); - } - - #[test] - fn test_expand_indexed_insufficient_data() { - let err = expand_indexed_to_rgb(&[0u8, 1u8, 2u8], &[10, 11, 12], 4, 1, 8, 1) - .expect_err("indexed sample buffer is truncated"); - - assert!(matches!( - err, - PdfImageError::TruncatedImageData { - expected_bytes: 4, - actual_bytes: 3 - } - )); - } - - #[test] - fn test_expand_indexed_lookup_oob() { - let err = expand_indexed_to_rgb(&[2u8], &[10, 11, 12], 1, 1, 8, 2) - .expect_err("palette lookup should fail"); - - assert!(matches!(err, PdfImageError::InvalidImageData(_))); - } -} diff --git a/crates/pdf-image/src/lib.rs b/crates/pdf-image/src/lib.rs index 269120fc..c57f2fd9 100644 --- a/crates/pdf-image/src/lib.rs +++ b/crates/pdf-image/src/lib.rs @@ -1,6 +1,5 @@ pub mod error; pub mod image_xobject; -pub mod indexed; pub mod inline_image; pub use error::PdfImageError;