lopdf 0.40.0

A Rust library for PDF document manipulation.
Documentation
use lopdf::{dictionary, Document, Object, ObjectStream};

#[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<</Type/Catalog", catalog_id.0)),
            "Catalog should be in object stream");
    assert!(!content.contains(&format!("{} 0 obj\n<</Title", info_id.0)),
            "Info should be in object stream");
    
    // Load and verify the PDF is valid
    let loaded = Document::load_mem(&output).unwrap();
    assert!(loaded.trailer.get(b"Root").is_ok(), "Loaded PDF should have valid Root");
    assert!(loaded.trailer.get(b"Info").is_ok(), "Loaded PDF should have valid Info");
}