From 55c43617e4a580bb623a01363c3b6b2287558874 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 31 Jul 2026 23:44:34 +0300 Subject: [PATCH] pdf-filter: share decoded data with Arc Replace Cow-based filter decoding with Arc> so unfiltered streams retain their allocation and decoded buffers can be shared. Store inline image payloads in Arc as well and pass shared data through the decoding pipeline without intermediate copies. Co-authored-by: Codex --- crates/pdf-filter/src/filter.rs | 107 +++++++++++------- crates/pdf-filter/src/lib.rs | 2 +- crates/pdf-image/src/image_xobject.rs | 2 +- crates/pdf-image/src/inline_image.rs | 37 +++++- .../src/object_collection.rs | 5 - 5 files changed, 96 insertions(+), 57 deletions(-) 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,