use std::collections::HashMap;
use std::io::Result;
use std::path::Path;
use std::sync::Arc;
use hipdf::hatching::{
CustomPattern, HatchConfig, HatchStyle, HatchingManager, PatternedShapeBuilder,
ProceduralPattern, Transform,
};
use hipdf::lopdf::{content::Content, dictionary, Dictionary, Document, Object, Stream};
const HATCHING_TEST_OUTPUT_DIR: &str = "tests/outputs/hatching_integration_test";
fn ensure_hatching_output_dir() {
if !Path::new(HATCHING_TEST_OUTPUT_DIR).exists() {
std::fs::create_dir_all(HATCHING_TEST_OUTPUT_DIR)
.expect("Failed to create hatching test output directory");
}
}
#[test]
fn test_hatching_patterns_showcase() -> Result<()> {
ensure_hatching_output_dir();
let mut doc = Document::with_version("1.5");
let pages_id = doc.add_object(dictionary! {
"Type" => "Pages",
"Count" => 1,
});
let font_id = doc.add_object(dictionary! {
"Type" => "Font",
"Subtype" => "Type1",
"BaseFont" => "Helvetica",
});
let mut hatching_manager = HatchingManager::new();
let mut resources = dictionary! {
"Font" => dictionary! { "F1" => font_id },
"Pattern" => Dictionary::new(),
};
let patterns: Vec<(String, HatchConfig)> = vec![
(
"diagonal_right".to_string(),
HatchConfig::new(HatchStyle::DiagonalRight)
.with_spacing(8.0)
.with_line_width(0.5)
.with_color(0.0, 0.0, 0.0),
),
(
"diagonal_left_blue".to_string(),
HatchConfig::new(HatchStyle::DiagonalLeft)
.with_spacing(10.0)
.with_line_width(1.0)
.with_color(0.0, 0.0, 1.0),
),
(
"horizontal_bg".to_string(),
HatchConfig::new(HatchStyle::Horizontal)
.with_spacing(6.0)
.with_line_width(0.3)
.with_color(0.5, 0.0, 0.0)
.with_background(1.0, 0.95, 0.9),
),
(
"vertical_thick".to_string(),
HatchConfig::new(HatchStyle::Vertical)
.with_spacing(12.0)
.with_line_width(2.0)
.with_color(0.2, 0.2, 0.2),
),
(
"cross_grid".to_string(),
HatchConfig::new(HatchStyle::Cross)
.with_spacing(8.0)
.with_line_width(0.5)
.with_color(0.3, 0.3, 0.3),
),
(
"diagonal_cross".to_string(),
HatchConfig::new(HatchStyle::DiagonalCross)
.with_spacing(10.0)
.with_line_width(0.7)
.with_color(0.0, 0.5, 0.0),
),
(
"dots".to_string(),
HatchConfig::new(HatchStyle::Dots)
.with_spacing(5.0)
.with_color(0.0, 0.0, 0.0),
),
(
"checkerboard".to_string(),
HatchConfig::new(HatchStyle::Checkerboard)
.with_spacing(10.0)
.with_color(0.0, 0.0, 0.0)
.with_background(1.0, 1.0, 1.0),
),
(
"brick".to_string(),
HatchConfig::new(HatchStyle::Brick)
.with_spacing(15.0)
.with_line_width(1.0)
.with_color(0.6, 0.3, 0.1),
),
(
"hexagonal".to_string(),
HatchConfig::new(HatchStyle::Hexagonal)
.with_spacing(20.0)
.with_line_width(0.8)
.with_color(0.0, 0.3, 0.6),
),
(
"wave".to_string(),
HatchConfig::new(HatchStyle::Wave)
.with_spacing(15.0)
.with_line_width(1.5)
.with_color(0.0, 0.5, 0.8),
),
(
"zigzag".to_string(),
HatchConfig::new(HatchStyle::Zigzag)
.with_spacing(12.0)
.with_line_width(1.0)
.with_color(0.8, 0.0, 0.8),
),
(
"circles".to_string(),
HatchConfig::new(HatchStyle::Circles)
.with_spacing(15.0)
.with_line_width(0.5)
.with_color(0.5, 0.0, 0.5),
),
(
"triangles".to_string(),
HatchConfig::new(HatchStyle::Triangles)
.with_spacing(18.0)
.with_line_width(0.7)
.with_color(0.0, 0.6, 0.3),
),
(
"diamond".to_string(),
HatchConfig::new(HatchStyle::Diamond)
.with_spacing(12.0)
.with_line_width(0.6)
.with_color(0.7, 0.0, 0.0),
),
(
"scales".to_string(),
HatchConfig::new(HatchStyle::Scales)
.with_spacing(10.0)
.with_line_width(0.4)
.with_color(0.0, 0.4, 0.4),
),
(
"spiral".to_string(),
HatchConfig::new(HatchStyle::Spiral)
.with_spacing(25.0)
.with_line_width(0.8)
.with_color(0.3, 0.3, 0.0),
),
(
"dotted_grid".to_string(),
HatchConfig::new(HatchStyle::DottedGrid)
.with_spacing(8.0)
.with_line_width(0.3)
.with_color(0.4, 0.4, 0.4),
),
(
"concentric".to_string(),
HatchConfig::new(HatchStyle::ConcentricCircles)
.with_spacing(20.0)
.with_line_width(0.5)
.with_color(0.0, 0.2, 0.6),
),
(
"wood_grain".to_string(),
HatchConfig::new(HatchStyle::WoodGrain)
.with_spacing(30.0)
.with_line_width(0.7)
.with_color(0.4, 0.2, 0.0),
),
];
let mut pattern_map = HashMap::new();
for (name, config) in &patterns {
let (pattern_id, pattern_name) = hatching_manager.create_pattern(&mut doc, config);
hatching_manager.add_pattern_to_resources(&mut resources, &pattern_name, pattern_id);
pattern_map.insert(name.clone(), pattern_name);
}
let mut shape_builder = PatternedShapeBuilder::new();
let mut text_ops = Vec::new();
text_ops.push(lopdf::content::Operation::new("0 g", vec![])); text_ops.push(lopdf::content::Operation::new("BT", vec![]));
text_ops.push(lopdf::content::Operation::new(
"Tf",
vec![Object::Name(b"F1".to_vec()), 14.into()],
));
text_ops.push(lopdf::content::Operation::new(
"Td",
vec![200.into(), 800.into()],
));
text_ops.push(lopdf::content::Operation::new(
"Tj",
vec![Object::string_literal("20 Different Hatching Patterns")],
));
text_ops.push(lopdf::content::Operation::new("ET", vec![]));
let start_x = 50.0;
let start_y = 700.0;
let cell_width = 100.0;
let cell_height = 100.0;
let spacing = 10.0;
for (index, (name, _config)) in patterns.iter().enumerate() {
let row = index / 5;
let col = index % 5;
let x = start_x + col as f32 * (cell_width + spacing);
let y = start_y - row as f32 * (cell_height + spacing + 20.0);
if let Some(pattern_name) = pattern_map.get(name) {
shape_builder.rectangle(x, y, cell_width, cell_height, pattern_name);
}
text_ops.push(lopdf::content::Operation::new("0 g", vec![])); text_ops.push(lopdf::content::Operation::new("BT", vec![]));
text_ops.push(lopdf::content::Operation::new(
"Tf",
vec![Object::Name(b"F1".to_vec()), 8.into()],
));
text_ops.push(lopdf::content::Operation::new(
"Td",
vec![x.into(), (y - 15.0).into()],
));
let label = name.replace('_', " ");
let label = label
.chars()
.take(15) .collect::<String>();
text_ops.push(lopdf::content::Operation::new(
"Tj",
vec![Object::string_literal(label.as_bytes().to_vec())],
));
text_ops.push(lopdf::content::Operation::new("ET", vec![]));
}
let demo_y = 150.0;
if let Some(pattern_name) = pattern_map.get("spiral") {
shape_builder.circle(100.0, demo_y, 30.0, pattern_name);
}
if let Some(pattern_name) = pattern_map.get("hexagonal") {
shape_builder.triangle(
200.0,
demo_y - 30.0,
250.0,
demo_y + 30.0,
150.0,
demo_y + 30.0,
pattern_name,
);
}
if let Some(pattern_name) = pattern_map.get("wood_grain") {
shape_builder.rectangle(300.0, demo_y - 30.0, 150.0, 60.0, pattern_name);
}
text_ops.push(lopdf::content::Operation::new("0 g", vec![])); text_ops.push(lopdf::content::Operation::new("BT", vec![]));
text_ops.push(lopdf::content::Operation::new(
"Tf",
vec![Object::Name(b"F1".to_vec()), 10.into()],
));
text_ops.push(lopdf::content::Operation::new(
"Td",
vec![50.into(), 100.into()],
));
text_ops.push(lopdf::content::Operation::new(
"Tj",
vec![Object::string_literal(
"Demo: Different shapes with various patterns",
)],
));
text_ops.push(lopdf::content::Operation::new("ET", vec![]));
let mut all_operations = shape_builder.build();
all_operations.extend(text_ops);
let content = Content {
operations: all_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));
let output_path = std::path::Path::new(
"tests/outputs/hatching_integration_test/hatching_patterns_integration_test.pdf",
);
let absolute_path = std::env::current_dir()?.join(output_path);
doc.save(&absolute_path)?;
println!("Successfully created PDF with 20 different hatching patterns!");
println!("Output: {}", absolute_path.display());
println!("\nPatterns included:");
for (i, (name, _)) in patterns.iter().enumerate() {
println!(" {}. {}", i + 1, name.replace('_', " "));
}
assert!(absolute_path.exists(), "Output PDF file should exist");
Ok(())
}
#[test]
fn test_custom_patterns_showcase() -> Result<()> {
ensure_hatching_output_dir();
let mut doc = Document::with_version("1.5");
let pages_id = doc.add_object(dictionary! {
"Type" => "Pages",
"Count" => 1,
});
let font_id = doc.add_object(dictionary! {
"Type" => "Font",
"Subtype" => "Type1",
"BaseFont" => "Helvetica",
});
let mut hatching_manager = HatchingManager::new();
let mut resources = dictionary! {
"Font" => dictionary! { "F1" => font_id },
"Pattern" => Dictionary::new(),
};
let mut pattern_map = HashMap::new();
let (pattern_id, pattern_name) =
hatching_manager.create_custom_pattern(&mut doc, 20.0, 20.0, |builder| {
builder
.set_fill_color(0.8, 0.2, 0.2)
.circle(5.0, 5.0, 2.0)
.fill()
.circle(15.0, 8.0, 1.5)
.fill()
.circle(10.0, 15.0, 2.5)
.fill()
});
hatching_manager.add_pattern_to_resources(&mut resources, &pattern_name, pattern_id);
pattern_map.insert("custom_dots", pattern_name);
let (pattern_id, pattern_name) =
hatching_manager.create_custom_pattern(&mut doc, 30.0, 30.0, |builder| {
let cx = 15.0;
let cy = 15.0;
let outer = 12.0;
let inner = 5.0;
let points = 5;
let mut star_points = Vec::new();
for i in 0..points * 2 {
let angle = i as f32 * std::f32::consts::PI / points as f32;
let r = if i % 2 == 0 { outer } else { inner };
star_points.push((cx + r * angle.cos(), cy + r * angle.sin()));
}
builder
.set_fill_color(1.0, 0.8, 0.0)
.polygon(&star_points)
.fill()
});
hatching_manager.add_pattern_to_resources(&mut resources, &pattern_name, pattern_id);
pattern_map.insert("stars", pattern_name);
let (pattern_id, pattern_name) =
hatching_manager.create_custom_pattern(&mut doc, 10.0, 10.0, |builder| {
builder.set_line_width(0.5);
for i in 0..10 {
let intensity = i as f32 / 10.0;
builder
.set_stroke_color(intensity, 0.0, 1.0 - intensity)
.move_to(i as f32, 0.0)
.line_to(i as f32, 10.0)
.stroke();
}
builder
});
hatching_manager.add_pattern_to_resources(&mut resources, &pattern_name, pattern_id);
pattern_map.insert("gradient_lines", pattern_name);
let (pattern_id, pattern_name) =
hatching_manager.create_custom_pattern(&mut doc, 40.0, 40.0, |builder| {
builder
.set_stroke_color(0.0, 0.4, 0.6)
.set_line_width(1.0)
.rectangle(5.0, 5.0, 30.0, 30.0)
.stroke()
.push_transform(Transform {
translate: (20.0, 20.0),
rotate: 45.0,
scale: (0.7, 0.7),
})
.rectangle(-10.0, -10.0, 20.0, 20.0)
.stroke()
.pop_transform()
.circle(20.0, 20.0, 5.0)
.stroke()
});
hatching_manager.add_pattern_to_resources(&mut resources, &pattern_name, pattern_id);
pattern_map.insert("geometric", pattern_name);
let sierpinski = HatchConfig::new(HatchStyle::Custom(CustomPattern::Procedural(
ProceduralPattern {
sampler: Arc::new(|x, y, _t| {
let xi = x as i32;
let yi = y as i32;
(xi & yi) == 0
}),
resolution: 16,
fill: true,
},
)));
let (pattern_id, pattern_name) = hatching_manager.create_pattern(&mut doc, &sierpinski);
hatching_manager.add_pattern_to_resources(&mut resources, &pattern_name, pattern_id);
pattern_map.insert("sierpinski", pattern_name);
let mut shape_builder = PatternedShapeBuilder::new();
let mut text_ops = Vec::new();
text_ops.push(lopdf::content::Operation::new("0 g", vec![])); text_ops.push(lopdf::content::Operation::new("BT", vec![]));
text_ops.push(lopdf::content::Operation::new(
"Tf",
vec![Object::Name(b"F1".to_vec()), 16.into()],
));
text_ops.push(lopdf::content::Operation::new(
"Td",
vec![150.into(), 800.into()],
));
text_ops.push(lopdf::content::Operation::new(
"Tj",
vec![Object::string_literal("Custom Pattern Examples")],
));
text_ops.push(lopdf::content::Operation::new("ET", vec![]));
let patterns = vec![
("custom_dots", "Random Dots"),
("stars", "Star Pattern"),
("gradient_lines", "Gradient Lines"),
("geometric", "Geometric"),
("sierpinski", "Sierpinski"),
];
let start_x = 50.0;
let start_y = 700.0;
let cell_size = 80.0;
let spacing = 20.0;
for (index, (key, label)) in patterns.iter().enumerate() {
let x = start_x + (index % 4) as f32 * (cell_size + spacing);
let y = start_y - (index / 4) as f32 * (cell_size + spacing + 20.0);
if let Some(pattern_name) = pattern_map.get(*key) {
shape_builder.rectangle(x, y, cell_size, cell_size, pattern_name);
}
text_ops.push(lopdf::content::Operation::new("0 g", vec![])); text_ops.push(lopdf::content::Operation::new("BT", vec![]));
text_ops.push(lopdf::content::Operation::new(
"Tf",
vec![Object::Name(b"F1".to_vec()), 10.into()],
));
text_ops.push(lopdf::content::Operation::new(
"Td",
vec![x.into(), (y - 15.0).into()],
));
text_ops.push(lopdf::content::Operation::new(
"Tj",
vec![Object::string_literal(label.as_bytes().to_vec())],
));
text_ops.push(lopdf::content::Operation::new("ET", vec![]));
}
let mut all_operations = shape_builder.build();
all_operations.extend(text_ops);
let content = Content {
operations: all_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));
let output_path = std::path::Path::new(
"tests/outputs/hatching_integration_test/hatching_custom_patterns_integration_test.pdf",
);
let absolute_path = std::env::current_dir()?.join(output_path);
doc.save(&absolute_path)?;
println!("Successfully created PDF with custom patterns!");
println!("Output: {}", absolute_path.display());
println!("\nCustom patterns included:");
for (key, label) in &patterns {
println!(" - {}: {}", key, label);
}
assert!(absolute_path.exists(), "Output PDF file should exist");
Ok(())
}