diff --git a/crates/pdf-filter/src/filter.rs b/crates/pdf-filter/src/filter.rs index a47e7a4c..346f5a56 100644 --- a/crates/pdf-filter/src/filter.rs +++ b/crates/pdf-filter/src/filter.rs @@ -1,5 +1,5 @@ -use std::borrow::Cow; use std::fmt; +use std::sync::Arc; use crate::{error::FilterError, predictor::PredictorParams}; @@ -260,24 +260,25 @@ impl Filter { } } -/// Decodes borrowed stream data by applying the filter chain from its dictionary. +/// Decodes stream data by applying the filter chain from its dictionary. /// /// Reads the `/Filter` entry from `dictionary` and applies each filter in order. -/// Returns the fully decoded bytes, or `Cow::Borrowed` if no filters are present. +/// Returns shared ownership of the fully decoded bytes. +/// Unfiltered input retains the original shared allocation. /// -/// This entry point accepts a dictionary and data slice separately so callers +/// This entry point accepts a dictionary and shared data separately so callers /// such as inline-image decoders do not need to construct a temporary /// [`StreamObject`]. /// /// # Errors /// /// Returns [`FilterError`] if any filter in the chain fails or is unsupported. -pub fn decode_data_with_resolver<'a>( +pub fn decode_data_with_resolver( dictionary: &Dictionary, - stream_data: &'a [u8], + stream_data: Arc>, objects: &dyn ObjectResolver, -) -> Result, FilterError> { - let mut data: Cow<'a, [u8]> = Cow::Borrowed(stream_data); +) -> Result>, FilterError> { + let mut data = stream_data; let filters = Filter::from_dictionary(dictionary, objects)?; let Some(filters) = &filters else { @@ -289,14 +290,14 @@ pub fn decode_data_with_resolver<'a>( for (filter, params) in filters.iter().zip(decode_params.iter()) { match filter { Filter::FlateDecode => { - let decoded = Filter::decode_flate(&data)?; + let decoded = Filter::decode_flate(data.as_slice())?; let decoded = match params { DecodeParms::Flate { predictor } if !predictor.is_none() => { crate::predictor::apply_predictor(&decoded, predictor)? } _ => decoded, }; - data = Cow::Owned(decoded); + data = Arc::new(decoded); } Filter::LZWDecode => { let (early_change, predictor) = match params { @@ -306,32 +307,32 @@ pub fn decode_data_with_resolver<'a>( } => (*early_change, Some(predictor)), _ => (true, None), }; - let decoded = crate::lzw::decode(&data, early_change)?; + let decoded = crate::lzw::decode(data.as_slice(), early_change)?; let decoded = match predictor { Some(p) if !p.is_none() => crate::predictor::apply_predictor(&decoded, p)?, _ => decoded, }; - data = Cow::Owned(decoded); + data = Arc::new(decoded); } Filter::JPXDecode => { - let decoded = Filter::decode_jpeg2000(&data)?; - data = Cow::Owned(decoded); + let decoded = Filter::decode_jpeg2000(data.as_slice())?; + data = Arc::new(decoded); } Filter::DCTDecode => { - let decoded = Filter::decode_jpeg_baseline(&data)?; - data = Cow::Owned(decoded); + let decoded = Filter::decode_jpeg_baseline(data.as_slice())?; + data = Arc::new(decoded); } Filter::ASCII85Decode => { - let decoded = crate::ascii85::decode_ascii85(&data)?; - data = Cow::Owned(decoded); + let decoded = crate::ascii85::decode_ascii85(data.as_slice())?; + data = Arc::new(decoded); } Filter::ASCIIHexDecode => { - let decoded = crate::asciihex::decode_ascii_hex(&data)?; - data = Cow::Owned(decoded); + let decoded = crate::asciihex::decode_ascii_hex(data.as_slice())?; + data = Arc::new(decoded); } Filter::RunLengthDecode => { - let decoded = crate::runlength::decode_run_length(&data)?; - data = Cow::Owned(decoded); + let decoded = crate::runlength::decode_run_length(data.as_slice())?; + data = Arc::new(decoded); } Filter::JBIG2Decode => { let (width, height) = resolve_jbig2_dimensions(dictionary, objects)?; @@ -339,16 +340,16 @@ pub fn decode_data_with_resolver<'a>( DecodeParms::Jbig2 { globals } => globals.as_deref(), _ => None, }; - let decoded = pdf_jbig2::decode(&data, width, height, globals)?; - data = Cow::Owned(decoded); + let decoded = pdf_jbig2::decode(data.as_slice(), width, height, globals)?; + data = Arc::new(decoded); } Filter::CCITTFaxDecode => { let ccitt_params = match params { DecodeParms::CcittFax(p) => p, _ => &CCITTFaxParams::DEFAULT, }; - let decoded = pdf_ccitt::decode(&data, ccitt_params)?; - data = Cow::Owned(decoded); + let decoded = pdf_ccitt::decode(data.as_slice(), ccitt_params)?; + data = Arc::new(decoded); } Filter::Unsupported(name) => { return Err(FilterError::UnsupportedFilter(name.clone())); @@ -360,24 +361,24 @@ pub fn decode_data_with_resolver<'a>( /// Decodes a [`StreamObject`] by applying its full filter chain. /// -/// This compatibility entry point forwards the stream's dictionary and raw -/// data to [`decode_data_with_resolver`]. +/// Unfiltered data shares the stream's existing allocation. Filtered data is +/// returned in a newly allocated shared buffer. /// /// # Errors /// /// Returns [`FilterError`] if any filter in the chain fails or is unsupported. -pub fn decode_with_resolver<'a>( - stream: &'a StreamObject, +pub fn decode_with_resolver( + stream: &StreamObject, objects: &dyn ObjectResolver, -) -> Result, FilterError> { - decode_data_with_resolver(&stream.dictionary, stream.raw_data(), objects) +) -> Result>, FilterError> { + decode_data_with_resolver(&stream.dictionary, stream.shared_data(), objects) } /// Decodes a [`StreamObject`] by applying its full filter chain. /// /// This convenience wrapper uses a passthrough resolver, so it only supports /// direct `/Filter` and `/DecodeParms` values. -pub fn decode(stream: &StreamObject) -> Result, FilterError> { +pub fn decode(stream: &StreamObject) -> Result>, FilterError> { let objects = PassthroughResolver; decode_with_resolver(stream, &objects) } @@ -550,31 +551,49 @@ mod tests { } #[test] - fn decode_data_without_filters_borrows_input() { + fn decode_data_without_filters_preserves_shared_data() { let dictionary = Dictionary::new(BTreeMap::new()); - let data = b"borrowed stream data"; + let data = Arc::new(b"stream data".to_vec()); - let decoded = decode_data_with_resolver(&dictionary, data, &PassthroughResolver) - .expect("unfiltered data should decode"); + let decoded = + decode_data_with_resolver(&dictionary, Arc::clone(&data), &PassthroughResolver) + .expect("unfiltered data should decode"); - assert!(matches!(&decoded, Cow::Borrowed(_))); - assert_eq!(decoded.as_ref().as_ptr(), data.as_ptr()); - assert_eq!(decoded.as_ref(), data); + assert!(Arc::ptr_eq(&decoded, &data)); + assert_eq!(decoded.as_slice(), b"stream data"); } #[test] - fn decode_data_with_filter_returns_decoded_owned_data() { + fn decode_data_with_filter_returns_shared_decoded_data() { let dictionary = Dictionary::new(BTreeMap::from([( "Filter".to_string(), ObjectVariant::Name(b"ASCIIHexDecode".to_vec()), )])); + let encoded = Arc::new(b"48 65 6c 6c 6f>".to_vec()); let decoded = - decode_data_with_resolver(&dictionary, b"48 65 6c 6c 6f>", &PassthroughResolver) + decode_data_with_resolver(&dictionary, Arc::clone(&encoded), &PassthroughResolver) .expect("filtered data should decode"); - assert!(matches!(&decoded, Cow::Owned(_))); - assert_eq!(decoded.as_ref(), b"Hello"); + assert_eq!(Arc::strong_count(&decoded), 1); + assert!(!Arc::ptr_eq(&decoded, &encoded)); + assert_eq!(decoded.as_slice(), b"Hello"); + } + + #[test] + fn decode_unfiltered_stream_shares_data() { + let stream = StreamObject::new( + 1, + 0, + Box::new(Dictionary::new(BTreeMap::new())), + b"shared stream data".to_vec(), + ); + + let decoded = decode_with_resolver(&stream, &PassthroughResolver) + .expect("unfiltered data should decode"); + + assert!(Arc::ptr_eq(&decoded, &stream.data)); + assert_eq!(decoded.as_slice(), b"shared stream data"); } #[test] diff --git a/crates/pdf-filter/src/lib.rs b/crates/pdf-filter/src/lib.rs index d2bb7169..b0c3e9fa 100644 --- a/crates/pdf-filter/src/lib.rs +++ b/crates/pdf-filter/src/lib.rs @@ -15,7 +15,7 @@ //! The main entry point is [`filter::decode`], which accepts a //! [`StreamObject`](pdf_object::stream::StreamObject) and applies the full //! filter chain declared in its `/Filter` dictionary entry. Callers that hold -//! a dictionary and borrowed data separately can use +//! a dictionary and shared data separately can use //! [`filter::decode_data_with_resolver`] without constructing a stream object. pub(crate) mod ascii85; diff --git a/crates/pdf-image/src/image_xobject.rs b/crates/pdf-image/src/image_xobject.rs index 7543a9f8..23ae1b09 100644 --- a/crates/pdf-image/src/image_xobject.rs +++ b/crates/pdf-image/src/image_xobject.rs @@ -80,7 +80,7 @@ impl ImageXObject { soft_mask: Option, ) -> Result { let dictionary = image.normalized_dictionary(); - let decoded = decode_data_with_resolver(&dictionary, image.data(), objects)?; + let decoded = decode_data_with_resolver(&dictionary, image.shared_data(), objects)?; Self::decode_normalized_image(&dictionary, decoded.as_ref(), objects, soft_mask) } diff --git a/crates/pdf-image/src/inline_image.rs b/crates/pdf-image/src/inline_image.rs index a2336021..9fd0468c 100644 --- a/crates/pdf-image/src/inline_image.rs +++ b/crates/pdf-image/src/inline_image.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::sync::Arc; use pdf_object::{dictionary::Dictionary, object_variant::ObjectVariant}; @@ -6,13 +7,16 @@ use pdf_object::{dictionary::Dictionary, object_variant::ObjectVariant}; #[derive(Debug, Clone, PartialEq)] pub struct InlineImage { dictionary: Dictionary, - data: Vec, + data: Arc>, } impl InlineImage { /// Creates a new inline image from its parsed dictionary and raw payload bytes. - pub fn new(dictionary: Dictionary, data: Vec) -> Self { - Self { dictionary, data } + pub fn new(dictionary: Dictionary, data: impl Into>>) -> Self { + Self { + dictionary, + data: data.into(), + } } /// Returns the parsed inline-image dictionary. @@ -22,11 +26,16 @@ impl InlineImage { /// Returns the raw inline-image payload bytes. pub fn data(&self) -> &[u8] { - &self.data + self.data.as_slice() + } + + /// Returns shared ownership of the raw inline-image payload bytes. + pub fn shared_data(&self) -> Arc> { + Arc::clone(&self.data) } - /// Splits the inline image into its parsed dictionary and raw payload. - pub fn into_parts(self) -> (Dictionary, Vec) { + /// Splits the inline image into its parsed dictionary and shared raw payload. + pub fn into_parts(self) -> (Dictionary, Arc>) { (self.dictionary, self.data) } @@ -100,11 +109,27 @@ fn normalize_inline_image_value(key: &str, value: &ObjectVariant) -> ObjectVaria #[cfg(test)] mod tests { use std::collections::BTreeMap; + use std::sync::Arc; use pdf_object::object_variant::ObjectVariant; use super::{InlineImage, normalize_inline_image_dictionary}; + #[test] + fn inline_image_shares_payload_data() { + let data = Arc::new(vec![1, 2, 3, 4]); + let image = InlineImage::new( + pdf_object::dictionary::Dictionary::new(BTreeMap::new()), + Arc::clone(&data), + ); + + assert!(Arc::ptr_eq(&image.shared_data(), &data)); + assert_eq!(image.data(), data.as_slice()); + + let (_, payload) = image.into_parts(); + assert!(Arc::ptr_eq(&payload, &data)); + } + #[test] fn normalize_inline_image_dictionary_expands_abbreviations() { let dictionary = pdf_object::dictionary::Dictionary::new(BTreeMap::from([ diff --git a/crates/pdf-object-collection/src/object_collection.rs b/crates/pdf-object-collection/src/object_collection.rs index 572b545a..7e1013c2 100644 --- a/crates/pdf-object-collection/src/object_collection.rs +++ b/crates/pdf-object-collection/src/object_collection.rs @@ -2,7 +2,6 @@ use pdf_object::indirect_object::IndirectObject; use pdf_object::object_resolver::ObjectResolver; use pdf_object::stream::StreamObject; use pdf_object::{error::ObjectError, object_variant::ObjectVariant}; -use std::borrow::Cow; use std::collections::{HashMap, HashSet}; #[cfg(feature = "json")] @@ -86,10 +85,6 @@ impl ObjectCollection { } ObjectVariant::Stream(stream) => { let data = pdf_filter::filter::decode_with_resolver(&stream, self) - .map(|data| match data { - Cow::Borrowed(_) => stream.shared_data(), - Cow::Owned(data) => data.into(), - }) .unwrap_or_else(|_| stream.shared_data()); let StreamObject { object_number,