use lopdf::{dictionary, Document, Object, ObjectStream, Stream, SaveOptions};
#[test]
fn test_object_stream_parse_empty() {
let mut stream = Stream::new(
dictionary! {
"Type" => "ObjStm",
"N" => 0,
"First" => 0
},
vec![]
);
let obj_stream = ObjectStream::new(&mut stream).unwrap();
assert_eq!(obj_stream.objects.len(), 0);
}
#[test]
fn test_object_stream_config_default() {
let config = lopdf::ObjectStreamConfig::default();
assert_eq!(config.max_objects_per_stream, 100);
assert_eq!(config.compression_level, 6);
}
#[test]
fn test_object_stream_to_stream_object() {
let mut obj_stream = ObjectStream::builder().build();
obj_stream.add_object((1, 0), Object::Integer(42)).unwrap();
obj_stream.add_object((2, 0), Object::Boolean(true)).unwrap();
obj_stream.add_object((3, 0), Object::Name(b"Test".to_vec())).unwrap();
let stream_obj = obj_stream.to_stream_object().unwrap();
assert_eq!(stream_obj.dict.get(b"Type").unwrap(), &Object::Name(b"ObjStm".to_vec()));
assert_eq!(stream_obj.dict.get(b"N").unwrap(), &Object::Integer(3));
assert!(stream_obj.dict.has(b"First"));
assert!(stream_obj.dict.has(b"Length"));
assert!(!stream_obj.content.is_empty());
}
#[test]
fn test_save_options_with_object_streams() {
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,
});
doc.trailer.set("Root", catalog_id);
let save_options = SaveOptions {
use_object_streams: true,
use_xref_streams: false,
linearize: false,
object_stream_config: Default::default(),
};
let mut buffer = Vec::new();
doc.save_with_options(&mut buffer, save_options).unwrap();
let content = String::from_utf8_lossy(&buffer);
assert!(content.contains("/ObjStm"), "Object streams should be created");
assert!(content.starts_with("%PDF-1.5") || content.starts_with("%PDF-1.6") || content.starts_with("%PDF-1.7"),
"PDF version should be 1.5 or higher for object streams");
}
#[test]
fn test_save_options_without_object_streams() {
let mut doc = Document::with_version("1.4");
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,
});
doc.trailer.set("Root", catalog_id);
let save_options = SaveOptions {
use_object_streams: false,
use_xref_streams: false,
linearize: false,
object_stream_config: Default::default(),
};
let mut buffer = Vec::new();
doc.save_with_options(&mut buffer, save_options).unwrap();
let content = String::from_utf8_lossy(&buffer);
assert!(!content.contains("/ObjStm"), "Object streams should not be created");
assert!(content.contains(&format!("{} 0 obj", catalog_id.0)));
assert!(content.contains(&format!("{} 0 obj", pages_id.0)));
assert!(content.contains(&format!("{} 0 obj", page_id.0)));
}
#[test]
fn test_encrypted_document_object_streams() {
let mut doc = Document::with_version("1.5");
let pages_id = doc.new_object_id();
let encrypt_id = doc.add_object(dictionary! {
"Filter" => "Standard",
"V" => 2,
"R" => 3,
"Length" => 128,
"P" => -1,
"O" => Object::String(vec![0; 32], lopdf::StringFormat::Hexadecimal),
"U" => Object::String(vec![0; 32], lopdf::StringFormat::Hexadecimal),
});
doc.trailer.set("Encrypt", encrypt_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,
});
doc.trailer.set("Root", catalog_id);
let encrypt_obj = doc.objects.get(&encrypt_id).unwrap();
assert!(!ObjectStream::can_be_compressed(encrypt_id, encrypt_obj, &doc),
"Encryption dictionary must not be compressible");
let mut buffer = Vec::new();
let result = doc.save_modern(&mut buffer);
assert!(result.is_ok());
let content = String::from_utf8_lossy(&buffer);
assert!(content.contains(&format!("{} 0 obj", encrypt_id.0)),
"Encryption dictionary must remain as individual object");
}
#[test]
fn test_linearized_pdf_catalog_handling() {
let mut doc = Document::with_version("1.5");
let _lin_id = doc.add_object(dictionary! {
"Linearized" => 1,
"L" => 50000, "H" => vec![Object::Integer(1000), Object::Integer(2000)], "O" => 10, "E" => 5000, "N" => 1, "T" => 45000 });
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,
});
doc.trailer.set("Root", catalog_id);
let catalog_obj = doc.objects.get(&catalog_id).unwrap();
assert!(!ObjectStream::can_be_compressed(catalog_id, catalog_obj, &doc),
"Catalog must not be compressible in linearized PDFs");
let pages_obj = doc.objects.get(&pages_id).unwrap();
let page_obj = doc.objects.get(&page_id).unwrap();
assert!(ObjectStream::can_be_compressed(pages_id, pages_obj, &doc),
"Pages should still be compressible in linearized PDFs");
assert!(ObjectStream::can_be_compressed(page_id, page_obj, &doc),
"Page should still be compressible in linearized PDFs");
}
#[test]
fn test_object_stream_with_references() {
let mut obj_stream = ObjectStream::builder().build();
obj_stream.add_object((1, 0), Object::Dictionary(dictionary! {
"Type" => "Font",
"BaseFont" => "Helvetica",
"Encoding" => Object::Reference((5, 0)) })).unwrap();
obj_stream.add_object((2, 0), Object::Array(vec![
Object::Integer(100),
Object::Reference((10, 0)), Object::String(b"Test".to_vec(), lopdf::StringFormat::Literal)
])).unwrap();
let stream_obj = obj_stream.to_stream_object().unwrap();
assert_eq!(stream_obj.dict.get(b"N").unwrap(), &Object::Integer(2));
let content = String::from_utf8_lossy(&stream_obj.content);
assert!(content.contains("5 0 R")); assert!(content.contains("10 0 R"));
}
#[test]
fn test_object_stream_with_nested_dictionaries() {
let mut obj_stream = ObjectStream::builder().build();
obj_stream.add_object((1, 0), Object::Dictionary(dictionary! {
"Type" => "ExtGState",
"CA" => 0.5,
"ca" => 0.5,
"BM" => Object::Array(vec![
Object::Name(b"Normal".to_vec()),
Object::Name(b"Multiply".to_vec())
]),
"SMask" => dictionary! {
"Type" => "Mask",
"S" => "Alpha",
"G" => Object::Reference((10, 0))
}
})).unwrap();
let stream_obj = obj_stream.to_stream_object().unwrap();
assert!(!stream_obj.content.is_empty());
}
#[test]
fn test_multiple_object_streams_in_document() {
let mut doc = Document::with_version("1.5");
let pages_id = doc.new_object_id();
let mut page_ids = vec![];
for i in 0..250 { let page_id = doc.add_object(dictionary! {
"Type" => "Page",
"Parent" => pages_id,
"MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
"Contents" => Object::Reference((1000 + i, 0))
});
page_ids.push(page_id);
}
doc.objects.insert(pages_id, Object::Dictionary(dictionary! {
"Type" => "Pages",
"Kids" => page_ids.into_iter().map(Object::Reference).collect::<Vec<_>>(),
"Count" => 250,
}));
let catalog_id = doc.add_object(dictionary! {
"Type" => "Catalog",
"Pages" => pages_id,
});
doc.trailer.set("Root", catalog_id);
let mut buffer = Vec::new();
doc.save_modern(&mut buffer).unwrap();
let content = String::from_utf8_lossy(&buffer);
let objstm_count = content.matches("/ObjStm").count();
assert!(objstm_count >= 3, "Should have multiple object streams for 250+ objects");
}