use oxidize_pdf::annotations::{Annotation, AnnotationType, MarkupAnnotation};
use oxidize_pdf::geometry::{Point, Rectangle};
use oxidize_pdf::graphics::Color;
use oxidize_pdf::parser::PdfReader;
use oxidize_pdf::text::Font;
use oxidize_pdf::{Document, Page};
use std::io::Cursor;
fn create_pdf_with_annotations(annotations: Vec<Annotation>) -> Vec<u8> {
let mut doc = Document::new();
let mut page = Page::a4();
{
let text = page.text();
text.set_font(Font::Helvetica, 12.0);
text.at(72.0, 700.0);
let _ = text.write("Test page with annotations");
}
for annot in annotations {
page.add_annotation(annot);
}
doc.add_page(page);
doc.to_bytes().expect("Failed to write PDF to bytes")
}
fn count_page_annotations(pdf_bytes: &[u8]) -> usize {
let cursor = Cursor::new(pdf_bytes);
let reader = PdfReader::new(cursor).expect("Failed to parse PDF");
let doc = reader.into_document();
doc.get_page_annotations(0).unwrap_or_default().len()
}
fn get_annotation_subtype(pdf_bytes: &[u8], index: usize) -> Option<String> {
let cursor = Cursor::new(pdf_bytes);
let reader = PdfReader::new(cursor).expect("Failed to parse PDF");
let doc = reader.into_document();
let annots = doc.get_page_annotations(0).unwrap_or_default();
let annot = annots.get(index)?;
annot
.get("Subtype")
.and_then(|s| s.as_name())
.map(|n| n.0.clone())
}
fn annotation_has_key(pdf_bytes: &[u8], index: usize, key: &str) -> bool {
let cursor = Cursor::new(pdf_bytes);
let reader = PdfReader::new(cursor).expect("Failed to parse PDF");
let doc = reader.into_document();
let annots = doc.get_page_annotations(0).unwrap_or_default();
annots
.get(index)
.map(|a| a.contains_key(key))
.unwrap_or(false)
}
fn get_annotation_rect(pdf_bytes: &[u8], index: usize) -> Option<[f64; 4]> {
let cursor = Cursor::new(pdf_bytes);
let reader = PdfReader::new(cursor).expect("Failed to parse PDF");
let doc = reader.into_document();
let annots = doc.get_page_annotations(0).unwrap_or_default();
let annot = annots.get(index)?;
let rect_obj = annot.get("Rect")?;
let rect_arr = rect_obj.as_array()?;
if rect_arr.len() != 4 {
return None;
}
let vals: Vec<f64> = (0..4)
.filter_map(|i| rect_arr.get(i).and_then(|v| v.as_real()))
.collect();
if vals.len() == 4 {
Some([vals[0], vals[1], vals[2], vals[3]])
} else {
None
}
}
fn get_annotation_string_field(pdf_bytes: &[u8], index: usize, field: &str) -> Option<String> {
let cursor = Cursor::new(pdf_bytes);
let reader = PdfReader::new(cursor).expect("Failed to parse PDF");
let doc = reader.into_document();
let annots = doc.get_page_annotations(0).unwrap_or_default();
let annot = annots.get(index)?;
let val = annot.get(field)?;
val.as_string()
.map(|s| String::from_utf8_lossy(&s.0).to_string())
}
fn page_has_annotations(pdf_bytes: &[u8]) -> bool {
count_page_annotations(pdf_bytes) > 0
}
#[test]
fn test_highlight_annotation_appears_in_annots_array() {
let rect = Rectangle::new(Point::new(100.0, 700.0), Point::new(300.0, 712.0));
let highlight = MarkupAnnotation::highlight(rect);
let annot = highlight.to_annotation();
let pdf_bytes = create_pdf_with_annotations(vec![annot]);
assert_eq!(
count_page_annotations(&pdf_bytes),
1,
"Page should have exactly 1 annotation in /Annots"
);
assert_eq!(
get_annotation_subtype(&pdf_bytes, 0),
Some("Highlight".to_string()),
"Annotation should have /Subtype /Highlight"
);
}
#[test]
fn test_multiple_annotations_written() {
let rect1 = Rectangle::new(Point::new(100.0, 700.0), Point::new(300.0, 712.0));
let rect2 = Rectangle::new(Point::new(100.0, 680.0), Point::new(300.0, 692.0));
let rect3 = Rectangle::new(Point::new(100.0, 660.0), Point::new(300.0, 672.0));
let annots = vec![
MarkupAnnotation::highlight(rect1).to_annotation(),
MarkupAnnotation::underline(rect2).to_annotation(),
MarkupAnnotation::strikeout(rect3).to_annotation(),
];
let pdf_bytes = create_pdf_with_annotations(annots);
assert_eq!(
count_page_annotations(&pdf_bytes),
3,
"Page should have exactly 3 annotations"
);
assert_eq!(
get_annotation_subtype(&pdf_bytes, 0),
Some("Highlight".to_string())
);
assert_eq!(
get_annotation_subtype(&pdf_bytes, 1),
Some("Underline".to_string())
);
assert_eq!(
get_annotation_subtype(&pdf_bytes, 2),
Some("StrikeOut".to_string())
);
}
#[test]
fn test_annotation_rect_written_correctly() {
let rect = Rectangle::new(Point::new(72.0, 500.0), Point::new(200.0, 515.0));
let highlight = MarkupAnnotation::highlight(rect);
let annot = highlight.to_annotation();
let pdf_bytes = create_pdf_with_annotations(vec![annot]);
let written_rect = get_annotation_rect(&pdf_bytes, 0).expect("Annotation should have a /Rect");
assert!(
(written_rect[0] - 72.0).abs() < 0.01,
"x1 should be 72.0, got {}",
written_rect[0]
);
assert!(
(written_rect[1] - 500.0).abs() < 0.01,
"y1 should be 500.0, got {}",
written_rect[1]
);
assert!(
(written_rect[2] - 200.0).abs() < 0.01,
"x2 should be 200.0, got {}",
written_rect[2]
);
assert!(
(written_rect[3] - 515.0).abs() < 0.01,
"y2 should be 515.0, got {}",
written_rect[3]
);
}
#[test]
fn test_annotation_quad_points_written() {
let rect = Rectangle::new(Point::new(100.0, 700.0), Point::new(300.0, 712.0));
let highlight = MarkupAnnotation::highlight(rect);
let annot = highlight.to_annotation();
let pdf_bytes = create_pdf_with_annotations(vec![annot]);
assert!(
annotation_has_key(&pdf_bytes, 0, "QuadPoints"),
"Highlight annotation should have /QuadPoints"
);
}
#[test]
fn test_annotation_color_written() {
let rect = Rectangle::new(Point::new(100.0, 700.0), Point::new(300.0, 712.0));
let highlight = MarkupAnnotation::highlight(rect).with_color(Color::Rgb(1.0, 0.0, 0.0));
let annot = highlight.to_annotation();
let pdf_bytes = create_pdf_with_annotations(vec![annot]);
assert!(
annotation_has_key(&pdf_bytes, 0, "C"),
"Annotation should have /C (color) entry"
);
}
#[test]
fn test_widget_annotations_still_work_after_refactor() {
let mut doc = Document::new();
let mut page = Page::a4();
{
let text = page.text();
text.set_font(Font::Helvetica, 12.0);
text.at(72.0, 700.0);
let _ = text.write("Form page");
}
let widget_rect = Rectangle::new(Point::new(72.0, 650.0), Point::new(272.0, 670.0));
let widget = Annotation::new(AnnotationType::Widget, widget_rect).with_contents("text_field_1");
page.add_annotation(widget);
let hl_rect = Rectangle::new(Point::new(100.0, 600.0), Point::new(300.0, 612.0));
let highlight = MarkupAnnotation::highlight(hl_rect).to_annotation();
page.add_annotation(highlight);
doc.add_page(page);
let pdf_bytes = doc.to_bytes().expect("Failed to write PDF");
let annot_count = count_page_annotations(&pdf_bytes);
assert!(
annot_count >= 2,
"Page should have at least 2 annotations (widget + highlight), got {}",
annot_count
);
let mut found_highlight = false;
let mut found_widget = false;
for i in 0..annot_count {
if let Some(subtype) = get_annotation_subtype(&pdf_bytes, i) {
if subtype == "Highlight" {
found_highlight = true;
}
if subtype == "Widget" {
found_widget = true;
}
}
}
assert!(found_highlight, "Should find a Highlight annotation");
assert!(found_widget, "Should find a Widget annotation");
}
#[test]
fn test_empty_annotations_no_annots_key() {
let mut doc = Document::new();
let mut page = Page::a4();
{
let text = page.text();
text.set_font(Font::Helvetica, 12.0);
text.at(72.0, 700.0);
let _ = text.write("No annotations on this page");
}
doc.add_page(page);
let pdf_bytes = doc.to_bytes().expect("Failed to write PDF");
assert!(
!page_has_annotations(&pdf_bytes),
"Page without annotations should NOT have any annotations"
);
}
#[test]
fn test_annotation_with_contents_string() {
let rect = Rectangle::new(Point::new(100.0, 700.0), Point::new(300.0, 712.0));
let annot = Annotation::new(AnnotationType::Text, rect).with_contents("This is a comment");
let pdf_bytes = create_pdf_with_annotations(vec![annot]);
assert_eq!(count_page_annotations(&pdf_bytes), 1);
assert_eq!(
get_annotation_subtype(&pdf_bytes, 0),
Some("Text".to_string())
);
let contents = get_annotation_string_field(&pdf_bytes, 0, "Contents");
assert_eq!(
contents,
Some("This is a comment".to_string()),
"Annotation should have /Contents with the correct text"
);
}