Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 63 additions & 44 deletions crates/pdf-filter/src/filter.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::borrow::Cow;
use std::fmt;
use std::sync::Arc;

use crate::{error::FilterError, predictor::PredictorParams};

Expand Down Expand Up @@ -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<Vec<u8>>,
objects: &dyn ObjectResolver,
) -> Result<Cow<'a, [u8]>, FilterError> {
let mut data: Cow<'a, [u8]> = Cow::Borrowed(stream_data);
) -> Result<Arc<Vec<u8>>, FilterError> {
let mut data = stream_data;
let filters = Filter::from_dictionary(dictionary, objects)?;

let Some(filters) = &filters else {
Expand All @@ -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 {
Expand All @@ -306,49 +307,49 @@ 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)?;
let globals = match params {
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()));
Expand All @@ -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<Cow<'a, [u8]>, FilterError> {
decode_data_with_resolver(&stream.dictionary, stream.raw_data(), objects)
) -> Result<Arc<Vec<u8>>, 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<Cow<'_, [u8]>, FilterError> {
pub fn decode(stream: &StreamObject) -> Result<Arc<Vec<u8>>, FilterError> {
let objects = PassthroughResolver;
decode_with_resolver(stream, &objects)
}
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion crates/pdf-filter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion crates/pdf-image/src/image_xobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ impl ImageXObject {
soft_mask: Option<ImageXObject>,
) -> Result<Self, PdfImageError> {
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)
}
Expand Down
37 changes: 31 additions & 6 deletions crates/pdf-image/src/inline_image.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
use std::collections::BTreeMap;
use std::sync::Arc;

use pdf_object::{dictionary::Dictionary, object_variant::ObjectVariant};

/// Canonical parsed representation of a PDF inline image.
#[derive(Debug, Clone, PartialEq)]
pub struct InlineImage {
dictionary: Dictionary,
data: Vec<u8>,
data: Arc<Vec<u8>>,
}

impl InlineImage {
/// Creates a new inline image from its parsed dictionary and raw payload bytes.
pub fn new(dictionary: Dictionary, data: Vec<u8>) -> Self {
Self { dictionary, data }
pub fn new(dictionary: Dictionary, data: impl Into<Arc<Vec<u8>>>) -> Self {
Self {
dictionary,
data: data.into(),
}
}

/// Returns the parsed inline-image dictionary.
Expand All @@ -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<Vec<u8>> {
Arc::clone(&self.data)
}

/// Splits the inline image into its parsed dictionary and raw payload.
pub fn into_parts(self) -> (Dictionary, Vec<u8>) {
/// Splits the inline image into its parsed dictionary and shared raw payload.
pub fn into_parts(self) -> (Dictionary, Arc<Vec<u8>>) {
(self.dictionary, self.data)
}

Expand Down Expand Up @@ -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([
Expand Down
5 changes: 0 additions & 5 deletions crates/pdf-object-collection/src/object_collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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,
Expand Down
Loading