From 97614f38041c3af62fe2ec8a97aca6b5bc963ae5 Mon Sep 17 00:00:00 2001 From: Velli20 Date: Mon, 26 Oct 2026 00:33:29 +0200 Subject: [PATCH] Store dictionaries as sorted vectors with inline name keys Dictionary entries move from a BTreeMap, ObjectVariant> to a private Vec<(PdfName, ObjectVariant)> sorted by key, looked up by binary search. PdfName stores names of up to 22 bytes inline, so dictionary keys no longer allocate, and the parser builds names straight from the input unless they contain # escapes. Dictionary::new and its sort-and-dedup pass are removed. The parser inserts each entry as it reads it, so a repeated key keeps its last value as before; decryption decrypts values in place through iter_mut; inline-image normalization builds its dictionary directly. Callers with loose entries use the new FromIterator impl. optional_entries and lenient_entries borrow the resolved sub-dictionary instead of cloning it. Co-Authored-By: Claude Opus 5.5 --- crates/pdf-canvas/benches/text_recording.rs | 2 +- crates/pdf-canvas/tests/common/mod.rs | 2 +- crates/pdf-canvas/tests/pdf_canvas.rs | 4 +- crates/pdf-ccitt/src/ccitt_fax_params.rs | 33 ++---- .../src/device_n_color_space.rs | 5 +- .../src/indexed_color_space.rs | 8 +- .../benches/operator_parsing.rs | 7 +- .../pdf-content-stream/src/content_stream.rs | 11 +- crates/pdf-content-stream/tests/parser_api.rs | 11 +- crates/pdf-document/src/decryption.rs | 34 +++--- crates/pdf-document/src/encryption.rs | 2 +- crates/pdf-document/tests/pages.rs | 8 +- crates/pdf-filter/src/filter.rs | 44 +++---- crates/pdf-font/src/pdf_font_metrics.rs | 2 +- crates/pdf-image/src/decoded_samples.rs | 2 +- crates/pdf-image/src/image_decoder.rs | 52 ++++----- crates/pdf-image/src/image_metadata.rs | 16 +-- crates/pdf-image/src/pdf_image.rs | 82 ++++++------- .../src/object_collection.rs | 23 ++-- crates/pdf-object-reader/src/context.rs | 56 ++++++--- crates/pdf-object-reader/src/decode.rs | 4 +- crates/pdf-object-reader/src/dictionary.rs | 102 ++++++++++++----- crates/pdf-object-reader/src/pdf_name.rs | 108 +++++++++++++++++- crates/pdf-object-reader/src/stream.rs | 24 +--- .../pdf-parser/src/cross_reference_stream.rs | 22 ++-- crates/pdf-parser/src/dictionary.rs | 45 ++++---- crates/pdf-parser/src/inline_image.rs | 2 +- crates/pdf-parser/src/name.rs | 15 ++- .../src/external_graphics_state.rs | 6 +- crates/pdf-resources/src/form.rs | 4 +- crates/pdf-resources/src/soft_mask.rs | 18 +-- crates/pdf-resources/src/xobject.rs | 16 ++- crates/pdf-resources/tests/resources.rs | 86 +++++++------- crates/pdf-shading/tests/parse.rs | 9 +- tools/conformance/src/inventory.rs | 4 +- 35 files changed, 481 insertions(+), 388 deletions(-) diff --git a/crates/pdf-canvas/benches/text_recording.rs b/crates/pdf-canvas/benches/text_recording.rs index f1c8ff50..0e76fa22 100644 --- a/crates/pdf-canvas/benches/text_recording.rs +++ b/crates/pdf-canvas/benches/text_recording.rs @@ -18,7 +18,7 @@ fn content_stream(text: &[u8]) -> ContentStream { let mut data = b"BT /F1 10 Tf (".to_vec(); data.extend_from_slice(text); data.extend_from_slice(b") Tj ET"); - let stream = StreamObject::new(1, 0, Dictionary::new(Default::default()), data); + let stream = StreamObject::new(1, 0, Dictionary::default(), data); pdf_object_reader::ObjectReader::new(EmptySource) .read::(&ObjectVariant::Stream(stream)) .expect("benchmark stream should parse") diff --git a/crates/pdf-canvas/tests/common/mod.rs b/crates/pdf-canvas/tests/common/mod.rs index fa8897e3..a19a87a7 100644 --- a/crates/pdf-canvas/tests/common/mod.rs +++ b/crates/pdf-canvas/tests/common/mod.rs @@ -157,7 +157,7 @@ pub fn content_stream(object_number: usize, data: &[u8]) -> ContentStream { let stream = StreamObject::new( object_number, 0, - Dictionary::new(std::collections::BTreeMap::< + Dictionary::from_iter(std::collections::BTreeMap::< Vec, pdf_object_reader::object_variant::ObjectVariant, >::new()), diff --git a/crates/pdf-canvas/tests/pdf_canvas.rs b/crates/pdf-canvas/tests/pdf_canvas.rs index 09159d24..2f33c3aa 100644 --- a/crates/pdf-canvas/tests/pdf_canvas.rs +++ b/crates/pdf-canvas/tests/pdf_canvas.rs @@ -53,7 +53,7 @@ fn form_resource(name: &str, stream: ContentStream) -> Resources { } fn image_dictionary() -> Dictionary { - Dictionary::new(std::collections::BTreeMap::from([ + Dictionary::from_iter(std::collections::BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(1)), ( Vec::from(b"ColorSpace"), @@ -75,7 +75,7 @@ fn image_dictionary() -> Dictionary { fn inline_image() -> PdfImage { PdfImage::inline( - Dictionary::new(std::collections::BTreeMap::from([ + Dictionary::from_iter(std::collections::BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(1)), ( Vec::from(b"CS"), diff --git a/crates/pdf-ccitt/src/ccitt_fax_params.rs b/crates/pdf-ccitt/src/ccitt_fax_params.rs index 38f37dca..e2259583 100644 --- a/crates/pdf-ccitt/src/ccitt_fax_params.rs +++ b/crates/pdf-ccitt/src/ccitt_fax_params.rs @@ -79,12 +79,10 @@ mod tests { use pdf_object_reader::EmptySource; use pdf_object_reader::object_variant::ObjectVariant; use pdf_object_reader::{Dictionary, ObjectReader}; - use std::collections::BTreeMap; #[test] fn params_from_empty_dict_uses_defaults() -> ReadResult<()> { - use std::collections::BTreeMap; - let dict = Dictionary::new(BTreeMap::, ObjectVariant>::new()); + let dict = Dictionary::default(); let p = ObjectReader::new(EmptySource) .read::(&ObjectVariant::Dictionary(dict))?; assert_eq!(p.k, 0); @@ -102,25 +100,16 @@ mod tests { fn params_from_dict_reads_all_keys() -> ReadResult<()> { use pdf_object_reader::object_variant::ObjectVariant; - let mut dict = Dictionary::new(BTreeMap::, ObjectVariant>::new()); - dict.dictionary - .insert(b"K".to_vec(), ObjectVariant::Integer(-1)); - dict.dictionary - .insert(b"Columns".to_vec(), ObjectVariant::Integer(800)); - dict.dictionary - .insert(b"Rows".to_vec(), ObjectVariant::Integer(600)); - dict.dictionary - .insert(b"EndOfLine".to_vec(), ObjectVariant::Boolean(true)); - dict.dictionary - .insert(b"EncodedByteAlign".to_vec(), ObjectVariant::Boolean(true)); - dict.dictionary - .insert(b"EndOfBlock".to_vec(), ObjectVariant::Boolean(false)); - dict.dictionary - .insert(b"BlackIs1".to_vec(), ObjectVariant::Boolean(true)); - dict.dictionary.insert( - b"DamagedRowsBeforeError".to_vec(), - ObjectVariant::Integer(2), - ); + let dict = Dictionary::from_entries([ + (b"K", ObjectVariant::Integer(-1)), + (b"Columns", ObjectVariant::Integer(800)), + (b"Rows", ObjectVariant::Integer(600)), + (b"EndOfLine", ObjectVariant::Boolean(true)), + (b"EncodedByteAlign", ObjectVariant::Boolean(true)), + (b"EndOfBlock", ObjectVariant::Boolean(false)), + (b"BlackIs1", ObjectVariant::Boolean(true)), + (b"DamagedRowsBeforeError", ObjectVariant::Integer(2)), + ]); let p = ObjectReader::new(EmptySource) .read::(&ObjectVariant::Dictionary(dict))?; diff --git a/crates/pdf-color-space/src/device_n_color_space.rs b/crates/pdf-color-space/src/device_n_color_space.rs index 205ed673..60a7a5ee 100644 --- a/crates/pdf-color-space/src/device_n_color_space.rs +++ b/crates/pdf-color-space/src/device_n_color_space.rs @@ -141,7 +141,7 @@ mod tests { ObjectVariant::Stream(StreamObject::new( 1, 0, - Dictionary::new(dict), + Dictionary::from_iter(dict), code.as_bytes().to_vec(), )) } @@ -173,8 +173,7 @@ mod tests { #[test] fn parses_five_element_device_n_array_with_attributes() { - let attrs = - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::, ObjectVariant>::new())); + let attrs = ObjectVariant::Dictionary(Dictionary::default()); let arr = device_n_array(vec![ ObjectVariant::Array(vec![name("Spot")].into()), name("DeviceCMYK"), diff --git a/crates/pdf-color-space/src/indexed_color_space.rs b/crates/pdf-color-space/src/indexed_color_space.rs index b1ff2010..3b5bfe40 100644 --- a/crates/pdf-color-space/src/indexed_color_space.rs +++ b/crates/pdf-color-space/src/indexed_color_space.rs @@ -144,7 +144,6 @@ impl IndexedColorSpace { #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { - use std::collections::BTreeMap; use pdf_object_reader::{ EmptySource, ObjectReader, dictionary::Dictionary, object_variant::ObjectVariant, @@ -169,12 +168,7 @@ mod tests { #[test] fn stream_lookup_shares_decoded_stream_bytes() { - let stream = StreamObject::new( - 1, - 0, - Dictionary::new(BTreeMap::, ObjectVariant>::new()), - vec![1, 2, 3, 4], - ); + let stream = StreamObject::new(1, 0, Dictionary::default(), vec![1, 2, 3, 4]); let stream_data = stream.shared_data(); let lookup = ObjectVariant::Stream(stream); diff --git a/crates/pdf-content-stream/benches/operator_parsing.rs b/crates/pdf-content-stream/benches/operator_parsing.rs index 84766c2b..31e23df1 100644 --- a/crates/pdf-content-stream/benches/operator_parsing.rs +++ b/crates/pdf-content-stream/benches/operator_parsing.rs @@ -16,12 +16,7 @@ fn repeated_stream(fragment: &[u8]) -> ObjectVariant { data.extend_from_slice(fragment); } - ObjectVariant::Stream(StreamObject::new( - 1, - 0, - Dictionary::new(Default::default()), - data, - )) + ObjectVariant::Stream(StreamObject::new(1, 0, Dictionary::default(), data)) } fn benchmark_stream( diff --git a/crates/pdf-content-stream/src/content_stream.rs b/crates/pdf-content-stream/src/content_stream.rs index 154956cc..1b69e858 100644 --- a/crates/pdf-content-stream/src/content_stream.rs +++ b/crates/pdf-content-stream/src/content_stream.rs @@ -96,12 +96,7 @@ mod tests { } fn stream_object(object_number: usize, data: &[u8]) -> StreamObject { - StreamObject::new( - object_number, - 0, - Dictionary::new(BTreeMap::, ObjectVariant>::new()), - data.to_vec(), - ) + StreamObject::new(object_number, 0, Dictionary::default(), data.to_vec()) } struct MapResolver { @@ -291,7 +286,7 @@ mod tests { #[test] fn optional_contents_preserves_allocator_for_missing_contents() { - let page = Dictionary::new(BTreeMap::, ObjectVariant>::new()); + let page = Dictionary::default(); let reader = pdf_object_reader::ObjectReader::new(EmptySource); let contents = reader @@ -318,7 +313,7 @@ mod tests { ] .into(), ); - let page = Dictionary::new(BTreeMap::from([(Vec::from(b"Contents"), contents)])); + let page = Dictionary::from_iter(BTreeMap::from([(Vec::from(b"Contents"), contents)])); let resolver = MapResolver { objects: BTreeMap::from([ (1, ObjectVariant::Stream(stream_object(1, b"q"))), diff --git a/crates/pdf-content-stream/tests/parser_api.rs b/crates/pdf-content-stream/tests/parser_api.rs index 71f9819e..d44c6432 100644 --- a/crates/pdf-content-stream/tests/parser_api.rs +++ b/crates/pdf-content-stream/tests/parser_api.rs @@ -24,12 +24,7 @@ impl pdf_object_reader::FromPdfObject for PageContents { } fn stream_object(object_number: usize, data: &[u8]) -> StreamObject { - StreamObject::new( - object_number, - 0, - Dictionary::new(BTreeMap::, ObjectVariant>::new()), - data.to_vec(), - ) + StreamObject::new(object_number, 0, Dictionary::default(), data.to_vec()) } struct MapResolver { @@ -151,7 +146,7 @@ fn content_stream_read_skips_unknown_operator_and_recovers() { #[test] fn optional_contents_preserves_allocator_for_missing_contents() { - let page = Dictionary::new(BTreeMap::, ObjectVariant>::new()); + let page = Dictionary::default(); let reader = pdf_object_reader::ObjectReader::new(EmptySource); let contents = reader @@ -178,7 +173,7 @@ fn optional_contents_parses_stream_arrays_and_allocates_monotonically() { ] .into(), ); - let page = Dictionary::new(BTreeMap::from([(Vec::from(b"Contents"), contents)])); + let page = Dictionary::from_iter(BTreeMap::from([(Vec::from(b"Contents"), contents)])); let resolver = MapResolver { objects: BTreeMap::from([ (1, ObjectVariant::Stream(stream_object(1, b"1 2"))), diff --git a/crates/pdf-document/src/decryption.rs b/crates/pdf-document/src/decryption.rs index 4f376ac7..484e6b6c 100644 --- a/crates/pdf-document/src/decryption.rs +++ b/crates/pdf-document/src/decryption.rs @@ -292,27 +292,19 @@ impl DocumentDecryptor { /// Decrypts dictionary values while preserving signature contents required by PDF rules. fn decrypt_dictionary( &self, - dictionary: Dictionary, + mut dictionary: Dictionary, object_number: usize, generation_number: usize, ) -> Result { let is_signature = is_signature_dictionary(&dictionary); - let entries = dictionary - .dictionary - .into_iter() - .map(|(key, value)| { - if is_signature && key == b"Contents" { - Ok((key, value)) - } else { - self.decrypt_object_value(value, object_number, generation_number) - .map(|value| (key, value)) - } - }) - .collect::>()?; - Ok(Dictionary { - dictionary: entries, - object_number: dictionary.object_number, - }) + for (key, value) in dictionary.iter_mut() { + if is_signature && key == b"Contents" { + continue; + } + let encrypted = std::mem::replace(value, ObjectVariant::Null); + *value = self.decrypt_object_value(encrypted, object_number, generation_number)?; + } + Ok(dictionary) } /// Decrypts a stream's data and recursively transforms its dictionary values. @@ -385,7 +377,7 @@ mod tests { ); } - StreamObject::new(7, 0, Dictionary::new(entries), data) + StreamObject::new(7, 0, Dictionary::from_iter(entries), data) } fn encrypt_for_object( @@ -473,7 +465,7 @@ mod tests { number: object_number, generation: generation_number, }; - let object = ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([ + let object = ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Contents"), pdf_object_reader::pdf_string::PdfString::from( @@ -556,7 +548,7 @@ mod tests { let stream = StreamObject::new( 99, 1, - Dictionary::new(BTreeMap::from([( + Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Label"), pdf_object_reader::pdf_string::PdfString::from( encrypt_for_object( @@ -602,7 +594,7 @@ mod tests { number: 9, generation: 0, }; - let object = ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([ + let object = ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Type"), pdf_object_reader::pdf_string::PdfString::from( diff --git a/crates/pdf-document/src/encryption.rs b/crates/pdf-document/src/encryption.rs index 74ab398f..1672950f 100644 --- a/crates/pdf-document/src/encryption.rs +++ b/crates/pdf-document/src/encryption.rs @@ -316,7 +316,7 @@ mod tests { for (key, value) in entries { map.insert(Vec::from(key.as_bytes()), value); } - Dictionary::new(map) + Dictionary::from_iter(map) } #[test] diff --git a/crates/pdf-document/tests/pages.rs b/crates/pdf-document/tests/pages.rs index 976f7a23..efa7a32e 100644 --- a/crates/pdf-document/tests/pages.rs +++ b/crates/pdf-document/tests/pages.rs @@ -48,14 +48,14 @@ fn color_space(value: &str) -> ObjectVariant { } fn resources_dictionary(entries: &[(&str, &str)]) -> Dictionary { - let color_spaces = Dictionary::new( + let color_spaces = Dictionary::from_iter( entries .iter() .map(|(name, value)| (Vec::from(name.as_bytes()), color_space(value))) .collect::>(), ); - Dictionary::new(BTreeMap::from([( + Dictionary::from_iter(BTreeMap::from([( Vec::from(b"ColorSpace"), ObjectVariant::Dictionary(color_spaces), )])) @@ -92,7 +92,7 @@ fn page_dictionary( ); } - let mut dictionary = Dictionary::new(entries); + let mut dictionary = Dictionary::from_iter(entries); dictionary.object_number = Some(object_number); dictionary } @@ -134,7 +134,7 @@ fn pages_dictionary( ); } - let mut dictionary = Dictionary::new(entries); + let mut dictionary = Dictionary::from_iter(entries); dictionary.object_number = Some(object_number); dictionary } diff --git a/crates/pdf-filter/src/filter.rs b/crates/pdf-filter/src/filter.rs index e22fdd58..8574bef1 100644 --- a/crates/pdf-filter/src/filter.rs +++ b/crates/pdf-filter/src/filter.rs @@ -299,7 +299,7 @@ mod tests { #[test] fn decode_data_without_filters_preserves_shared_data() { - let dictionary = Dictionary::new(BTreeMap::, ObjectVariant>::new()); + let dictionary = Dictionary::default(); let data = Bytes::from_static(b"stream data"); let decoded = decode_with( @@ -314,7 +314,7 @@ mod tests { #[test] fn decode_data_with_filter_returns_shared_decoded_data() { - let dictionary = Dictionary::new(BTreeMap::from([( + let dictionary = Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Filter"), pdf_object_reader::pdf_string::PdfString::from( b"ASCIIHexDecode", @@ -336,12 +336,7 @@ mod tests { #[test] fn decode_unfiltered_stream_shares_data() { - let stream = StreamObject::new( - 1, - 0, - Dictionary::new(BTreeMap::, ObjectVariant>::new()), - b"shared stream data".to_vec(), - ); + let stream = StreamObject::new(1, 0, Dictionary::default(), b"shared stream data".to_vec()); let data = stream.shared_data(); let decoded = decode(stream).expect("unfiltered data should decode"); @@ -361,7 +356,7 @@ mod tests { ), ); - let stream = StreamObject::new(1, 0, Dictionary::new(dict), Vec::new()); + let stream = StreamObject::new(1, 0, Dictionary::from_iter(dict), Vec::new()); let err = decode(stream).expect_err("expected decode failure"); assert!(matches!(err, FilterError::Decompression(_))); } @@ -401,7 +396,8 @@ mod tests { pdf_object_reader::string_kind::StringKind::Name, ), ); - let globals_stream = StreamObject::new(3, 0, Dictionary::new(globals_dict), Vec::new()); + let globals_stream = + StreamObject::new(3, 0, Dictionary::from_iter(globals_dict), Vec::new()); let mut decode_parms = BTreeMap::new(); decode_parms.insert( @@ -424,10 +420,13 @@ mod tests { ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new(2, 0)), ); - let stream = StreamObject::new_encoded(1, 0, Dictionary::new(dict), Vec::new()); + let stream = StreamObject::new_encoded(1, 0, Dictionary::from_iter(dict), Vec::new()); let mut objects = BTreeMap::new(); - objects.insert(2, ObjectVariant::Dictionary(Dictionary::new(decode_parms))); + objects.insert( + 2, + ObjectVariant::Dictionary(Dictionary::from_iter(decode_parms)), + ); objects.insert(3, ObjectVariant::Stream(globals_stream)); let resolver = TrackingResolver { @@ -453,7 +452,7 @@ mod tests { dict.insert(Vec::from(b"Width"), ObjectVariant::Integer(8)); dict.insert(Vec::from(b"Height"), ObjectVariant::Integer(1)); - let stream = StreamObject::new(1, 0, Dictionary::new(dict), vec![0x00]); + let stream = StreamObject::new(1, 0, Dictionary::from_iter(dict), vec![0x00]); let err = decode(stream).expect_err("expected decode failure"); assert!(matches!(err, FilterError::Decompression(_))); @@ -473,7 +472,7 @@ mod tests { let stream = StreamObject::new( 1, 0, - Dictionary::new(dict), + Dictionary::from_iter(dict), b"48 65 6c 6c 6f>ignored".to_vec(), ); @@ -519,7 +518,7 @@ mod tests { ObjectVariant::Integer(compressed.len() as i64), ); - let stream = StreamObject::new_encoded(1, 0, Dictionary::new(dict), compressed); + let stream = StreamObject::new_encoded(1, 0, Dictionary::from_iter(dict), compressed); let mut objects = BTreeMap::new(); objects.insert( @@ -554,7 +553,7 @@ mod tests { } encoded.push('>'); - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Filter"), ObjectVariant::Array( @@ -597,7 +596,7 @@ mod tests { let decode_parms = ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new(2, 0)); - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Filter"), pdf_object_reader::pdf_string::PdfString::from( @@ -612,12 +611,7 @@ mod tests { ])); let resolver = TestResolver { - objects: BTreeMap::from([( - 2, - ObjectVariant::Dictionary(Dictionary::new( - BTreeMap::, ObjectVariant>::new(), - )), - )]), + objects: BTreeMap::from([(2, ObjectVariant::Dictionary(Dictionary::default()))]), }; let decoded = decode_with( StreamObject::new_encoded(1, 0, dictionary, compressed), @@ -642,7 +636,7 @@ mod tests { let stream = StreamObject::new( 1, 0, - Dictionary::new(dict), + Dictionary::from_iter(dict), vec![2, b'A', b'B', b'C', 255, b'!', 128], ); @@ -661,7 +655,7 @@ mod tests { ), ); - let stream = StreamObject::new(1, 0, Dictionary::new(dict), vec![0, b'X', 128]); + let stream = StreamObject::new(1, 0, Dictionary::from_iter(dict), vec![0, b'X', 128]); let decoded = decode(stream).expect("decode failed"); assert_eq!(decoded.as_ref(), b"X"); diff --git a/crates/pdf-font/src/pdf_font_metrics.rs b/crates/pdf-font/src/pdf_font_metrics.rs index bd48c00d..429d31b8 100644 --- a/crates/pdf-font/src/pdf_font_metrics.rs +++ b/crates/pdf-font/src/pdf_font_metrics.rs @@ -102,7 +102,7 @@ mod tests { ), )]) .collect(); - Dictionary::new(map) + Dictionary::from_iter(map) } fn int(n: i64) -> ObjectVariant { diff --git a/crates/pdf-image/src/decoded_samples.rs b/crates/pdf-image/src/decoded_samples.rs index 463eee6f..099e46dc 100644 --- a/crates/pdf-image/src/decoded_samples.rs +++ b/crates/pdf-image/src/decoded_samples.rs @@ -474,7 +474,7 @@ mod tests { #[test] fn decode_direct_preserves_shared_identity_samples() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), diff --git a/crates/pdf-image/src/image_decoder.rs b/crates/pdf-image/src/image_decoder.rs index 5d55f032..edb7bd57 100644 --- a/crates/pdf-image/src/image_decoder.rs +++ b/crates/pdf-image/src/image_decoder.rs @@ -243,7 +243,7 @@ mod tests { ObjectVariant::Array( vec![ name("CalRGB"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([ + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"WhitePoint"), ObjectVariant::Array( @@ -282,7 +282,7 @@ mod tests { let function = StreamObject::new( 1, 0, - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"FunctionType"), ObjectVariant::Integer(4)), ( Vec::from(b"Domain"), @@ -342,7 +342,7 @@ mod tests { #[test] fn decode_normalized_1bpc_gray_inverts_samples() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(1)), ( Vec::from(b"ColorSpace"), @@ -373,7 +373,7 @@ mod tests { #[test] fn decode_normalized_8bpc_gray_remaps_samples() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), @@ -404,7 +404,7 @@ mod tests { #[test] fn decode_normalized_indexed_image_applies_decode_before_lookup() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(1)), ( Vec::from(b"ColorSpace"), @@ -449,7 +449,7 @@ mod tests { #[test] fn indexed_device_n_palette_is_converted_through_cal_rgb() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), @@ -479,7 +479,7 @@ mod tests { #[test] fn direct_four_component_device_n_is_not_interpreted_as_cmyk() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), (Vec::from(b"ColorSpace"), fixture_device_n()), (Vec::from(b"Height"), ObjectVariant::Integer(1)), @@ -497,7 +497,7 @@ mod tests { #[test] fn indexed_device_cmyk_keeps_device_fast_path() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), @@ -529,7 +529,7 @@ mod tests { let tint_function = StreamObject::new( 2, 0, - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"FunctionType"), ObjectVariant::Integer(4)), ( Vec::from(b"Domain"), @@ -548,7 +548,7 @@ mod tests { ])), b"{ pop 1 1 1 }".to_vec(), ); - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), @@ -584,7 +584,7 @@ mod tests { #[test] fn decode_normalized_image_rejects_invalid_decode_length() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), @@ -615,7 +615,7 @@ mod tests { #[test] fn decode_normalized_image_without_decode_preserves_samples() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), @@ -642,7 +642,7 @@ mod tests { #[test] fn decode_normalized_image_trims_trailing_samples() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), @@ -668,7 +668,7 @@ mod tests { #[test] fn decode_normalized_image_mask_defaults_bits_per_component_to_one() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"ImageMask"), ObjectVariant::Boolean(true)), (Vec::from(b"Height"), ObjectVariant::Integer(1)), (Vec::from(b"Width"), ObjectVariant::Integer(4)), @@ -686,7 +686,7 @@ mod tests { #[test] fn decode_normalized_non_mask_image_still_requires_bits_per_component() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"ColorSpace"), pdf_object_reader::pdf_string::PdfString::from( @@ -709,7 +709,7 @@ mod tests { #[test] fn decode_normalized_dct_cmyk_accepts_preconverted_rgb_samples() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), @@ -741,7 +741,7 @@ mod tests { #[test] fn decode_normalized_non_dct_cmyk_still_rejects_rgb_sized_samples() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), @@ -771,7 +771,7 @@ mod tests { #[test] fn decode_normalized_dct_single_pixel_expands_to_declared_size() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), @@ -806,7 +806,7 @@ mod tests { #[test] fn decode_normalized_image_applies_resolved_soft_mask() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), @@ -842,7 +842,7 @@ mod tests { #[test] fn decode_inline_image_applies_filter_chain_before_samples() { let image = PdfImage::inline( - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(8)), ( Vec::from(b"CS"), @@ -875,7 +875,7 @@ mod tests { #[test] fn decode_inline_image_accepts_abbreviated_gray_color_space() { let image = PdfImage::inline( - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(1)), ( Vec::from(b"CS"), @@ -903,7 +903,7 @@ mod tests { #[test] fn decode_inline_image_preserves_unfiltered_shared_samples() { let image = PdfImage::inline( - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(8)), ( Vec::from(b"CS"), @@ -932,7 +932,7 @@ mod tests { #[test] fn decode_inline_image_accepts_abbreviated_indexed_color_space() { let image = PdfImage::inline( - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(1)), ( Vec::from(b"CS"), @@ -978,7 +978,7 @@ mod tests { #[test] fn decode_normalized_1bpc_image_expands_samples() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(1)), ( Vec::from(b"ColorSpace"), @@ -1077,7 +1077,7 @@ mod tests { } fn indexed_dictionary(bits_per_component: i64) -> Dictionary { - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(bits_per_component), @@ -1109,7 +1109,7 @@ mod tests { } fn filtered_gray_dictionary() -> Dictionary { - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), diff --git a/crates/pdf-image/src/image_metadata.rs b/crates/pdf-image/src/image_metadata.rs index 3d7bdb1a..fe8e157f 100644 --- a/crates/pdf-image/src/image_metadata.rs +++ b/crates/pdf-image/src/image_metadata.rs @@ -181,7 +181,7 @@ mod tests { #[test] fn image_mask_defaults_bits_per_component_to_one() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"Height"), ObjectVariant::Integer(1)), (Vec::from(b"ImageMask"), ObjectVariant::Boolean(true)), (Vec::from(b"Width"), ObjectVariant::Integer(1)), @@ -197,7 +197,7 @@ mod tests { #[test] fn image_mask_rejects_non_one_bit_components() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), (Vec::from(b"Height"), ObjectVariant::Integer(1)), (Vec::from(b"ImageMask"), ObjectVariant::Boolean(true)), @@ -216,7 +216,7 @@ mod tests { #[test] fn image_mask_stores_filter_chain() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Filter"), pdf_object_reader::pdf_string::PdfString::from( @@ -276,7 +276,7 @@ mod tests { #[test] fn jpx_images_default_bits_per_component_to_eight() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Filter"), pdf_object_reader::pdf_string::PdfString::from( @@ -301,7 +301,7 @@ mod tests { #[test] fn stores_ordered_filter_chain() { let mut dictionary = direct_dictionary(1, 1, 8); - dictionary.dictionary.insert( + dictionary.insert( b"Filter".to_vec(), ObjectVariant::Array( vec![ @@ -331,7 +331,7 @@ mod tests { #[test] fn non_jpx_images_require_bits_per_component() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"Height"), ObjectVariant::Integer(1)), (Vec::from(b"Width"), ObjectVariant::Integer(1)), ])); @@ -347,7 +347,7 @@ mod tests { } fn direct_dictionary(width: i64, height: i64, bits_per_component: i64) -> Dictionary { - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(bits_per_component), @@ -365,7 +365,7 @@ mod tests { } fn indexed_dictionary(bits_per_component: i64) -> Dictionary { - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(bits_per_component), diff --git a/crates/pdf-image/src/pdf_image.rs b/crates/pdf-image/src/pdf_image.rs index e02e9196..9ff43f18 100644 --- a/crates/pdf-image/src/pdf_image.rs +++ b/crates/pdf-image/src/pdf_image.rs @@ -1,10 +1,10 @@ -use std::{collections::BTreeMap, sync::Arc}; +use std::sync::Arc; use bytes::Bytes; use pdf_filter::{filter::Filters, image_payload::ImagePayload}; use pdf_object_reader::{ FromPdfObject, ObjectAccess, ObjectContext, ObjectReadError, ReadResult, - dictionary::Dictionary, object_variant::ObjectVariant, stream::StreamObject, + dictionary::Dictionary, object_variant::ObjectVariant, pdf_name::PdfName, stream::StreamObject, }; use crate::{error::PdfImageError, image_metadata::ImageMetadata}; @@ -94,7 +94,7 @@ impl InlineImageHeader { let dictionary = match self.0 { ObjectVariant::Dictionary(dictionary) => dictionary, // Unreachable: a header is only built from a dictionary. - _ => Dictionary::new(BTreeMap::new()), + _ => Dictionary::default(), }; ObjectVariant::Stream(StreamObject::new_encoded(0, 0, dictionary, data)) } @@ -140,34 +140,35 @@ impl FromPdfObject for InlineDataLength { /// The parser preserves the abbreviated inline-image keys from the content stream. Where /// both spellings of a key are present, the abbreviation wins. fn normalize_inline_image_dictionary(dictionary: Dictionary) -> Dictionary { - let mut normalized = BTreeMap::new(); - - for (key, value) in dictionary.dictionary { - let canonical_key = match key.as_slice() { - b"W" => b"Width".to_vec(), - b"H" => b"Height".to_vec(), - b"BPC" => b"BitsPerComponent".to_vec(), - b"CS" => b"ColorSpace".to_vec(), - b"IM" => b"ImageMask".to_vec(), - b"I" => b"Interpolate".to_vec(), - b"F" => b"Filter".to_vec(), - b"D" => b"Decode".to_vec(), - b"DP" => b"DecodeParms".to_vec(), + let mut normalized = Dictionary::default(); + normalized.object_number = dictionary.object_number; + + // Entries arrive in key order, where each abbreviation sorts before its full name. + for (key, value) in dictionary { + let canonical_key = match key.as_bytes() { + b"W" => PdfName::from(b"Width"), + b"H" => PdfName::from(b"Height"), + b"BPC" => PdfName::from(b"BitsPerComponent"), + b"CS" => PdfName::from(b"ColorSpace"), + b"IM" => PdfName::from(b"ImageMask"), + b"I" => PdfName::from(b"Interpolate"), + b"F" => PdfName::from(b"Filter"), + b"D" => PdfName::from(b"Decode"), + b"DP" => PdfName::from(b"DecodeParms"), _ => key, }; + if normalized.get(canonical_key.as_bytes()).is_some() { + continue; + } - let canonical_value = match canonical_key.as_slice() { + let canonical_value = match canonical_key.as_bytes() { b"ColorSpace" => normalize_inline_color_space(value), _ => value, }; - - normalized.entry(canonical_key).or_insert(canonical_value); + normalized.insert(canonical_key, canonical_value); } - Dictionary { - dictionary: normalized, - object_number: dictionary.object_number, - } + normalized } /// Expands abbreviated device colour-space names, including inside an array. @@ -206,7 +207,7 @@ mod tests { #[test] fn normalize_inline_image_dictionary_expands_abbreviations() { - let dictionary = pdf_object_reader::dictionary::Dictionary::new(BTreeMap::from([ + let dictionary = pdf_object_reader::dictionary::Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(8)), ( Vec::from(b"CS"), @@ -233,8 +234,8 @@ mod tests { let normalized = normalize_inline_image_dictionary(dictionary); assert_eq!( - normalized.dictionary, - BTreeMap::from([ + normalized, + Dictionary::from_iter(BTreeMap::from([ (b"BitsPerComponent".to_vec(), ObjectVariant::Integer(8)), ( b"ColorSpace".to_vec(), @@ -256,7 +257,7 @@ mod tests { (b"ImageMask".to_vec(), ObjectVariant::Boolean(true)), (b"Interpolate".to_vec(), ObjectVariant::Boolean(false)), (b"Width".to_vec(), ObjectVariant::Integer(1)), - ]) + ])) ); } @@ -270,18 +271,19 @@ mod tests { ]; for (abbreviated, canonical) in cases { - let dictionary = pdf_object_reader::dictionary::Dictionary::new(BTreeMap::from([( - Vec::from(b"CS"), - pdf_object_reader::pdf_string::PdfString::from( - abbreviated.as_bytes(), - pdf_object_reader::string_kind::StringKind::Name, - ), - )])); + let dictionary = + pdf_object_reader::dictionary::Dictionary::from_iter(BTreeMap::from([( + Vec::from(b"CS"), + pdf_object_reader::pdf_string::PdfString::from( + abbreviated.as_bytes(), + pdf_object_reader::string_kind::StringKind::Name, + ), + )])); let normalized = normalize_inline_image_dictionary(dictionary); assert_eq!( - normalized.dictionary.get(b"ColorSpace".as_slice()), + normalized.get(b"ColorSpace".as_slice()), Some(&pdf_object_reader::pdf_string::PdfString::from( canonical.as_bytes(), pdf_object_reader::string_kind::StringKind::Name @@ -292,7 +294,7 @@ mod tests { #[test] fn normalize_inline_image_dictionary_expands_indexed_color_space_arrays() { - let dictionary = pdf_object_reader::dictionary::Dictionary::new(BTreeMap::from([( + let dictionary = pdf_object_reader::dictionary::Dictionary::from_iter(BTreeMap::from([( Vec::from(b"CS"), ObjectVariant::Array( vec![ @@ -317,7 +319,7 @@ mod tests { let normalized = normalize_inline_image_dictionary(dictionary); assert_eq!( - normalized.dictionary.get(b"ColorSpace".as_slice()), + normalized.get(b"ColorSpace".as_slice()), Some(&ObjectVariant::Array( vec![ pdf_object_reader::pdf_string::PdfString::from( @@ -355,7 +357,7 @@ mod tests { #[test] fn inline_image_applies_filters_during_construction() { let mut dictionary = gray_dictionary(); - dictionary.dictionary.insert( + dictionary.insert( b"F".to_vec(), pdf_object_reader::pdf_string::PdfString::from( b"ASCIIHexDecode", @@ -376,7 +378,7 @@ mod tests { #[test] fn invalid_metadata_is_rejected_during_construction() { let error = PdfImage::inline( - Dictionary::new(BTreeMap::, ObjectVariant>::new()), + Dictionary::default(), vec![1], &mut pdf_object_reader::ObjectReader::new(EmptySource).session(), ) @@ -389,7 +391,7 @@ mod tests { } fn gray_dictionary() -> Dictionary { - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BPC"), ObjectVariant::Integer(8)), ( Vec::from(b"CS"), diff --git a/crates/pdf-object-collection/src/object_collection.rs b/crates/pdf-object-collection/src/object_collection.rs index 1fc1dfb5..5dd8b492 100644 --- a/crates/pdf-object-collection/src/object_collection.rs +++ b/crates/pdf-object-collection/src/object_collection.rs @@ -217,7 +217,7 @@ impl ObjectCollection { #[cfg(feature = "json")] fn dictionary_to_json(dict: &pdf_object_reader::dictionary::Dictionary) -> JsonValue { let mut map = serde_json::Map::new(); - for (key, value) in &dict.dictionary { + for (key, value) in dict.iter() { map.insert( String::from_utf8_lossy(key).into_owned(), Self::object_variant_to_json(value), @@ -280,12 +280,7 @@ mod tests { fn inserting_unfiltered_stream_preserves_the_byte_allocation() { let data = vec![1, 2, 3, 4]; let original = data.as_ptr(); - let stream = StreamObject::new( - 99, - 4, - Dictionary::new(BTreeMap::, ObjectVariant>::new()), - data, - ); + let stream = StreamObject::new(99, 4, Dictionary::default(), data); let mut collection = ObjectCollection::default(); collection @@ -319,9 +314,7 @@ mod tests { number: 7, generation: 2, }, - ObjectVariant::Dictionary(Dictionary::new( - BTreeMap::, ObjectVariant>::new(), - )), + ObjectVariant::Dictionary(Dictionary::default()), ) .expect("dictionary insert failed"); @@ -353,7 +346,7 @@ mod tests { decode_parms.insert(Vec::from(b"Colors"), ObjectVariant::Integer(1)); decode_parms.insert(Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)); - let decode_parms_object = ObjectVariant::Dictionary(Dictionary::new(decode_parms)); + let decode_parms_object = ObjectVariant::Dictionary(Dictionary::from_iter(decode_parms)); let mut collection = ObjectCollection::default(); collection @@ -386,7 +379,7 @@ mod tests { let stream = ObjectVariant::Stream(StreamObject::new_encoded( 1, 0, - Dictionary::new(stream_dict), + Dictionary::from_iter(stream_dict), compressed, )); @@ -410,7 +403,7 @@ mod tests { #[test] fn insert_preserves_encoded_state_when_filter_dependencies_are_unresolved() { - let stream_dictionary = Dictionary::new(BTreeMap::from([ + let stream_dictionary = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"DecodeParms"), ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new(2, 0)), @@ -451,9 +444,7 @@ mod tests { number: 2, generation: 0, }, - ObjectVariant::Dictionary(Dictionary::new( - BTreeMap::, ObjectVariant>::new(), - )), + ObjectVariant::Dictionary(Dictionary::default()), ) .expect("decode parameters should be inserted"); diff --git a/crates/pdf-object-reader/src/context.rs b/crates/pdf-object-reader/src/context.rs index d8e4c355..b925051a 100644 --- a/crates/pdf-object-reader/src/context.rs +++ b/crates/pdf-object-reader/src/context.rs @@ -124,7 +124,7 @@ impl<'read, A: ObjectAccess + ?Sized> DictionaryContext<'read, A> { /// The names borrow the dictionary rather than the context, so entries can be read /// while iterating. pub fn keys(&self) -> impl Iterator + use<'read, A> { - self.dictionary.dictionary.keys().map(Vec::as_slice) + self.dictionary.keys() } /// Returns the number of the indirect object holding this dictionary, if it is one. @@ -241,16 +241,16 @@ impl<'read, A: ObjectAccess + ?Sized> DictionaryContext<'read, A> { key: &[u8], mut decode: impl FnMut(&mut Self, &ObjectVariant) -> ReadResult, ) -> ReadResult, T>> { - let Some(dictionary) = self.optional::(key)? else { - return Ok(HashMap::new()); - }; - let mut entries = HashMap::with_capacity(dictionary.dictionary.len()); - for (name, value) in &dictionary.dictionary { - if !self.is_absent(value)? { - entries.insert(name.clone(), decode(self, value)?); + let entries = self.with_optional_dictionary(key, |context, dictionary| { + let mut entries = HashMap::with_capacity(dictionary.len()); + for (name, value) in dictionary.iter() { + if !context.is_absent(value)? { + entries.insert(name.to_vec(), decode(context, value)?); + } } - } - Ok(entries) + Ok(entries) + })?; + Ok(entries.unwrap_or_default()) } /// Decodes each entry of an optional sub-dictionary as `T`, keyed by entry name, skipping @@ -261,14 +261,36 @@ impl<'read, A: ObjectAccess + ?Sized> DictionaryContext<'read, A> { &mut self, key: &[u8], ) -> ReadResult, T>> { - let Some(dictionary) = self.optional::(key)? else { - return Ok(HashMap::new()); + let entries = self.with_optional_dictionary(key, |context, dictionary| { + Ok(dictionary + .iter() + .filter_map(|(name, value)| Some((name.to_vec(), context.access.read(value).ok()?))) + .collect()) + })?; + Ok(entries.unwrap_or_default()) + } + + /// Runs `visit` on an optional sub-dictionary, borrowed from its resolved object rather + /// than copied. A missing or null entry yields `None`. + fn with_optional_dictionary( + &mut self, + key: &[u8], + visit: impl FnOnce(&mut Self, &Dictionary) -> ReadResult, + ) -> ReadResult> { + let Some(object) = self.resolve_entry(key)? else { + return Ok(None); }; - Ok(dictionary - .dictionary - .iter() - .filter_map(|(name, value)| Some((name.clone(), self.access.read(value).ok()?))) - .collect()) + match object.value() { + ObjectVariant::Null => Ok(None), + ObjectVariant::Dictionary(dictionary) => visit(self, dictionary).map(Some), + other => Err(ObjectReadError::At { + location: ReadLocation::DictionaryKey(PdfName::from_bytes(key)), + source: Box::new(ObjectReadError::TypeMismatch { + expected: ObjectKind::Dictionary, + actual: other.kind(), + }), + }), + } } /// Reads an entry holding one value or an array of values, as a list. diff --git a/crates/pdf-object-reader/src/decode.rs b/crates/pdf-object-reader/src/decode.rs index cd60e833..fbb6a284 100644 --- a/crates/pdf-object-reader/src/decode.rs +++ b/crates/pdf-object-reader/src/decode.rs @@ -326,11 +326,11 @@ impl FromPdfObject for MaskMode { } /// Reads a name, accepting a string in its place as producers substitute one for the -/// other. The bytes are shared with the object, not copied. +/// other. Short names are copied inline; longer ones share the object's bytes. impl FromPdfObject for PdfName { fn from_pdf_object(context: ObjectContext<'_, impl ObjectAccess + ?Sized>) -> ReadResult { match context.object() { - ObjectVariant::String(value) => Ok(Self(std::sync::Arc::clone(&value.bytes))), + ObjectVariant::String(value) => Ok(Self::from_shared(&value.bytes)), other => Err(ObjectError::TypeMismatch("Name", other.name()).into()), } } diff --git a/crates/pdf-object-reader/src/dictionary.rs b/crates/pdf-object-reader/src/dictionary.rs index 45baf884..c090c55e 100644 --- a/crates/pdf-object-reader/src/dictionary.rs +++ b/crates/pdf-object-reader/src/dictionary.rs @@ -1,32 +1,18 @@ -use std::collections::BTreeMap; +use crate::{object_id::ObjectId, object_variant::ObjectVariant, pdf_name::PdfName}; -use crate::{object_id::ObjectId, object_variant::ObjectVariant}; - -#[derive(Debug, PartialEq, Clone)] +/// Stores an immutable PDF dictionary. +/// +/// Entries are kept sorted by key with each key present once. +#[derive(Debug, PartialEq, Clone, Default)] pub struct Dictionary { - pub dictionary: BTreeMap, ObjectVariant>, + entries: Vec<(PdfName, ObjectVariant)>, pub object_number: Option, } impl Dictionary { - /// Creates a dictionary from already-owned PDF Name keys. - pub fn new(dictionary: BTreeMap, ObjectVariant>) -> Self { - Dictionary { - dictionary, - object_number: None, - } - } - /// Creates a hand-built dictionary from borrowed PDF Name keys. - /// - /// This keeps the unavoidable ownership copy at the dictionary boundary. pub fn from_entries(entries: [(&[u8], ObjectVariant); N]) -> Self { - Self::new( - entries - .into_iter() - .map(|(key, value)| (Vec::from(key), value)) - .collect(), - ) + entries.into_iter().collect() } /// Returns a reference to the value associated with the given key, if present. @@ -39,24 +25,37 @@ impl Dictionary { /// /// Returns an optional reference to [`ObjectVariant`] when the key exists, or `None` if it does not. pub fn get(&self, key: &[u8]) -> Option<&ObjectVariant> { - self.dictionary.get(key) + let index = self.position(key).ok()?; + self.entries.get(index).map(|(_, value)| value) } /// Iterates the entries in key order, yielding each PDF Name key as a byte slice. pub fn iter(&self) -> impl Iterator { - self.dictionary + self.entries .iter() - .map(|(key, value)| (key.as_slice(), value)) + .map(|(key, value)| (key.as_bytes(), value)) + } + + /// Iterates the entries in key order with mutable access to each value. + pub fn iter_mut(&mut self) -> impl Iterator { + self.entries + .iter_mut() + .map(|(key, value)| (key.as_bytes(), value)) + } + + /// Iterates the entry names in key order. + pub fn keys(&self) -> impl Iterator { + self.entries.iter().map(|(key, _)| key.as_bytes()) } /// Returns the number of entries. pub fn len(&self) -> usize { - self.dictionary.len() + self.entries.len() } /// Returns `true` when the dictionary has no entries. pub fn is_empty(&self) -> bool { - self.dictionary.is_empty() + self.entries.is_empty() } /// Removes and returns the value associated with the given key, if present. @@ -69,7 +68,27 @@ impl Dictionary { /// /// Returns an `Option` containing the [`ObjectVariant`] if the key exists, or `None` if it does not. pub fn take(&mut self, key: &[u8]) -> Option { - self.dictionary.remove(key) + let index = self.position(key).ok()?; + (index < self.entries.len()).then(|| self.entries.remove(index).1) + } + + /// Sets the value of `key`, returning the value it replaces, if any. + pub fn insert( + &mut self, + key: impl Into, + value: ObjectVariant, + ) -> Option { + let key = key.into(); + match self.position(key.as_bytes()) { + Ok(index) => self + .entries + .get_mut(index) + .map(|(_, existing)| std::mem::replace(existing, value)), + Err(index) => { + self.entries.insert(index, (key, value)); + None + } + } } /// Returns the `/Parent` entry and its object id when it is an indirect reference. @@ -79,4 +98,33 @@ impl Dictionary { _ => None, } } + + /// Finds the index of `key`, or where it would be inserted. + #[inline] + fn position(&self, key: &[u8]) -> Result { + self.entries + .binary_search_by(|(name, _)| name.as_bytes().cmp(key)) + } +} + +/// Collects entries in source order; when a key repeats, the later definition replaces +/// the earlier one. +impl> FromIterator<(K, ObjectVariant)> for Dictionary { + fn from_iter>(entries: T) -> Self { + let mut dictionary = Self::default(); + for (key, value) in entries { + dictionary.insert(key, value); + } + dictionary + } +} + +impl IntoIterator for Dictionary { + type Item = (PdfName, ObjectVariant); + type IntoIter = std::vec::IntoIter<(PdfName, ObjectVariant)>; + + /// Moves the entries out in key order. + fn into_iter(self) -> Self::IntoIter { + self.entries.into_iter() + } } diff --git a/crates/pdf-object-reader/src/pdf_name.rs b/crates/pdf-object-reader/src/pdf_name.rs index 4b128d13..46ccf3d3 100644 --- a/crates/pdf-object-reader/src/pdf_name.rs +++ b/crates/pdf-object-reader/src/pdf_name.rs @@ -1,21 +1,81 @@ //! PDF names. use std::borrow::Borrow; +use std::cmp::Ordering; +use std::fmt; +use std::hash::{Hash, Hasher}; use std::sync::Arc; -/// Stores a PDF name as immutable shared bytes. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PdfName(pub(crate) Arc<[u8]>); +/// The longest name stored without a heap allocation. +/// +/// With the length byte and the enum tag this keeps a name the size of a `Vec`. +const INLINE_CAPACITY: usize = 22; + +/// The storage of a [`PdfName`]: short names live inline, longer ones are shared. +#[derive(Clone)] +enum NameStorage { + Inline { + len: u8, + bytes: [u8; INLINE_CAPACITY], + }, + Shared(Arc<[u8]>), +} + +/// Stores a PDF name as immutable bytes. +/// +/// Names of up to 22 bytes, which covers nearly every dictionary key and name +/// value in practice, are stored inline without a heap allocation. Longer +/// names share one immutable allocation between clones. +#[derive(Clone)] +pub struct PdfName(NameStorage); impl PdfName { /// Copies a byte sequence into a new PDF name. pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - Self(Arc::from(bytes.as_ref())) + let bytes = bytes.as_ref(); + let mut inline = [0; INLINE_CAPACITY]; + match (u8::try_from(bytes.len()), inline.get_mut(..bytes.len())) { + (Ok(len), Some(prefix)) => { + prefix.copy_from_slice(bytes); + Self(NameStorage::Inline { len, bytes: inline }) + } + _ => Self(NameStorage::Shared(Arc::from(bytes))), + } } /// Returns the decoded bytes of the name without a leading slash. pub fn as_bytes(&self) -> &[u8] { - &self.0 + match &self.0 { + NameStorage::Inline { len, bytes } => bytes.get(..usize::from(*len)).unwrap_or(&[]), + NameStorage::Shared(bytes) => bytes, + } + } + + /// Shares the bytes of an existing allocation, copying them inline when they are short. + pub(crate) fn from_shared(bytes: &Arc<[u8]>) -> Self { + if bytes.len() <= INLINE_CAPACITY { + Self::from_bytes(bytes.as_ref()) + } else { + Self(NameStorage::Shared(Arc::clone(bytes))) + } + } +} + +impl From<&[u8]> for PdfName { + fn from(bytes: &[u8]) -> Self { + Self::from_bytes(bytes) + } +} + +impl From<&[u8; N]> for PdfName { + fn from(bytes: &[u8; N]) -> Self { + Self::from_bytes(bytes) + } +} + +impl From> for PdfName { + fn from(bytes: Vec) -> Self { + Self::from_bytes(bytes) } } @@ -24,3 +84,41 @@ impl Borrow<[u8]> for PdfName { self.as_bytes() } } + +impl AsRef<[u8]> for PdfName { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl PartialEq for PdfName { + fn eq(&self, other: &Self) -> bool { + self.as_bytes() == other.as_bytes() + } +} + +impl Eq for PdfName {} + +impl PartialOrd for PdfName { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PdfName { + fn cmp(&self, other: &Self) -> Ordering { + self.as_bytes().cmp(other.as_bytes()) + } +} + +impl Hash for PdfName { + fn hash(&self, state: &mut H) { + self.as_bytes().hash(state); + } +} + +impl fmt::Debug for PdfName { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "/{}", self.as_bytes().escape_ascii()) + } +} diff --git a/crates/pdf-object-reader/src/stream.rs b/crates/pdf-object-reader/src/stream.rs index 51a58e95..6cd5d421 100644 --- a/crates/pdf-object-reader/src/stream.rs +++ b/crates/pdf-object-reader/src/stream.rs @@ -89,26 +89,13 @@ impl StreamObject { #[cfg(test)] mod tests { - use std::collections::BTreeMap; - use super::StreamObject; use crate::dictionary::Dictionary; - use crate::object_variant::ObjectVariant; #[test] fn constructors_record_filter_state() { - let decoded = StreamObject::new( - 1, - 0, - Dictionary::new(BTreeMap::, ObjectVariant>::new()), - vec![1], - ); - let encoded = StreamObject::new_encoded( - 2, - 0, - Dictionary::new(BTreeMap::, ObjectVariant>::new()), - vec![2], - ); + let decoded = StreamObject::new(1, 0, Dictionary::default(), vec![1]); + let encoded = StreamObject::new_encoded(2, 0, Dictionary::default(), vec![2]); assert!(decoded.filters_applied()); assert!(!encoded.filters_applied()); @@ -116,12 +103,7 @@ mod tests { #[test] fn setting_filtered_data_updates_bytes_and_state() { - let mut stream = StreamObject::new_encoded( - 1, - 0, - Dictionary::new(BTreeMap::, ObjectVariant>::new()), - vec![1], - ); + let mut stream = StreamObject::new_encoded(1, 0, Dictionary::default(), vec![1]); stream.set_filtered_data(vec![2, 3]); diff --git a/crates/pdf-parser/src/cross_reference_stream.rs b/crates/pdf-parser/src/cross_reference_stream.rs index e42c3af6..0f0ef409 100644 --- a/crates/pdf-parser/src/cross_reference_stream.rs +++ b/crates/pdf-parser/src/cross_reference_stream.rs @@ -217,7 +217,7 @@ mod tests { ObjectVariant::Integer(raw_data.len() as i64), ); - StreamObject::new_encoded(0, 0, Dictionary::new(dict_map), raw_data) + StreamObject::new_encoded(0, 0, Dictionary::from_iter(dict_map), raw_data) } #[test] @@ -384,24 +384,20 @@ mod tests { ); dict_map.insert( Vec::from(b"DecodeParms"), - ObjectVariant::Dictionary(Dictionary::new( - vec![ - ( - Vec::from(b"Columns"), - ObjectVariant::Integer(columns as i64), - ), - (Vec::from(b"Predictor"), ObjectVariant::Integer(12)), - ] - .into_iter() - .collect(), - )), + ObjectVariant::Dictionary(Dictionary::from_iter(vec![ + ( + Vec::from(b"Columns"), + ObjectVariant::Integer(columns as i64), + ), + (Vec::from(b"Predictor"), ObjectVariant::Integer(12)), + ])), ); dict_map.insert( Vec::from(b"Length"), ObjectVariant::Integer(compressed.len() as i64), ); - StreamObject::new_encoded(0, 0, Dictionary::new(dict_map), compressed) + StreamObject::new_encoded(0, 0, Dictionary::from_iter(dict_map), compressed) } #[test] diff --git a/crates/pdf-parser/src/dictionary.rs b/crates/pdf-parser/src/dictionary.rs index 4156456f..fb7e0989 100644 --- a/crates/pdf-parser/src/dictionary.rs +++ b/crates/pdf-parser/src/dictionary.rs @@ -1,8 +1,6 @@ -use std::collections::BTreeMap; - use pdf_object_reader::{ - dictionary::Dictionary, object_variant::ObjectVariant, pdf_string::PdfString, - string_kind::StringKind, + dictionary::Dictionary, object_variant::ObjectVariant, pdf_name::PdfName, + pdf_string::PdfString, string_kind::StringKind, }; use pdf_tokenizer::PdfToken; @@ -10,8 +8,8 @@ use crate::{error::ParserError, parser::PdfParser}; enum DictionaryEntryState { ExpectKeyOrTerminator, - ExpectValue { key: Vec }, - ContinueNameValue { key: Vec, value: Vec }, + ExpectValue { key: PdfName }, + ContinueNameValue { key: PdfName, value: PdfName }, } impl PdfParser<'_> { @@ -37,7 +35,7 @@ impl PdfParser<'_> { /// Parses the key-value pairs inside a regular dictionary until `>>`. fn parse_dictionary_entries(&mut self) -> Result { - let mut dictionary = BTreeMap::new(); + let mut dictionary = Dictionary::default(); let mut state = DictionaryEntryState::ExpectKeyOrTerminator; loop { @@ -52,7 +50,7 @@ impl PdfParser<'_> { if matches!(state, DictionaryEntryState::ExpectKeyOrTerminator) && self.is_at_dictionary_end() { - return Ok(Dictionary::new(dictionary)); + return Ok(dictionary); } state = self.next_dictionary_state(&mut dictionary, state)?; @@ -64,14 +62,14 @@ impl PdfParser<'_> { /// This stops as soon as the next non-whitespace token is no longer a name /// key, leaving `ID` in the input stream for the caller to consume. pub(crate) fn parse_inline_image_entries(&mut self) -> Result { - let mut dictionary = BTreeMap::new(); + let mut dictionary = Dictionary::default(); loop { self.skip_whitespace_and_comments(); match self.tokenizer.peek() { Some(PdfToken::Solidus) => {} - Some(_) => return Ok(Dictionary::new(dictionary)), + Some(_) => return Ok(dictionary), None => return Err(ParserError::UnexpectedEndOfFile), } @@ -96,7 +94,7 @@ impl PdfParser<'_> { /// Advances the regular dictionary state machine by one step. fn next_dictionary_state( &mut self, - dictionary: &mut BTreeMap, ObjectVariant>, + dictionary: &mut Dictionary, state: DictionaryEntryState, ) -> Result { match state { @@ -122,8 +120,8 @@ impl PdfParser<'_> { /// characters so spaced name values remain intact. fn parse_dictionary_value_state( &mut self, - dictionary: &mut BTreeMap, ObjectVariant>, - key: Vec, + dictionary: &mut Dictionary, + key: PdfName, ) -> Result { match self.tokenizer.peek() { Some(PdfToken::Solidus) => { @@ -141,9 +139,9 @@ impl PdfParser<'_> { /// Extends a spaced name value until it reaches a real delimiter or key boundary. fn continue_dictionary_name_value( &mut self, - dictionary: &mut BTreeMap, ObjectVariant>, - key: Vec, - mut value: Vec, + dictionary: &mut Dictionary, + key: PdfName, + value: PdfName, ) -> Result { if self.is_at_dictionary_end() || matches!(self.tokenizer.peek(), Some(PdfToken::Solidus)) { dictionary.insert(key, PdfString::from(value, StringKind::Name)); @@ -160,10 +158,11 @@ impl PdfParser<'_> { } let suffix = self.tokenizer.read_while_u8(Self::is_pdf_regular_character); - if !suffix.is_empty() { - value.push(b' '); - value.extend_from_slice(suffix); - } + let value = if suffix.is_empty() { + value + } else { + PdfName::from([value.as_bytes(), suffix].join(&b' ')) + }; Ok(DictionaryEntryState::ContinueNameValue { key, value }) } @@ -202,12 +201,12 @@ mod tests { let result = parser.parse_dictionary().unwrap(); assert_eq!( - result.dictionary.len(), + result.len(), expected_count, "Expected {} elements for input '{}', got {}", expected_count, String::from_utf8_lossy(input), - result.dictionary.len() + result.len() ); } } @@ -324,7 +323,7 @@ mod tests { let mut parser = PdfParser::from(b"/IM true /W 1 /H 1 ID \x00\x01".as_slice()); let result = parser.parse_inline_image_entries().unwrap(); - assert_eq!(result.dictionary.len(), 3); + assert_eq!(result.len(), 3); assert!(parser.tokenizer.data().starts_with(b"ID \x00\x01")); } diff --git a/crates/pdf-parser/src/inline_image.rs b/crates/pdf-parser/src/inline_image.rs index 2af82c54..bf3fa2c4 100644 --- a/crates/pdf-parser/src/inline_image.rs +++ b/crates/pdf-parser/src/inline_image.rs @@ -279,7 +279,7 @@ mod tests { let mut parser = PdfParser::from(b"ID x\nEI".as_slice()); let dictionary = parser.parse_inline_image_dictionary().unwrap(); - assert!(dictionary.dictionary.is_empty()); + assert!(dictionary.is_empty()); assert_eq!(parser.parse_inline_image_data(None).unwrap(), b"x\n"); assert_eq!(parser.tokenizer.data(), b""); } diff --git a/crates/pdf-parser/src/name.rs b/crates/pdf-parser/src/name.rs index b1f1a551..a11cc4a9 100644 --- a/crates/pdf-parser/src/name.rs +++ b/crates/pdf-parser/src/name.rs @@ -1,3 +1,4 @@ +use pdf_object_reader::pdf_name::PdfName; use pdf_tokenizer::{PdfToken, error::TokenizerError}; use thiserror::Error; @@ -21,13 +22,17 @@ impl PdfParser<'_> { /// /// # Returns /// - /// A `Name` object containing the decoded name string (with hex escapes resolved), - /// or a `NameObjectError` on failure. - pub fn parse_name(&mut self) -> Result, NameObjectError> { + /// The decoded name (with hex escapes resolved), or a `NameObjectError` on failure. + /// A name without escapes is copied straight from the input. + pub fn parse_name(&mut self) -> Result { self.tokenizer.expect(PdfToken::Solidus)?; let name = self.tokenizer.read_while_u8(|b| !Self::is_pdf_delimiter(b)); - escape(name) + if name.contains(&b'#') { + escape(name).map(PdfName::from) + } else { + Ok(PdfName::from_bytes(name)) + } } } @@ -112,7 +117,7 @@ mod tests { let mut parser = PdfParser::from(input); let value = parser.parse_name().unwrap(); assert_eq!( - value, + value.as_bytes(), expected, "input: `{}`", String::from_utf8_lossy(input) diff --git a/crates/pdf-resources/src/external_graphics_state.rs b/crates/pdf-resources/src/external_graphics_state.rs index b64eb065..b863777d 100644 --- a/crates/pdf-resources/src/external_graphics_state.rs +++ b/crates/pdf-resources/src/external_graphics_state.rs @@ -177,7 +177,7 @@ mod tests { use super::{ExternalGraphicsState, ExternalGraphicsStateKey}; fn dash_dict(dash_array: Vec, dash_phase: f32) -> Dictionary { - Dictionary::new(BTreeMap::from([( + Dictionary::from_iter(BTreeMap::from([( Vec::from(b"D"), ObjectVariant::Array( vec![ @@ -268,7 +268,7 @@ mod tests { #[test] fn soft_mask_none_is_preserved() { - let dictionary = Dictionary::new(BTreeMap::from([( + let dictionary = Dictionary::from_iter(BTreeMap::from([( Vec::from(b"SMask"), pdf_object_reader::pdf_string::PdfString::from( b"None", @@ -288,7 +288,7 @@ mod tests { #[test] fn invalid_soft_mask_name_is_rejected() { - let dictionary = Dictionary::new(BTreeMap::from([( + let dictionary = Dictionary::from_iter(BTreeMap::from([( Vec::from(b"SMask"), pdf_object_reader::pdf_string::PdfString::from( b"Invalid", diff --git a/crates/pdf-resources/src/form.rs b/crates/pdf-resources/src/form.rs index 9d22488f..4e93b707 100644 --- a/crates/pdf-resources/src/form.rs +++ b/crates/pdf-resources/src/form.rs @@ -59,7 +59,7 @@ mod tests { #[test] fn read_xobject_normalizes_inverted_bbox() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"BBox"), ObjectVariant::Array( @@ -97,7 +97,7 @@ mod tests { #[test] fn empty_from_dictionary_creates_empty_content_stream() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"BBox"), ObjectVariant::Array( diff --git a/crates/pdf-resources/src/soft_mask.rs b/crates/pdf-resources/src/soft_mask.rs index 76fcac0c..10cf7952 100644 --- a/crates/pdf-resources/src/soft_mask.rs +++ b/crates/pdf-resources/src/soft_mask.rs @@ -112,7 +112,7 @@ mod tests { use super::{MaskTransfer, SoftMask}; fn soft_mask_dictionary(stream_object_number: usize, subtype: &str) -> Dictionary { - let form_dictionary = Dictionary::new(BTreeMap::from([ + let form_dictionary = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"BBox"), ObjectVariant::Array( @@ -135,7 +135,7 @@ mod tests { ])); let stream = StreamObject::new(stream_object_number, 0, form_dictionary, Vec::new()); - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"G"), ObjectVariant::Stream(stream)), ( Vec::from(b"S"), @@ -170,9 +170,7 @@ mod tests { ]), b"{1 exch sub}".to_vec(), ); - dictionary - .dictionary - .insert(b"TR".to_vec(), ObjectVariant::Stream(function)); + dictionary.insert(b"TR".to_vec(), ObjectVariant::Stream(function)); let reader = pdf_object_reader::ObjectReader::new(EmptySource); let mask = reader .read::(&ObjectVariant::Dictionary(dictionary)) @@ -186,9 +184,7 @@ mod tests { #[test] fn identity_mask_transfer_needs_no_table() { let mut dictionary = soft_mask_dictionary(7, "Form"); - dictionary - .dictionary - .insert(b"TR".to_vec(), ObjectVariant::name_from_bytes(b"Identity")); + dictionary.insert(b"TR".to_vec(), ObjectVariant::name_from_bytes(b"Identity")); let reader = pdf_object_reader::ObjectReader::new(EmptySource); let mask = reader .read::(&ObjectVariant::Dictionary(dictionary)) @@ -229,7 +225,7 @@ mod tests { let Some(ObjectVariant::Stream(mut stream)) = dictionary.take(b"G") else { panic!("group stream"); }; - stream.dictionary.dictionary.insert( + stream.dictionary.insert( b"Resources".to_vec(), ObjectVariant::Dictionary(Dictionary::from_entries([( b"ExtGState".as_slice(), @@ -242,9 +238,7 @@ mod tests { )])), )])), ); - dictionary - .dictionary - .insert(b"G".to_vec(), ObjectVariant::Reference(ObjectId::new(7, 0))); + dictionary.insert(b"G".to_vec(), ObjectVariant::Reference(ObjectId::new(7, 0))); let mut objects = ObjectCollection::default(); objects .insert(ObjectId::new(7, 0), ObjectVariant::Stream(stream)) diff --git a/crates/pdf-resources/src/xobject.rs b/crates/pdf-resources/src/xobject.rs index 69944df4..7299876a 100644 --- a/crates/pdf-resources/src/xobject.rs +++ b/crates/pdf-resources/src/xobject.rs @@ -178,7 +178,7 @@ mod tests { } fn image_dictionary(object_number: usize) -> Dictionary { - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Subtype"), pdf_object_reader::pdf_string::PdfString::from( @@ -234,7 +234,7 @@ mod tests { let image_stream = StreamObject::new( 1, 0, - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Subtype"), pdf_object_reader::pdf_string::PdfString::from( @@ -262,7 +262,7 @@ mod tests { let mask_stream = StreamObject::new( 2, 0, - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Subtype"), pdf_object_reader::pdf_string::PdfString::from( @@ -316,7 +316,7 @@ mod tests { let mask_stream = StreamObject::new( 2, 0, - Dictionary::new(BTreeMap::from([( + Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Subtype"), pdf_object_reader::pdf_string::PdfString::from( b"Unsupported", @@ -344,7 +344,7 @@ mod tests { #[test] fn encoded_image_retries_filters_after_dependencies_are_resolved() { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"BitsPerComponent"), ObjectVariant::Integer(8)), ( Vec::from(b"ColorSpace"), @@ -383,9 +383,7 @@ mod tests { objects .insert( object_id(2), - ObjectVariant::Dictionary(Dictionary::new( - BTreeMap::, ObjectVariant>::new(), - )), + ObjectVariant::Dictionary(Dictionary::default()), ) .expect("decode parameters should be inserted"); @@ -412,7 +410,7 @@ mod tests { let stream = StreamObject::new( 9, 0, - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Subtype"), pdf_object_reader::pdf_string::PdfString::from( diff --git a/crates/pdf-resources/tests/resources.rs b/crates/pdf-resources/tests/resources.rs index 45f2d513..f4b66754 100644 --- a/crates/pdf-resources/tests/resources.rs +++ b/crates/pdf-resources/tests/resources.rs @@ -37,7 +37,7 @@ fn array(values: Vec) -> ObjectVariant { } fn type2_function(c0: Vec, c1: Vec) -> ObjectVariant { - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([ + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"FunctionType"), integer(2)), (Vec::from(b"Domain"), array(vec![real(0.0), real(1.0)])), (Vec::from(b"C0"), array(c0)), @@ -47,7 +47,7 @@ fn type2_function(c0: Vec, c1: Vec) -> ObjectVaria } fn inline_axial_shading() -> ObjectVariant { - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([ + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"ShadingType"), integer(2)), (Vec::from(b"ColorSpace"), name("DeviceRGB")), ( @@ -65,7 +65,7 @@ fn inline_axial_shading() -> ObjectVariant { } fn form_xobject_stream(object_number: usize, data: &[u8]) -> ObjectVariant { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Subtype"), pdf_object_reader::pdf_string::PdfString::from( @@ -95,7 +95,7 @@ fn recursive_form_xobject_stream( nested_object_number: usize, data: &[u8], ) -> ObjectVariant { - let dictionary = Dictionary::new(BTreeMap::from([ + let dictionary = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Subtype"), pdf_object_reader::pdf_string::PdfString::from( @@ -111,9 +111,9 @@ fn recursive_form_xobject_stream( ), ( Vec::from(b"Resources"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"XObject"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(nested_name.as_bytes()), ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new( nested_object_number, @@ -138,11 +138,11 @@ fn xobject_resources(entries: Vec<(&str, ObjectVariant)>) -> Dictionary { .map(|(name, value)| (Vec::from(name.as_bytes()), value)) .collect::>(); - Dictionary::new(BTreeMap::from([( + Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Resources"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"XObject"), - ObjectVariant::Dictionary(Dictionary::new(xobjects)), + ObjectVariant::Dictionary(Dictionary::from_iter(xobjects)), )]))), )])) } @@ -153,11 +153,11 @@ fn page_resources(entries: Vec<(&str, ObjectVariant)>) -> Dictionary { .map(|(name, value)| (Vec::from(name.as_bytes()), value)) .collect::>(); - Dictionary::new(BTreeMap::from([( + Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Resources"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Shading"), - ObjectVariant::Dictionary(Dictionary::new(shading_entries)), + ObjectVariant::Dictionary(Dictionary::from_iter(shading_entries)), )]))), )])) } @@ -166,13 +166,13 @@ fn type3_char_proc(data: &[u8]) -> ObjectVariant { ObjectVariant::Stream(StreamObject::new( 0, 0, - Dictionary::new(BTreeMap::, ObjectVariant>::new()), + Dictionary::default(), data.to_vec(), )) } fn self_referential_type3_font(object_number: usize) -> Dictionary { - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"Type"), name("Font")), (Vec::from(b"Subtype"), name("Type3")), (Vec::from(b"Name"), name("Self")), @@ -198,16 +198,16 @@ fn self_referential_type3_font(object_number: usize) -> Dictionary { ), ( Vec::from(b"CharProcs"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"A"), type3_char_proc(b"0 0 d0"), )]))), ), ( Vec::from(b"Resources"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Font"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Self"), ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new( object_number, @@ -223,7 +223,7 @@ fn self_referential_tiling_pattern(object_number: usize) -> ObjectVariant { ObjectVariant::Stream(StreamObject::new( object_number, 0, - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"PatternType"), integer(1)), (Vec::from(b"PaintType"), integer(1)), (Vec::from(b"TilingType"), integer(1)), @@ -237,9 +237,9 @@ fn self_referential_tiling_pattern(object_number: usize) -> ObjectVariant { (Vec::from(b"YStep"), real(10.0)), ( Vec::from(b"Resources"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Pattern"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Self"), ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new( object_number, @@ -327,7 +327,7 @@ fn form_resources_accept_an_indirect_stream_dictionary() { let nested_resources = ObjectVariant::Stream(StreamObject::new( 15, 0, - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"Subtype"), name("Form")), ( Vec::from(b"BBox"), @@ -339,7 +339,7 @@ fn form_resources_accept_an_indirect_stream_dictionary() { let form = ObjectVariant::Stream(StreamObject::new( 14, 0, - Dictionary::new(BTreeMap::from([ + Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"Subtype"), name("Form")), ( Vec::from(b"BBox"), @@ -386,20 +386,20 @@ fn form_resources_accept_an_indirect_stream_dictionary() { #[test] fn dictionary_only_form_xobjects_are_loaded_as_empty_forms() { - let page_dict = Dictionary::new(BTreeMap::from([( + let page_dict = Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Resources"), ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new(100, 0)), )])); - let resources_dict = Dictionary::new(BTreeMap::from([( + let resources_dict = Dictionary::from_iter(BTreeMap::from([( Vec::from(b"XObject"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Meta6"), ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new(101, 0)), )]))), )])); - let form_dict = Dictionary::new(BTreeMap::from([ + let form_dict = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Subtype"), pdf_object_reader::pdf_string::PdfString::from( @@ -493,12 +493,12 @@ fn cyclic_form_resources_resolve_lazily_without_recursing_forever() { Vec::from(b"Self"), ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new(11, 0)), )]); - let resource_dict = Dictionary::new(BTreeMap::from([( + let resource_dict = Dictionary::from_iter(BTreeMap::from([( Vec::from(b"XObject"), - ObjectVariant::Dictionary(Dictionary::new(xobject_entries)), + ObjectVariant::Dictionary(Dictionary::from_iter(xobject_entries)), )])); - let form_dict = Dictionary::new(BTreeMap::from([ + let form_dict = Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Subtype"), pdf_object_reader::pdf_string::PdfString::from( @@ -518,7 +518,7 @@ fn cyclic_form_resources_resolve_lazily_without_recursing_forever() { ), ])); - let page_dict = Dictionary::new(BTreeMap::from([( + let page_dict = Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Resources"), ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new(10, 0)), )])); @@ -587,11 +587,11 @@ fn cyclic_form_resources_resolve_lazily_without_recursing_forever() { #[test] fn mutually_recursive_form_xobjects_resolve_lazily() { - let page_dict = Dictionary::new(BTreeMap::from([( + let page_dict = Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Resources"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"XObject"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"First"), ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new(6, 0)), )]))), @@ -648,11 +648,11 @@ fn mutually_recursive_form_xobjects_resolve_lazily() { #[test] fn self_referential_font_resources_resolve_lazily() { - let page_dict = Dictionary::new(BTreeMap::from([( + let page_dict = Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Resources"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Font"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Self"), ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new(21, 0)), )]))), @@ -699,7 +699,7 @@ fn self_referential_font_resources_resolve_lazily() { #[test] fn fallback_fonts_do_not_read_nested_type3_resources() { - let malformed_type3 = ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([ + let malformed_type3 = ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([ ( Vec::from(b"Subtype"), pdf_object_reader::pdf_string::PdfString::from( @@ -709,11 +709,11 @@ fn fallback_fonts_do_not_read_nested_type3_resources() { ), (Vec::from(b"Resources"), ObjectVariant::Integer(1)), ]))); - let page_dict = Dictionary::new(BTreeMap::from([( + let page_dict = Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Resources"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Font"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"F1"), malformed_type3, )]))), @@ -734,11 +734,11 @@ fn fallback_fonts_do_not_read_nested_type3_resources() { #[test] fn self_referential_pattern_resources_resolve_lazily() { - let page_dict = Dictionary::new(BTreeMap::from([( + let page_dict = Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Resources"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Pattern"), - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([( + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([( Vec::from(b"Self"), ObjectVariant::Reference(pdf_object_reader::object_id::ObjectId::new(31, 0)), )]))), diff --git a/crates/pdf-shading/tests/parse.rs b/crates/pdf-shading/tests/parse.rs index a50b7519..017c4dee 100644 --- a/crates/pdf-shading/tests/parse.rs +++ b/crates/pdf-shading/tests/parse.rs @@ -26,7 +26,7 @@ fn number_array(values: &[f32]) -> ObjectVariant { } fn type_2_rgb_function() -> ObjectVariant { - ObjectVariant::Dictionary(Dictionary::new(BTreeMap::from([ + ObjectVariant::Dictionary(Dictionary::from_iter(BTreeMap::from([ (Vec::from(b"FunctionType"), integer(2)), (Vec::from(b"Domain"), number_array(&[0.0, 1.0])), (Vec::from(b"C0"), number_array(&[1.0, 0.0, 0.0])), @@ -95,7 +95,12 @@ fn mesh_entries( } fn shading_stream(entries: BTreeMap, ObjectVariant>, data: Vec) -> ObjectVariant { - ObjectVariant::Stream(StreamObject::new(1, 0, Dictionary::new(entries), data)) + ObjectVariant::Stream(StreamObject::new( + 1, + 0, + Dictionary::from_iter(entries), + data, + )) } fn push_field(bits: &mut Vec, value: u32, width: usize) { diff --git a/tools/conformance/src/inventory.rs b/tools/conformance/src/inventory.rs index c4914d8b..fa333dda 100644 --- a/tools/conformance/src/inventory.rs +++ b/tools/conformance/src/inventory.rs @@ -251,12 +251,12 @@ fn walk( } } ObjectVariant::Dictionary(dictionary) => { - for value in dictionary.dictionary.values() { + for (_, value) in dictionary.iter() { walk(objects, value, next, features); } } ObjectVariant::Stream(stream) => { - for value in stream.dictionary.dictionary.values() { + for (_, value) in stream.dictionary.iter() { walk(objects, value, next, features); } }