use hipdf::lopdf::{content::Content, dictionary, Document, Object, Stream};
use hipdf::ocg::{Layer, LayerContentBuilder, LayerOperations as Ops, OCGConfig, OCGManager};
use std::fs;
use std::path::Path;
const TEST_OUTPUT_DIR: &str = "tests/outputs";
fn ensure_output_dir() {
if !Path::new(TEST_OUTPUT_DIR).exists() {
fs::create_dir_all(TEST_OUTPUT_DIR).expect("Failed to create test output directory");
}
}
fn cleanup_test_files() {
if Path::new(TEST_OUTPUT_DIR).exists() {
if let Ok(entries) = fs::read_dir(TEST_OUTPUT_DIR) {
for entry in entries.flatten() {
if let Ok(file_type) = entry.file_type() {
if file_type.is_file() {
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
if !file_name_str.ends_with("_test.pdf") {
let _ = fs::remove_file(entry.path());
}
}
}
}
}
}
}
#[test]
fn test_ocg_manager_creation() {
let config = OCGConfig::default();
let manager = OCGManager::with_config(config);
assert!(!manager.has_oc_properties());
assert!(manager.is_empty());
}
#[test]
fn test_layer_creation() {
let layer = Layer::new("Test Layer", false);
assert_eq!(layer.name, "Test Layer");
assert!(!layer.default_visible);
assert!(layer.tag.is_none());
}
#[test]
fn test_layer_content_builder() {
let mut builder = LayerContentBuilder::new();
builder
.begin_layer("L0")
.add_operation(Ops::rectangle(0.0, 0.0, 100.0, 100.0))
.end_layer();
let operations = builder.build();
assert!(!operations.is_empty());
assert_eq!(operations.len(), 3); }
#[test]
fn test_layer_operations() {
let rect_op = Ops::rectangle(10.0, 20.0, 100.0, 200.0);
let fill_op = Ops::fill();
let stroke_op = Ops::stroke();
assert_eq!(
format!("{:?}", rect_op),
format!("{:?}", Ops::rectangle(10.0, 20.0, 100.0, 200.0))
);
assert_eq!(format!("{:?}", fill_op), format!("{:?}", Ops::fill()));
assert_eq!(format!("{:?}", stroke_op), format!("{:?}", Ops::stroke()));
let color_op = Ops::set_fill_color_rgb(1.0, 0.5, 0.0);
assert_eq!(
format!("{:?}", color_op),
format!("{:?}", Ops::set_fill_color_rgb(1.0, 0.5, 0.0))
);
let text_op = Ops::show_text("Test Text");
assert_eq!(
format!("{:?}", text_op),
format!("{:?}", Ops::show_text("Test Text"))
);
assert!(true);
}
#[test]
fn test_ocg_configuration() {
let configs = vec![
OCGConfig {
base_state: "OFF".to_string(),
create_panel_ui: false,
intent: vec!["View".to_string()],
},
OCGConfig {
base_state: "ON".to_string(),
create_panel_ui: true,
intent: vec!["View".to_string(), "Design".to_string()],
},
];
for config in configs {
let manager = OCGManager::with_config(config.clone());
assert_eq!(manager.config.base_state, config.base_state);
assert_eq!(manager.config.create_panel_ui, config.create_panel_ui);
assert_eq!(manager.config.intent, config.intent);
}
}
#[test]
fn test_ocg_performance() {
use std::time::Instant;
let mut manager = OCGManager::new();
let start = Instant::now();
for i in 0..50 {
manager.add_layer(Layer::new(format!("Layer {}", i), true));
}
let duration = start.elapsed();
let avg_time = duration.as_nanos() as f64 / 50_000_000.0;
println!("📊 Layer creation: {:.2}ms per layer", avg_time);
assert!(avg_time < 1.0, "Layer creation should be fast");
}
#[test]
fn test_layer_retrieval() {
let mut manager = OCGManager::new();
let layer1 = Layer::new("Test Layer 1", true);
let layer2 = Layer::new("Test Layer 2", false);
let idx1 = manager.add_layer(layer1);
let idx2 = manager.add_layer(layer2);
assert_eq!(idx1, 0);
assert_eq!(idx2, 1);
assert_eq!(manager.len(), 2);
let retrieved = manager.get_layer("Test Layer 1");
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().name, "Test Layer 1");
assert!(retrieved.unwrap().default_visible);
let retrieved2 = manager.get_layer("Nonexistent");
assert!(retrieved2.is_none());
let mut_layer = manager.get_layer_mut("Test Layer 2");
assert!(mut_layer.is_some());
mut_layer.unwrap().default_visible = true;
assert!(manager.get_layer("Test Layer 2").unwrap().default_visible);
}
#[test]
fn test_complex_content_building() {
let mut builder = LayerContentBuilder::new();
builder
.begin_layer("L0")
.add_operation(Ops::begin_text())
.add_operation(Ops::set_font("F1", 12.0))
.add_operation(Ops::text_position(10.0, 20.0))
.add_operation(Ops::show_text("Layer 0 content"))
.add_operation(Ops::end_text())
.add_operation(Ops::rectangle(10.0, 10.0, 100.0, 50.0))
.add_operation(Ops::fill())
.end_layer()
.begin_layer("L1")
.add_operation(Ops::set_fill_color_rgb(1.0, 0.0, 0.0))
.add_operation(Ops::rectangle(20.0, 20.0, 80.0, 40.0))
.add_operation(Ops::stroke())
.end_layer();
let operations = builder.build();
assert!(!operations.is_empty());
assert_eq!(operations.len(), 14);
}
#[test]
fn test_ocg_config_variations() {
let configs = vec![
OCGConfig {
base_state: "OFF".to_string(),
create_panel_ui: false,
intent: vec!["View".to_string()],
},
OCGConfig {
base_state: "ON".to_string(),
create_panel_ui: true,
intent: vec![
"View".to_string(),
"Design".to_string(),
"Print".to_string(),
],
},
OCGConfig {
base_state: "Unchanged".to_string(),
create_panel_ui: true,
intent: vec![],
},
];
for config in configs {
let manager = OCGManager::with_config(config.clone());
assert_eq!(manager.config.base_state, config.base_state);
assert_eq!(manager.config.create_panel_ui, config.create_panel_ui);
assert_eq!(manager.config.intent, config.intent);
}
}
#[test]
fn test_layer_tags_and_resources() {
let mut doc = Document::with_version("1.5");
let mut manager = OCGManager::new();
manager.add_layer(Layer::new("Layer A", true));
manager.add_layer(Layer::new("Layer B", false));
manager.initialize(&mut doc);
let mut resources = dictionary! {
"Font" => dictionary! {
"F1" => doc.add_object(dictionary! {
"Type" => "Font",
"Subtype" => "Type1",
"BaseFont" => "Helvetica",
}),
},
};
let layer_tags = manager.setup_page_resources(&mut resources);
assert_eq!(layer_tags.len(), 2);
assert_eq!(
layer_tags.get(&"Layer A".to_string()),
Some(&"L0".to_string())
);
assert_eq!(
layer_tags.get(&"Layer B".to_string()),
Some(&"L1".to_string())
);
assert!(resources.has(b"Properties"));
}
#[test]
fn test_ocg_integration() {
ensure_output_dir();
let mut doc = Document::with_version("1.5");
let pages_id = doc.add_object(dictionary! {
"Type" => "Pages",
"Count" => 1,
});
let helvetica = doc.add_object(dictionary! {
"Type" => "Font",
"Subtype" => "Type1",
"BaseFont" => "Helvetica",
});
let helvetica_bold = doc.add_object(dictionary! {
"Type" => "Font",
"Subtype" => "Type1",
"BaseFont" => "Helvetica-Bold",
});
let config = OCGConfig {
base_state: "ON".to_string(),
create_panel_ui: true,
intent: vec!["View".to_string(), "Design".to_string()],
};
let mut ocg_manager = OCGManager::with_config(config);
ocg_manager.add_layer(Layer::new("Background", true));
ocg_manager.add_layer(Layer::new("Main Content", true));
ocg_manager.add_layer(Layer::new("Annotations", false));
ocg_manager.add_layer(Layer::new("Watermark", false));
ocg_manager.add_layer(Layer::new("Debug Info", false));
ocg_manager.initialize(&mut doc);
let mut resources = dictionary! {
"Font" => dictionary! {
"F1" => helvetica,
"F2" => helvetica_bold,
},
};
let layer_tags = ocg_manager.setup_page_resources(&mut resources);
let mut builder = LayerContentBuilder::new();
if let Some(bg_tag) = layer_tags.get(&"Background".to_string()) {
builder
.begin_layer(bg_tag)
.add_operation(Ops::set_fill_color_rgb(0.9, 0.95, 1.0))
.add_operation(Ops::rectangle(0.0, 0.0, 595.0, 842.0))
.add_operation(Ops::fill())
.end_layer();
}
if let Some(content_tag) = layer_tags.get(&"Main Content".to_string()) {
builder
.begin_layer(content_tag)
.add_operation(Ops::begin_text())
.add_operation(Ops::set_fill_color_gray(0.0))
.add_operation(Ops::set_font("F2", 24.0))
.add_operation(Ops::text_position(50.0, 750.0))
.add_operation(Ops::show_text("PDF with Optional Content Groups"))
.add_operation(Ops::end_text())
.add_operation(Ops::begin_text())
.add_operation(Ops::set_font("F1", 12.0))
.add_operation(Ops::text_position(50.0, 700.0))
.add_operation(Ops::show_text(
"This document contains multiple layers that can be toggled on/off.",
))
.add_operation(Ops::end_text())
.add_operation(Ops::set_fill_color_rgb(0.2, 0.6, 0.2))
.add_operation(Ops::rectangle(50.0, 550.0, 200.0, 100.0))
.add_operation(Ops::fill())
.end_layer();
}
if let Some(anno_tag) = layer_tags.get(&"Annotations".to_string()) {
builder
.begin_layer(anno_tag)
.add_operation(Ops::set_stroke_color_rgb(1.0, 0.0, 0.0))
.add_operation(Ops::rectangle(260.0, 560.0, 80.0, 80.0))
.add_operation(Ops::stroke())
.add_operation(Ops::begin_text())
.add_operation(Ops::set_fill_color_rgb(1.0, 0.0, 0.0))
.add_operation(Ops::set_font("F1", 10.0))
.add_operation(Ops::text_position(350.0, 590.0))
.add_operation(Ops::show_text("Important!"))
.add_operation(Ops::end_text())
.end_layer();
}
if let Some(watermark_tag) = layer_tags.get(&"Watermark".to_string()) {
builder
.begin_layer(watermark_tag)
.add_operation(Ops::begin_text())
.add_operation(Ops::set_fill_color_gray(0.8))
.add_operation(Ops::set_font("F2", 48.0))
.add_operation(Ops::text_position(150.0, 400.0))
.add_operation(Ops::show_text("DRAFT"))
.add_operation(Ops::end_text())
.end_layer();
}
if let Some(debug_tag) = layer_tags.get(&"Debug Info".to_string()) {
builder
.begin_layer(debug_tag)
.add_operation(Ops::begin_text())
.add_operation(Ops::set_fill_color_rgb(0.5, 0.5, 0.5))
.add_operation(Ops::set_font("F1", 8.0))
.add_operation(Ops::text_position(50.0, 50.0))
.add_operation(Ops::show_text("Debug: Document created with OCG support"))
.add_operation(Ops::end_text())
.add_operation(Ops::begin_text())
.add_operation(Ops::text_position(50.0, 40.0))
.add_operation(Ops::show_text(&format!("Layers: {}", layer_tags.len())))
.add_operation(Ops::end_text())
.end_layer();
}
builder
.add_operation(Ops::begin_text())
.add_operation(Ops::set_fill_color_gray(0.0))
.add_operation(Ops::set_font("F1", 10.0))
.add_operation(Ops::text_position(50.0, 100.0))
.add_operation(Ops::show_text(
"This text is always visible (not in any layer).",
))
.add_operation(Ops::end_text());
let operations = builder.build();
let content = Content { operations };
let content_stream = Stream::new(dictionary! {}, content.encode().unwrap());
let content_id = doc.add_object(content_stream);
let page_id = doc.add_object(dictionary! {
"Type" => "Page",
"Parent" => pages_id,
"MediaBox" => vec![0.into(), 0.into(), 595.into(), 842.into()],
"Contents" => content_id,
"Resources" => resources,
});
let pages_dict = doc
.get_object_mut(pages_id)
.and_then(Object::as_dict_mut)
.unwrap();
pages_dict.set("Kids", vec![Object::Reference(page_id)]);
let catalog_id = doc.add_object(dictionary! {
"Type" => "Catalog",
"Pages" => Object::Reference(pages_id),
});
doc.trailer.set("Root", Object::Reference(catalog_id));
ocg_manager.update_catalog(&mut doc);
let output_path = format!("{}/ocg_integration_test.pdf", TEST_OUTPUT_DIR);
doc.save(&output_path).expect("Failed to save PDF");
assert!(Path::new(&output_path).exists());
println!("✅ Advanced layered PDF test completed successfully");
println!("📄 PDF created: {}", output_path);
println!("\nLayers created:");
println!(" - Background (visible by default)");
println!(" - Main Content (visible by default)");
println!(" - Annotations (hidden by default)");
println!(" - Watermark (hidden by default)");
println!(" - Debug Info (hidden by default)");
println!(
"\n💡 Open the PDF in a viewer that supports layers (like Adobe Acrobat) to toggle them!"
);
}
#[test]
fn cleanup() {
cleanup_test_files();
}