use lopdf::{Document, Object, ObjectStream, dictionary}; #[test] fn test_multiple_trailer_references_all_compressible() { let mut doc = Document::with_version("1.5"); // Create multiple objects that will be referenced in trailer let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => Object::Reference((10, 0)) }); let info_id = doc.add_object(dictionary! { "Title" => "Test", "Author" => "Test Author" }); let metadata_id = doc.add_object(dictionary! { "Type" => "Metadata", "Subtype" => "XML" }); let outlines_id = doc.add_object(dictionary! { "Type" => "Outlines", "Count" => 0 }); // Add all to trailer doc.trailer.set("Root", catalog_id); doc.trailer.set("Info", info_id); doc.trailer.set("Metadata", metadata_id); doc.trailer.set("Outlines", outlines_id); // All should be compressible assert!( ObjectStream::can_be_compressed(catalog_id, doc.objects.get(&catalog_id).unwrap(), &doc), "Catalog should be compressible" ); assert!( ObjectStream::can_be_compressed(info_id, doc.objects.get(&info_id).unwrap(), &doc), "Info should be compressible" ); assert!( ObjectStream::can_be_compressed(metadata_id, doc.objects.get(&metadata_id).unwrap(), &doc), "Metadata should be compressible" ); assert!( ObjectStream::can_be_compressed(outlines_id, doc.objects.get(&outlines_id).unwrap(), &doc), "Outlines should be compressible" ); } #[test] fn test_encryption_dict_with_other_trailer_refs() { let mut doc = Document::with_version("1.5"); // Create catalog and encryption dictionary let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => Object::Reference((10, 0)) }); let encrypt_id = doc.add_object(dictionary! { "Filter" => "Standard", "V" => 2, "R" => 3, "Length" => 128 }); let info_id = doc.add_object(dictionary! { "Title" => "Encrypted PDF" }); // Add to trailer doc.trailer.set("Root", catalog_id); doc.trailer.set("Encrypt", encrypt_id); doc.trailer.set("Info", info_id); // Catalog and Info should be compressible, Encrypt should not assert!( ObjectStream::can_be_compressed(catalog_id, doc.objects.get(&catalog_id).unwrap(), &doc), "Catalog should be compressible even with encryption" ); assert!( !ObjectStream::can_be_compressed(encrypt_id, doc.objects.get(&encrypt_id).unwrap(), &doc), "Encryption dictionary should NOT be compressible" ); assert!( ObjectStream::can_be_compressed(info_id, doc.objects.get(&info_id).unwrap(), &doc), "Info should be compressible even with encryption" ); } #[test] fn test_trailer_with_non_reference_values() { let mut doc = Document::with_version("1.5"); let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog" }); // Add various types to trailer doc.trailer.set("Root", catalog_id); doc.trailer.set("Size", Object::Integer(100)); doc.trailer.set("Prev", Object::Integer(1234)); doc.trailer.set( "ID", Object::Array(vec![ Object::String(vec![1, 2, 3, 4], lopdf::StringFormat::Hexadecimal), Object::String(vec![5, 6, 7, 8], lopdf::StringFormat::Hexadecimal), ]), ); // Catalog should still be compressible assert!( ObjectStream::can_be_compressed(catalog_id, doc.objects.get(&catalog_id).unwrap(), &doc), "Catalog should be compressible with non-reference trailer entries" ); } #[test] fn test_same_object_referenced_multiple_times() { let mut doc = Document::with_version("1.5"); let shared_dict_id = doc.add_object(dictionary! { "Shared" => "Dictionary" }); // Reference the same object from multiple trailer keys doc.trailer.set("Custom1", shared_dict_id); doc.trailer.set("Custom2", shared_dict_id); doc.trailer.set("Custom3", shared_dict_id); // Should still be compressible (not encryption dict) assert!( ObjectStream::can_be_compressed(shared_dict_id, doc.objects.get(&shared_dict_id).unwrap(), &doc), "Object referenced multiple times in trailer should be compressible" ); } #[test] fn test_encrypt_key_with_non_reference_value() { let mut doc = Document::with_version("1.5"); let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog" }); // Set Encrypt to non-reference value (invalid but should not crash) doc.trailer.set("Root", catalog_id); doc.trailer.set("Encrypt", Object::Null); // Catalog should be compressible assert!( ObjectStream::can_be_compressed(catalog_id, doc.objects.get(&catalog_id).unwrap(), &doc), "Catalog should be compressible when Encrypt is not a reference" ); } #[test] fn test_missing_encrypt_key() { let mut doc = Document::with_version("1.5"); let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog" }); let some_dict_id = doc.add_object(dictionary! { "Filter" => "Standard" // Looks like encryption dict but not referenced as one }); doc.trailer.set("Root", catalog_id); // No Encrypt key in trailer // Both should be compressible assert!( ObjectStream::can_be_compressed(catalog_id, doc.objects.get(&catalog_id).unwrap(), &doc), "Catalog should be compressible without Encrypt in trailer" ); assert!( ObjectStream::can_be_compressed(some_dict_id, doc.objects.get(&some_dict_id).unwrap(), &doc), "Dictionary that looks like encryption should be compressible if not referenced as Encrypt" ); } #[test] fn test_linearized_with_trailer_references() { let mut doc = Document::with_version("1.5"); // Add linearization dictionary let _lin_id = doc.add_object(dictionary! { "Linearized" => 1 }); let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog" }); let info_id = doc.add_object(dictionary! { "Title" => "Linearized PDF" }); doc.trailer.set("Root", catalog_id); doc.trailer.set("Info", info_id); // In linearized PDF, catalog cannot be compressed but info can assert!( !ObjectStream::can_be_compressed(catalog_id, doc.objects.get(&catalog_id).unwrap(), &doc), "Catalog should NOT be compressible in linearized PDF" ); assert!( ObjectStream::can_be_compressed(info_id, doc.objects.get(&info_id).unwrap(), &doc), "Info should be compressible even in linearized PDF" ); } #[test] fn test_indirect_object_chain() { let mut doc = Document::with_version("1.5"); // Create a chain of objects let obj3_id = doc.add_object(dictionary! { "Level" => 3 }); let obj2_id = doc.add_object(dictionary! { "Level" => 2, "Next" => obj3_id }); let obj1_id = doc.add_object(dictionary! { "Level" => 1, "Next" => obj2_id }); // Only reference the first in trailer doc.trailer.set("Chain", obj1_id); // All should be compressible (trailer only directly references obj1) assert!( ObjectStream::can_be_compressed(obj1_id, doc.objects.get(&obj1_id).unwrap(), &doc), "Object directly referenced in trailer should be compressible" ); assert!( ObjectStream::can_be_compressed(obj2_id, doc.objects.get(&obj2_id).unwrap(), &doc), "Object indirectly referenced should be compressible" ); assert!( ObjectStream::can_be_compressed(obj3_id, doc.objects.get(&obj3_id).unwrap(), &doc), "Object indirectly referenced should be compressible" ); } #[test] fn test_real_world_trailer_structure() { let mut doc = Document::with_version("1.5"); // Simulate a real PDF trailer structure let pages_id = doc.new_object_id(); let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => pages_id, "Version" => "/1.5" }); let info_id = doc.add_object(dictionary! { "Title" => "Real World PDF", "Author" => "Test Suite", "Subject" => "Testing trailer compression", "Creator" => "lopdf", "Producer" => "lopdf test", "CreationDate" => "D:20250101120000Z", "ModDate" => "D:20250807120000Z" }); // Typical trailer entries doc.trailer.set("Root", catalog_id); doc.trailer.set("Info", info_id); doc.trailer.set("Size", Object::Integer(50)); doc.trailer.set( "ID", Object::Array(vec![ Object::String(b"<4E4F204944454153>".to_vec(), lopdf::StringFormat::Hexadecimal), Object::String(b"<4E4F204944454153>".to_vec(), lopdf::StringFormat::Hexadecimal), ]), ); // Both catalog and info should be compressible assert!( ObjectStream::can_be_compressed(catalog_id, doc.objects.get(&catalog_id).unwrap(), &doc), "Real-world catalog should be compressible" ); assert!( ObjectStream::can_be_compressed(info_id, doc.objects.get(&info_id).unwrap(), &doc), "Real-world info dictionary should be compressible" ); } #[test] fn test_compression_with_save_integration() { use lopdf::SaveOptions; let mut doc = Document::with_version("1.5"); let pages_id = doc.new_object_id(); let page_id = doc.add_object(dictionary! { "Type" => "Page", "Parent" => pages_id, "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()] }); doc.objects.insert( pages_id, Object::Dictionary(dictionary! { "Type" => "Pages", "Kids" => vec![page_id.into()], "Count" => 1 }), ); let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => pages_id }); let info_id = doc.add_object(dictionary! { "Title" => "Compression Test", "Keywords" => "test, compression, object streams" }); doc.trailer.set("Root", catalog_id); doc.trailer.set("Info", info_id); // Save with object streams let options = SaveOptions::builder().use_object_streams(true).build(); let mut output = Vec::new(); doc.save_with_options(&mut output, options).unwrap(); let content = String::from_utf8_lossy(&output); // Verify object streams exist assert!(content.contains("/ObjStm"), "Object streams should be created"); // Verify trailer-referenced objects are compressed assert!( !content.contains(&format!("{} 0 obj\n<