use printpdf::{
BlendMode, BuiltinFont, BuiltinOrExternalFontId, Color, ExtendedGraphicsState, Layer,
LayerIntent, LayerSubtype, LineCapStyle, LineDashPattern, LineJoinStyle, Mm, Op, OverprintMode,
PageAnnotId, PdfDocument, PdfPage, PdfParseOptions, PdfSaveOptions, Polygon, PolygonRing, Pt,
RenderingIntent, Rgb, SeperableBlendMode, TextItem,
};
#[test]
fn test_layer_parsing() {
let mut doc = PdfDocument::new("Layer Parsing Test");
let layer = Layer {
name: "Test Layer".to_string(),
creator: "Test Creator".to_string(),
intent: LayerIntent::Design,
usage: LayerSubtype::Artwork,
};
let layer_id = doc.add_layer(&layer);
let ops = vec![
Op::BeginLayer {
layer_id: layer_id.clone(),
},
Op::SetFillColor {
col: Color::Rgb(Rgb {
r: 1.0,
g: 0.0,
b: 0.0,
icc_profile: None,
}),
},
Op::DrawPolygon {
polygon: Polygon {
rings: vec![PolygonRing { points: vec![] }],
mode: printpdf::PaintMode::Fill,
winding_order: printpdf::WindingOrder::NonZero,
},
},
Op::EndLayer,
];
let page = PdfPage::new(Mm(210.0), Mm(297.0), ops);
doc.pages.push(page);
let mut warnings = Vec::new();
let bytes = doc.save(&PdfSaveOptions::default(), &mut warnings);
let parsed_doc =
PdfDocument::parse(&bytes, &PdfParseOptions::default(), &mut Vec::new()).unwrap();
assert!(
!parsed_doc.resources.layers.map.is_empty(),
"No layers found in parsed document"
);
let parsed_layer = parsed_doc.resources.layers.map.values().next().unwrap();
assert_eq!(parsed_layer.name, "Test Layer");
assert_eq!(parsed_layer.creator, "Test Creator");
let parsed_page = &parsed_doc.pages[0];
let has_begin_layer = parsed_page.ops.iter().any(|op| {
matches!(op, Op::BeginLayer { .. } | Op::BeginOptionalContent { .. })
});
assert!(has_begin_layer, "Begin layer operation not found");
let has_end_layer = parsed_page.ops.iter().any(|op| {
matches!(op, Op::EndLayer | Op::EndOptionalContent | Op::EndMarkedContent)
});
assert!(has_end_layer, "End layer operation not found");
}
#[test]
fn test_extgstate_parsing() {
let mut doc = PdfDocument::new("ExtGState Parsing Test");
let dash_pattern = LineDashPattern {
offset: 0,
dash_1: Some(5),
gap_1: Some(3),
dash_2: Some(2),
gap_2: Some(1),
dash_3: None,
gap_3: None,
};
let gs = ExtendedGraphicsState::default()
.with_line_width(2.0)
.with_line_cap(LineCapStyle::Round)
.with_line_join(LineJoinStyle::Bevel)
.with_miter_limit(10.0)
.with_line_dash_pattern(Some(dash_pattern))
.with_rendering_intent(RenderingIntent::Perceptual)
.with_overprint_stroke(true)
.with_overprint_fill(true)
.with_overprint_mode(OverprintMode::KeepUnderlying)
.with_stroke_adjustment(true)
.with_blend_mode(BlendMode::Seperable(SeperableBlendMode::Multiply))
.with_current_stroke_alpha(0.5)
.with_current_fill_alpha(0.7);
let gs_id = doc.add_graphics_state(gs.clone());
let ops = vec![
Op::SaveGraphicsState,
Op::LoadGraphicsState { gs: gs_id.clone() },
Op::SetFillColor {
col: Color::Rgb(Rgb {
r: 0.0,
g: 1.0,
b: 0.0,
icc_profile: None,
}),
},
Op::DrawPolygon {
polygon: Polygon {
rings: vec![PolygonRing { points: vec![] }],
mode: printpdf::PaintMode::Fill,
winding_order: printpdf::WindingOrder::NonZero,
},
},
Op::RestoreGraphicsState,
];
let page = PdfPage::new(Mm(210.0), Mm(297.0), ops);
doc.pages.push(page);
let mut warnings = Vec::new();
let bytes = doc.save(&PdfSaveOptions::default(), &mut warnings);
let parsed_doc =
PdfDocument::parse(&bytes, &PdfParseOptions::default(), &mut Vec::new()).unwrap();
assert!(
!parsed_doc.resources.extgstates.map.is_empty(),
"No extended graphics states found in parsed document"
);
let parsed_gs = parsed_doc.resources.extgstates.map.values().next().unwrap();
pretty_assertions::assert_eq!(parsed_gs.clone(), gs);
let parsed_page = &parsed_doc.pages[0];
let has_load_gs = parsed_page.ops.iter().any(|op| {
if let Op::LoadGraphicsState { gs: _ } = op {
true
} else {
false
}
});
assert!(has_load_gs, "Load graphics state operation not found");
}
#[test]
fn test_bookmark_parsing() {
let mut doc = PdfDocument::new("Bookmark Parsing Test");
for _ in 0..3 {
let page = PdfPage::new(Mm(210.0), Mm(297.0), vec![]);
doc.pages.push(page);
}
let _ = doc.add_bookmark("Chapter 1", 1);
let _ = doc.add_bookmark("Chapter 2", 2);
let _ = doc.add_bookmark("Chapter 3", 3);
let mut warnings = Vec::new();
let bytes = doc.save(&PdfSaveOptions::default(), &mut warnings);
let parsed_doc =
PdfDocument::parse(&bytes, &PdfParseOptions::default(), &mut Vec::new()).unwrap();
assert_eq!(
parsed_doc.bookmarks.map.len(),
3,
"Expected 3 bookmarks, found {}",
parsed_doc.bookmarks.map.len()
);
let bookmarks: Vec<(&PageAnnotId, &printpdf::PageAnnotation)> =
parsed_doc.bookmarks.map.iter().collect();
let chapter1 = bookmarks
.iter()
.find(|(_, b)| b.name == "Chapter 1")
.unwrap()
.1;
let chapter2 = bookmarks
.iter()
.find(|(_, b)| b.name == "Chapter 2")
.unwrap()
.1;
let chapter3 = bookmarks
.iter()
.find(|(_, b)| b.name == "Chapter 3")
.unwrap()
.1;
assert_eq!(chapter1.page, 1);
assert_eq!(chapter2.page, 2);
assert_eq!(chapter3.page, 3);
}
#[test]
fn test_complex_document_parsing() {
let mut doc = PdfDocument::new("Complex Document Parsing Test");
let layer1 = Layer {
name: "Background".to_string(),
creator: "Test Creator".to_string(),
intent: LayerIntent::View,
usage: LayerSubtype::Artwork,
};
let layer2 = Layer {
name: "Content".to_string(),
creator: "Test Creator".to_string(),
intent: LayerIntent::Design,
usage: LayerSubtype::Artwork,
};
let layer1_id = doc.add_layer(&layer1);
let layer2_id = doc.add_layer(&layer2);
let gs1 = ExtendedGraphicsState::default()
.with_line_width(1.5)
.with_line_cap(LineCapStyle::Butt)
.with_blend_mode(BlendMode::Seperable(SeperableBlendMode::Normal));
let gs2 = ExtendedGraphicsState::default()
.with_line_width(2.5)
.with_line_cap(LineCapStyle::Round)
.with_blend_mode(BlendMode::Seperable(SeperableBlendMode::Multiply))
.with_overprint_mode(OverprintMode::KeepUnderlying);
let gs1_id = doc.add_graphics_state(gs1);
let gs2_id = doc.add_graphics_state(gs2);
let ops1 = vec![
Op::BeginLayer {
layer_id: layer1_id.clone(),
},
Op::SaveGraphicsState,
Op::LoadGraphicsState { gs: gs1_id.clone() },
Op::StartTextSection,
Op::SetFont {
font: printpdf::ops::PdfFontHandle::Builtin(printpdf::BuiltinFont::Helvetica),
size: Pt(12.0),
},
Op::ShowText {
items: vec![TextItem::Text("Page 1 Content".to_string())],
},
Op::EndTextSection,
Op::RestoreGraphicsState,
Op::EndLayer,
];
let ops2 = vec![
Op::BeginLayer {
layer_id: layer2_id.clone(),
},
Op::SaveGraphicsState,
Op::LoadGraphicsState { gs: gs2_id.clone() },
Op::StartTextSection,
Op::SetFont {
font: printpdf::ops::PdfFontHandle::Builtin(printpdf::BuiltinFont::Helvetica),
size: Pt(14.0),
},
Op::ShowText {
items: vec![TextItem::Text("Page 2 Content".to_string())],
},
Op::EndTextSection,
Op::RestoreGraphicsState,
Op::EndLayer,
];
let ops3 = vec![
Op::BeginLayer {
layer_id: layer1_id.clone(),
},
Op::SaveGraphicsState,
Op::LoadGraphicsState { gs: gs1_id.clone() },
Op::SetFillColor {
col: Color::Rgb(Rgb {
r: 0.8,
g: 0.2,
b: 0.2,
icc_profile: None,
}),
},
Op::DrawPolygon {
polygon: Polygon {
rings: vec![PolygonRing { points: vec![] }],
mode: printpdf::PaintMode::Fill,
winding_order: printpdf::WindingOrder::NonZero,
},
},
Op::RestoreGraphicsState,
Op::EndLayer,
Op::BeginLayer {
layer_id: layer2_id.clone(),
},
Op::SaveGraphicsState,
Op::LoadGraphicsState { gs: gs2_id.clone() },
Op::SetFillColor {
col: Color::Rgb(Rgb {
r: 0.2,
g: 0.8,
b: 0.2,
icc_profile: None,
}),
},
Op::StartTextSection,
Op::SetFont {
font: printpdf::ops::PdfFontHandle::Builtin(printpdf::BuiltinFont::Helvetica),
size: Pt(16.0),
},
Op::ShowText {
items: vec![TextItem::Text("Page 3 Content".to_string())],
},
Op::EndTextSection,
Op::RestoreGraphicsState,
Op::EndLayer,
];
let page1 = PdfPage::new(Mm(210.0), Mm(297.0), ops1);
let page2 = PdfPage::new(Mm(210.0), Mm(297.0), ops2);
let page3 = PdfPage::new(Mm(210.0), Mm(297.0), ops3);
doc.pages.push(page1);
doc.pages.push(page2);
doc.pages.push(page3);
let _ = doc.add_bookmark("Background Section", 1);
let _ = doc.add_bookmark("Content Section", 2);
let _ = doc.add_bookmark("Combined Section", 3);
let mut warnings = Vec::new();
let bytes = doc.save(&PdfSaveOptions::default(), &mut warnings);
let parsed_doc =
PdfDocument::parse(&bytes, &PdfParseOptions::default(), &mut Vec::new()).unwrap();
assert_eq!(
parsed_doc.resources.layers.map.len(),
2,
"Expected 2 layers, found {}",
parsed_doc.resources.layers.map.len()
);
assert_eq!(
parsed_doc.resources.extgstates.map.len(),
2,
"Expected 2 graphics states, found {}",
parsed_doc.resources.extgstates.map.len()
);
assert_eq!(
parsed_doc.bookmarks.map.len(),
3,
"Expected 3 bookmarks, found {}",
parsed_doc.bookmarks.map.len()
);
let layer_names: Vec<String> = parsed_doc
.resources
.layers
.map
.values()
.map(|l| l.name.clone())
.collect();
assert!(layer_names.contains(&"Background".to_string()));
assert!(layer_names.contains(&"Content".to_string()));
let bookmark_names: Vec<String> = parsed_doc
.bookmarks
.map
.values()
.map(|b| b.name.clone())
.collect();
assert!(bookmark_names.contains(&"Background Section".to_string()));
assert!(bookmark_names.contains(&"Content Section".to_string()));
assert!(bookmark_names.contains(&"Combined Section".to_string()));
for (i, page) in parsed_doc.pages.iter().enumerate() {
let page_number = i + 1;
let layer_begins = page
.ops
.iter()
.filter(|op| {
matches!(op, Op::BeginLayer { .. } | Op::BeginOptionalContent { .. })
})
.count();
let layer_ends = page
.ops
.iter()
.filter(|op| {
matches!(op, Op::EndLayer | Op::EndOptionalContent | Op::EndMarkedContent)
})
.count();
match page_number {
1 | 2 => {
assert_eq!(
layer_begins, 1,
"Page {} should have 1 BeginLayer op",
page_number
);
assert_eq!(
layer_ends, 1,
"Page {} should have 1 EndLayer op",
page_number
);
}
3 => {
assert_eq!(
layer_begins, 2,
"Page {} should have 2 BeginLayer ops",
page_number
);
assert_eq!(
layer_ends, 2,
"Page {} should have 2 EndLayer ops",
page_number
);
}
_ => {}
}
let gs_loads = page
.ops
.iter()
.filter(|op| {
if let Op::LoadGraphicsState { gs: _ } = op {
true
} else {
false
}
})
.count();
match page_number {
1 | 2 => {
assert_eq!(
gs_loads, 1,
"Page {} should have 1 LoadGraphicsState op",
page_number
);
}
3 => {
assert_eq!(
gs_loads, 2,
"Page {} should have 2 LoadGraphicsState ops",
page_number
);
}
_ => {}
}
}
}
#[test]
fn test_extgstate_font_parsing() {
let mut doc = PdfDocument::new("ExtGState Font Parsing Test");
let font = BuiltinFont::Helvetica.get_subset_font();
let parsed_font = printpdf::ParsedFont::from_bytes(&font.bytes, 0, &mut Vec::new()).unwrap();
let font_id = doc.add_font(&parsed_font);
let gs = ExtendedGraphicsState::default()
.with_font(Some(BuiltinOrExternalFontId::External(font_id.clone())))
.with_line_width(1.5);
let gs_id = doc.add_graphics_state(gs);
let ops = vec![
Op::SaveGraphicsState,
Op::LoadGraphicsState { gs: gs_id.clone() },
Op::StartTextSection,
Op::SetFont {
font: printpdf::ops::PdfFontHandle::External(font_id.clone()),
size: Pt(14.0),
},
Op::ShowText {
items: vec![TextItem::Text("Testing Font in ExtGState".to_string())],
},
Op::EndTextSection,
Op::RestoreGraphicsState,
];
let page = PdfPage::new(Mm(210.0), Mm(297.0), ops);
doc.pages.push(page);
let mut warnings = Vec::new();
let bytes = doc.save(&PdfSaveOptions::default(), &mut warnings);
let parsed_doc =
PdfDocument::parse(&bytes, &PdfParseOptions::default(), &mut Vec::new()).unwrap();
assert!(
!parsed_doc.resources.extgstates.map.is_empty(),
"No extended graphics states found"
);
let parsed_page = &parsed_doc.pages[0];
let has_font_ops = parsed_page.ops.iter().any(|op| matches!(op, Op::ShowText { .. }));
assert!(has_font_ops, "Text operations with font not found");
}