use rpdfium::{
ArcDocument, ArcLibrary, Document, Error, Library, OpenOptions, ParsingMode, PdfReader,
RenderConfig, RgbaColor,
};
fn build_multi_page_pdf(page_count: usize) -> Vec<u8> {
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
let kids: Vec<String> = (0..page_count).map(|i| format!("{} 0 R", i + 3)).collect();
let kids_str = kids.join(" ");
let pages_dict =
format!("2 0 obj\n<< /Type /Pages /Kids [{kids_str}] /Count {page_count} >>\nendobj\n");
pdf.extend_from_slice(pages_dict.as_bytes());
let mut page_offsets = Vec::new();
for i in 0..page_count {
let offset = pdf.len();
page_offsets.push(offset);
let obj_num = i + 3;
let page_obj = format!(
"{obj_num} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"
);
pdf.extend_from_slice(page_obj.as_bytes());
}
let xref_offset = pdf.len();
let total_objects = page_count + 3; pdf.extend_from_slice(b"xref\n");
pdf.extend_from_slice(format!("0 {total_objects}\n").as_bytes());
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
for offset in &page_offsets {
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", offset).as_bytes());
}
pdf.extend_from_slice(b"trailer\n");
pdf.extend_from_slice(format!("<< /Size {total_objects} /Root 1 0 R >>\n").as_bytes());
pdf.extend_from_slice(format!("startxref\n{xref_offset}\n%%EOF").as_bytes());
pdf
}
fn build_pdf_with_metadata() -> Vec<u8> {
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [4 0 R] /Count 1 >>\nendobj\n");
let obj3_offset = pdf.len();
pdf.extend_from_slice(
b"3 0 obj\n<< /Title (Test Document) /Author (rpdfium) /Producer (rpdfium test) >>\nendobj\n",
);
let obj4_offset = pdf.len();
pdf.extend_from_slice(
b"4 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] >>\nendobj\n",
);
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 5\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj3_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj4_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n");
pdf.extend_from_slice(b"<< /Size 5 /Root 1 0 R /Info 3 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{xref_offset}\n%%EOF").as_bytes());
pdf
}
fn build_pdf_with_content() -> Vec<u8> {
let content = b"1 0 0 rg 100 100 200 200 re f";
let content_len = content.len();
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
let obj3_offset = pdf.len();
pdf.extend_from_slice(
b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>\nendobj\n",
);
let obj4_offset = pdf.len();
let stream_header = format!("4 0 obj\n<< /Length {} >>\nstream\n", content_len);
pdf.extend_from_slice(stream_header.as_bytes());
pdf.extend_from_slice(content);
pdf.extend_from_slice(b"\nendstream\nendobj\n");
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 5\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj3_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj4_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n");
pdf.extend_from_slice(b"<< /Size 5 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{xref_offset}\n%%EOF").as_bytes());
pdf
}
fn build_pdf_with_text(text: &str, font_size: f32) -> Vec<u8> {
let escaped = text
.replace('\\', "\\\\")
.replace('(', "\\(")
.replace(')', "\\)");
let content = format!("BT /F1 {} Tf 72 700 Td ({}) Tj ET", font_size, escaped);
let content_bytes = content.as_bytes();
let content_len = content_bytes.len();
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [4 0 R] /Count 1 >>\nendobj\n");
let obj3_offset = pdf.len();
pdf.extend_from_slice(
b"3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n",
);
let obj4_offset = pdf.len();
pdf.extend_from_slice(
b"4 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << /Font << /F1 3 0 R >> >> >>\nendobj\n",
);
let obj5_offset = pdf.len();
let stream_header = format!("5 0 obj\n<< /Length {} >>\nstream\n", content_len);
pdf.extend_from_slice(stream_header.as_bytes());
pdf.extend_from_slice(content_bytes);
pdf.extend_from_slice(b"\nendstream\nendobj\n");
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 6\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj3_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj4_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj5_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n");
pdf.extend_from_slice(b"<< /Size 6 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{xref_offset}\n%%EOF").as_bytes());
pdf
}
fn build_pdf_with_two_lines(line1: &str, line2: &str, font_size: f32) -> Vec<u8> {
let esc1 = line1
.replace('\\', "\\\\")
.replace('(', "\\(")
.replace(')', "\\)");
let esc2 = line2
.replace('\\', "\\\\")
.replace('(', "\\(")
.replace(')', "\\)");
let content = format!(
"BT /F1 {} Tf 72 700 Td ({}) Tj 0 -20 Td ({}) Tj ET",
font_size, esc1, esc2
);
let content_bytes = content.as_bytes();
let content_len = content_bytes.len();
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [4 0 R] /Count 1 >>\nendobj\n");
let obj3_offset = pdf.len();
pdf.extend_from_slice(
b"3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n",
);
let obj4_offset = pdf.len();
pdf.extend_from_slice(
b"4 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << /Font << /F1 3 0 R >> >> >>\nendobj\n",
);
let obj5_offset = pdf.len();
let stream_header = format!("5 0 obj\n<< /Length {} >>\nstream\n", content_len);
pdf.extend_from_slice(stream_header.as_bytes());
pdf.extend_from_slice(content_bytes);
pdf.extend_from_slice(b"\nendstream\nendobj\n");
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 6\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj3_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj4_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj5_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n");
pdf.extend_from_slice(b"<< /Size 6 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{xref_offset}\n%%EOF").as_bytes());
pdf
}
#[test]
fn test_document_open_valid_pdf() {
let lib = Library::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions {
parsing_mode: ParsingMode::Strict,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts);
assert!(doc.is_ok());
}
#[test]
fn test_document_open_invalid_data() {
let lib = Library::new();
let opts = OpenOptions::default();
let result = Document::open(&lib, b"not a pdf".to_vec(), &opts);
assert!(result.is_err());
}
#[test]
fn test_document_page_count_zero() {
let lib = Library::new();
let pdf = build_multi_page_pdf(0);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
assert_eq!(doc.page_count(), 0);
}
#[test]
fn test_document_page_count_single() {
let lib = Library::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
assert_eq!(doc.page_count(), 1);
}
#[test]
fn test_document_page_count_multiple() {
let lib = Library::new();
let pdf = build_multi_page_pdf(5);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
assert_eq!(doc.page_count(), 5);
}
#[test]
fn test_document_page_valid_index() {
let lib = Library::new();
let pdf = build_multi_page_pdf(3);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
assert!(doc.page(0).is_ok());
assert!(doc.page(1).is_ok());
assert!(doc.page(2).is_ok());
}
#[test]
fn test_document_page_out_of_range() {
let lib = Library::new();
let pdf = build_multi_page_pdf(2);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
match doc.page(2) {
Err(Error::PageOutOfRange { index, count }) => {
assert_eq!(index, 2);
assert_eq!(count, 2);
}
other => panic!("expected PageOutOfRange, got {:?}", other.err()),
}
match doc.page(100) {
Err(Error::PageOutOfRange { index, count }) => {
assert_eq!(index, 100);
assert_eq!(count, 2);
}
other => panic!("expected PageOutOfRange, got {:?}", other.err()),
}
}
#[test]
fn test_document_page_empty_document() {
let lib = Library::new();
let pdf = build_multi_page_pdf(0);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
assert!(matches!(
doc.page(0),
Err(Error::PageOutOfRange { index: 0, count: 0 })
));
}
#[test]
fn test_page_media_box() {
let lib = Library::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let mb = page.media_box();
assert!((mb.left - 0.0).abs() < 0.001);
assert!((mb.bottom - 0.0).abs() < 0.001);
assert!((mb.right - 612.0).abs() < 0.001);
assert!((mb.top - 792.0).abs() < 0.001);
}
#[test]
fn test_page_crop_box_absent() {
let lib = Library::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let crop = page.crop_box().unwrap();
assert!(crop.is_none());
}
#[test]
fn test_page_rotation_default() {
let lib = Library::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
assert_eq!(page.rotation().unwrap(), 0);
}
#[test]
fn test_page_index() {
let lib = Library::new();
let pdf = build_multi_page_pdf(3);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
for i in 0..3 {
let page = doc.page(i).unwrap();
assert_eq!(page.index(), i);
}
}
#[test]
fn test_page_interpret_empty_page() {
let lib = Library::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let tree = page.interpret().unwrap();
assert!(matches!(tree.root, rpdfium_page::DisplayNode::Group { .. }));
}
#[test]
fn test_page_interpret_with_content() {
let lib = Library::new();
let pdf = build_pdf_with_content();
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let tree = page.interpret().unwrap();
assert!(matches!(tree.root, rpdfium_page::DisplayNode::Group { .. }));
}
#[test]
fn test_page_interpret_cached() {
let lib = Library::new();
let pdf = build_pdf_with_content();
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let tree1 = page.interpret().unwrap();
let tree2 = page.interpret().unwrap();
assert!(std::ptr::eq(tree1, tree2));
}
#[test]
fn test_page_render_produces_bitmap() {
let lib = Library::new();
let pdf = build_pdf_with_content();
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let config = RenderConfig {
width: 100,
height: 100,
background: RgbaColor::WHITE,
media_box: Some(page.media_box()),
rotation: 0,
tile_size: None,
progress: None,
grayscale: false,
antialiasing: true,
..RenderConfig::default()
};
let bitmap = page.render(&config).unwrap();
assert_eq!(bitmap.width, 100);
assert_eq!(bitmap.height, 100);
assert!(!bitmap.data.is_empty());
}
#[test]
fn test_page_text_empty() {
let lib = Library::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
assert!(text_page.all_page_text().is_empty());
assert_eq!(text_page.char_count(), 0);
}
#[test]
fn test_page_annotations_empty() {
let lib = Library::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let annots = page.annotations().unwrap();
assert!(annots.is_empty());
}
#[test]
fn test_metadata_present() {
let lib = Library::new();
let pdf = build_pdf_with_metadata();
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let meta = doc.metadata().unwrap();
assert!(meta.is_some());
let meta = meta.unwrap();
assert_eq!(meta.title.as_deref(), Some("Test Document"));
assert_eq!(meta.author.as_deref(), Some("rpdfium"));
assert_eq!(meta.producer.as_deref(), Some("rpdfium test"));
}
#[test]
fn test_metadata_absent() {
let lib = Library::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let meta = doc.metadata().unwrap();
assert!(meta.is_none());
}
#[test]
fn test_bookmarks_empty() {
let lib = Library::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let bookmarks = doc.bookmarks().unwrap();
assert!(bookmarks.is_empty());
}
#[test]
fn test_page_custom_media_box() {
let lib = Library::new();
let pdf = build_pdf_with_metadata(); let opts = OpenOptions::default();
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let mb = page.media_box();
assert!((mb.right - 595.0).abs() < 0.001);
assert!((mb.top - 842.0).abs() < 0.001);
}
#[test]
fn test_error_display_page_out_of_range() {
let err = Error::PageOutOfRange { index: 5, count: 3 };
let msg = format!("{err}");
assert!(msg.contains("5"));
assert!(msg.contains("3"));
}
#[test]
fn test_arc_document_open_valid_pdf() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions {
parsing_mode: ParsingMode::Strict,
..OpenOptions::default()
};
let doc = ArcDocument::open(&lib, pdf, &opts);
assert!(doc.is_ok());
}
#[test]
fn test_arc_document_open_invalid_data() {
let lib = ArcLibrary::new();
let opts = OpenOptions::default();
let result = ArcDocument::open(&lib, b"not a pdf".to_vec(), &opts);
assert!(result.is_err());
}
#[test]
fn test_arc_document_page_count() {
let lib = ArcLibrary::new();
let opts = OpenOptions::default();
let doc0 = ArcDocument::open(&lib, build_multi_page_pdf(0), &opts).unwrap();
assert_eq!(doc0.page_count(), 0);
let doc1 = ArcDocument::open(&lib, build_multi_page_pdf(1), &opts).unwrap();
assert_eq!(doc1.page_count(), 1);
let doc5 = ArcDocument::open(&lib, build_multi_page_pdf(5), &opts).unwrap();
assert_eq!(doc5.page_count(), 5);
}
#[test]
fn test_arc_document_page_access() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(3);
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
assert!(doc.page(0).is_ok());
assert!(doc.page(1).is_ok());
assert!(doc.page(2).is_ok());
assert!(matches!(
doc.page(3),
Err(Error::PageOutOfRange { index: 3, count: 3 })
));
}
#[test]
fn test_arc_page_media_box() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let mb = page.media_box();
assert!((mb.left - 0.0).abs() < 0.001);
assert!((mb.bottom - 0.0).abs() < 0.001);
assert!((mb.right - 612.0).abs() < 0.001);
assert!((mb.top - 792.0).abs() < 0.001);
}
#[test]
fn test_arc_page_crop_box_absent() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
assert!(page.crop_box().unwrap().is_none());
}
#[test]
fn test_arc_page_rotation_default() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
assert_eq!(page.rotation().unwrap(), 0);
}
#[test]
fn test_arc_page_index() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(3);
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
for i in 0..3 {
let page = doc.page(i).unwrap();
assert_eq!(page.index(), i);
}
}
#[test]
fn test_arc_page_interpret_empty() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let tree = page.interpret().unwrap();
assert!(matches!(tree.root, rpdfium_page::DisplayNode::Group { .. }));
}
#[test]
fn test_arc_page_interpret_with_content() {
let lib = ArcLibrary::new();
let pdf = build_pdf_with_content();
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let tree = page.interpret().unwrap();
assert!(matches!(tree.root, rpdfium_page::DisplayNode::Group { .. }));
}
#[test]
fn test_arc_page_interpret_cached() {
let lib = ArcLibrary::new();
let pdf = build_pdf_with_content();
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let tree1 = page.interpret().unwrap();
let tree2 = page.interpret().unwrap();
assert!(std::ptr::eq(tree1, tree2));
}
#[test]
fn test_arc_page_render() {
let lib = ArcLibrary::new();
let pdf = build_pdf_with_content();
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let config = RenderConfig {
width: 100,
height: 100,
background: RgbaColor::WHITE,
media_box: Some(page.media_box()),
rotation: 0,
tile_size: None,
progress: None,
grayscale: false,
antialiasing: true,
..RenderConfig::default()
};
let bitmap = page.render(&config).unwrap();
assert_eq!(bitmap.width, 100);
assert_eq!(bitmap.height, 100);
assert!(!bitmap.data.is_empty());
}
#[test]
fn test_arc_page_text_empty() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
assert!(text_page.all_page_text().is_empty());
assert_eq!(text_page.char_count(), 0);
}
#[test]
fn test_arc_page_annotations_empty() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
assert!(page.annotations().unwrap().is_empty());
}
#[test]
fn test_arc_metadata_present() {
let lib = ArcLibrary::new();
let pdf = build_pdf_with_metadata();
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
let meta = doc.metadata().unwrap();
assert!(meta.is_some());
let meta = meta.unwrap();
assert_eq!(meta.title.as_deref(), Some("Test Document"));
assert_eq!(meta.author.as_deref(), Some("rpdfium"));
}
#[test]
fn test_arc_metadata_absent() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
assert!(doc.metadata().unwrap().is_none());
}
#[test]
fn test_arc_bookmarks_empty() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
assert!(doc.bookmarks().unwrap().is_empty());
}
#[test]
fn test_arc_document_clone_shares_data() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(3);
let opts = OpenOptions::default();
let doc1 = ArcDocument::open(&lib, pdf, &opts).unwrap();
let doc2 = doc1.clone();
assert_eq!(doc1.page_count(), doc2.page_count());
assert!(std::ptr::eq(doc1.store(), doc2.store()));
}
#[test]
fn test_arc_page_document_reference() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(1);
let opts = OpenOptions::default();
let doc = ArcDocument::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
assert_eq!(page.document().page_count(), 1);
}
#[test]
fn test_arc_types_are_send_sync() {
fn assert_send_sync<T: Send + Sync + 'static>() {}
assert_send_sync::<ArcLibrary>();
assert_send_sync::<ArcDocument>();
assert_send_sync::<rpdfium::ArcPage>();
}
#[test]
fn test_document_open_file_valid() {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("rpdfium_test_file.pdf");
std::fs::write(&path, build_multi_page_pdf(2)).unwrap();
let lib = Library::new();
let opts = OpenOptions::default();
let doc = Document::open_file(&lib, &path, &opts).unwrap();
assert_eq!(doc.page_count(), 2);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_document_open_file_with_content() {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("rpdfium_test_content.pdf");
std::fs::write(&path, build_pdf_with_content()).unwrap();
let lib = Library::new();
let opts = OpenOptions::default();
let doc = Document::open_file(&lib, &path, &opts).unwrap();
let page = doc.page(0).unwrap();
let tree = page.interpret().unwrap();
assert!(matches!(tree.root, rpdfium_page::DisplayNode::Group { .. }));
std::fs::remove_file(&path).ok();
}
#[test]
fn test_document_open_file_nonexistent() {
let lib = Library::new();
let opts = OpenOptions::default();
let result = Document::open_file(&lib, "/nonexistent/path/file.pdf", &opts);
assert!(result.is_err());
match result {
Err(Error::Parse(rpdfium::PdfError::Io(_))) => {
}
other => panic!("expected I/O error, got {:?}", other.err()),
}
}
#[test]
fn test_document_open_file_accepts_string() {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("rpdfium_test_string.pdf");
std::fs::write(&path, build_multi_page_pdf(1)).unwrap();
let lib = Library::new();
let opts = OpenOptions::default();
let doc = Document::open_file(&lib, path.to_string_lossy().to_string(), &opts).unwrap();
assert_eq!(doc.page_count(), 1);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_arc_document_open_file_valid() {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("rpdfium_test_arc.pdf");
std::fs::write(&path, build_multi_page_pdf(3)).unwrap();
let lib = ArcLibrary::new();
let opts = OpenOptions::default();
let doc = ArcDocument::open_file(&lib, &path, &opts).unwrap();
assert_eq!(doc.page_count(), 3);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_arc_document_open_file_with_content() {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join("rpdfium_test_arc_content.pdf");
std::fs::write(&path, build_pdf_with_content()).unwrap();
let lib = ArcLibrary::new();
let opts = OpenOptions::default();
let doc = ArcDocument::open_file(&lib, &path, &opts).unwrap();
let page = doc.page(0).unwrap();
let tree = page.interpret().unwrap();
assert!(matches!(tree.root, rpdfium_page::DisplayNode::Group { .. }));
std::fs::remove_file(&path).ok();
}
#[test]
fn test_arc_document_open_file_nonexistent() {
let lib = ArcLibrary::new();
let opts = OpenOptions::default();
let result = ArcDocument::open_file(&lib, "/nonexistent/arc/file.pdf", &opts);
assert!(result.is_err());
match result {
Err(Error::Parse(rpdfium::PdfError::Io(_))) => {
}
other => panic!("expected I/O error, got {:?}", other.err()),
}
}
#[test]
fn test_document_open_empty_data() {
let lib = Library::new();
let opts = OpenOptions::default();
let result = Document::open(&lib, Vec::new(), &opts);
assert!(result.is_err());
}
#[test]
fn test_arc_document_open_empty_data() {
let lib = ArcLibrary::new();
let opts = OpenOptions::default();
let result = ArcDocument::open(&lib, Vec::new(), &opts);
assert!(result.is_err());
}
#[test]
fn test_error_from_pdf_error() {
let pdf_err = rpdfium::PdfError::InvalidHeader;
let err: Error = Error::Parse(pdf_err);
let msg = format!("{err}");
assert!(!msg.is_empty());
}
#[test]
fn test_error_from_page_error() {
let page_err = rpdfium::PageError::TooManyOperators;
let err: Error = Error::Page(page_err);
let msg = format!("{err}");
assert!(!msg.is_empty());
}
#[test]
fn test_re_exports_accessible() {
let _: rpdfium::Rect = rpdfium::Rect::new(0.0, 0.0, 100.0, 100.0);
let _: rpdfium::Point = rpdfium::Point { x: 0.0, y: 0.0 };
let _: rpdfium::Size = rpdfium::Size {
width: 100.0,
height: 100.0,
};
let _: rpdfium::RgbaColor = rpdfium::RgbaColor::WHITE;
let _: rpdfium::BitmapFormat = rpdfium::BitmapFormat::Rgba32;
let _: rpdfium::ParsingMode = rpdfium::ParsingMode::Lenient;
let _ = rpdfium::OpenOptions::default();
}
#[test]
fn test_text_extraction_basic() {
let lib = Library::new();
let pdf = build_pdf_with_text("Hello World", 12.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
assert!(text_page.char_count() > 0);
let full_text = text_page.all_page_text();
assert!(
full_text.contains("Hello") || full_text.contains("World"),
"expected text to contain 'Hello' or 'World', got: {:?}",
full_text
);
}
#[test]
fn test_text_extraction_char_properties() {
let lib = Library::new();
let pdf = build_pdf_with_text("ABCDE", 14.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
let count = text_page.char_count();
assert!(count > 0, "expected at least one character");
let ch = text_page.unicode(0);
assert!(ch.is_some(), "expected Some(char) for index 0");
let fs = text_page.font_size(0);
assert!(fs.is_some(), "expected Some(f32) font size");
if let Some(size) = fs {
assert!(
(size - 14.0).abs() < 2.0,
"font size {} not close to 14.0",
size
);
}
let char_box = text_page.char_box(0);
assert!(char_box.is_some(), "expected Some(CharRect) for index 0");
let mat = text_page.matrix(0);
assert!(mat.is_some(), "expected Some([f32; 6]) for index 0");
let origin = text_page.char_origin(0);
assert!(origin.is_some(), "expected Some(CharOrigin) for index 0");
let generated = text_page.is_generated(0);
assert_eq!(generated, Some(false));
assert!(text_page.unicode(99999).is_none());
assert!(text_page.font_size(99999).is_none());
assert!(text_page.char_box(99999).is_none());
assert!(text_page.matrix(99999).is_none());
assert!(text_page.char_origin(99999).is_none());
assert!(text_page.is_generated(99999).is_none());
}
#[test]
fn test_text_search_basic() {
use rpdfium::{SearchOptions, TextPageFind};
let lib = Library::new();
let pdf = build_pdf_with_text("The quick brown fox", 12.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
let full_text = text_page.all_page_text();
let search_opts = SearchOptions {
match_case: false,
match_whole_word: false,
consecutive: false,
};
let mut finder = TextPageFind::new(full_text, "quick", search_opts);
let result = finder.find_next();
assert!(
result.is_some(),
"expected to find 'quick' in text: {:?}",
full_text
);
}
#[test]
fn test_text_link_extraction_integration() {
let lib = Library::new();
let pdf = build_pdf_with_text("Visit https://example.com today", 12.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
let links = text_page.extract_links();
assert!(
!links.is_empty(),
"expected at least one link, text was: {:?}",
text_page.all_page_text()
);
let has_example = links.iter().any(|l| l.url.contains("example.com"));
assert!(
has_example,
"expected link with example.com, got: {:?}",
links
);
}
#[test]
fn test_text_extraction_multiple_lines() {
let lib = Library::new();
let pdf = build_pdf_with_two_lines("First line", "Second line", 12.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
let full_text = text_page.all_page_text();
assert!(
text_page.char_count() > 0,
"expected characters from two-line PDF"
);
assert!(
full_text.contains("First") && full_text.contains("Second"),
"expected both lines in text, got: {:?}",
full_text
);
}
#[test]
fn test_text_page_rect_array() {
let lib = Library::new();
let pdf = build_pdf_with_text("Rectangle test", 12.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
let count = text_page.char_count();
assert!(count > 0, "expected at least one character");
let rects = text_page.rect_array(0, count);
assert!(
!rects.is_empty(),
"expected non-empty rect array for text range"
);
}
fn build_pdf_with_colored_text(text: &str, r: f32, g: f32, b: f32, font_size: f32) -> Vec<u8> {
let escaped = text
.replace('\\', "\\\\")
.replace('(', "\\(")
.replace(')', "\\)");
let content = format!(
"{} {} {} rg BT /F1 {} Tf 72 700 Td ({}) Tj ET",
r, g, b, font_size, escaped
);
let content_bytes = content.as_bytes();
let content_len = content_bytes.len();
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [4 0 R] /Count 1 >>\nendobj\n");
let obj3_offset = pdf.len();
pdf.extend_from_slice(
b"3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n",
);
let obj4_offset = pdf.len();
pdf.extend_from_slice(
b"4 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << /Font << /F1 3 0 R >> >> >>\nendobj\n",
);
let obj5_offset = pdf.len();
let stream_header = format!("5 0 obj\n<< /Length {} >>\nstream\n", content_len);
pdf.extend_from_slice(stream_header.as_bytes());
pdf.extend_from_slice(content_bytes);
pdf.extend_from_slice(b"\nendstream\nendobj\n");
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 6\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj3_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj4_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj5_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n");
pdf.extend_from_slice(b"<< /Size 6 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{xref_offset}\n%%EOF").as_bytes());
pdf
}
#[test]
fn test_text_get_index_at_pos() {
let lib = Library::new();
let pdf = build_pdf_with_text("Hello", 12.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
assert!(
text_page.char_count() > 0,
"expected characters to be extracted"
);
let idx = text_page.index_at_pos(80.0, 706.0, 50.0, 50.0);
assert!(
idx.is_some(),
"expected index_at_pos to find a character near (80, 706), text: {:?}",
text_page.all_page_text()
);
}
#[test]
fn test_text_get_text_by_rect() {
let lib = Library::new();
let pdf = build_pdf_with_text("Hello World", 12.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
let result = text_page.text_by_rect(0.0, 650.0, 400.0, 750.0);
assert!(
!result.is_empty(),
"expected text in rect, full text: {:?}",
text_page.all_page_text()
);
assert!(
result.contains("Hello") || result.chars().count() >= 5,
"expected at least 'Hello' in rect, got: {:?}",
result
);
}
#[test]
fn test_text_with_line_breaks_integration() {
let lib = Library::new();
let pdf = build_pdf_with_two_lines("Hello", "World", 12.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
assert!(
text_page.char_count() > 0,
"expected chars from two-line PDF"
);
let result = text_page.text_with_line_breaks();
assert!(
result.contains("\r\n"),
"expected \\r\\n line break between lines, got: {:?}",
result
);
}
#[test]
fn test_text_get_page_text_partial() {
let lib = Library::new();
let pdf = build_pdf_with_text("Hello", 12.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
let count = text_page.char_count();
assert!(count >= 3, "expected at least 3 chars, got {count}");
let partial = text_page.page_text(0, 3);
assert_eq!(
partial.chars().count(),
3,
"expected 3 chars from page_text(0, 3), got: {:?}",
partial
);
let mid = text_page.page_text(2, 2);
assert_eq!(
mid.chars().count(),
2,
"expected 2 chars from page_text(2, 2), got: {:?}",
mid
);
let full = text_page.all_page_text();
assert!(
full.starts_with(&partial),
"partial {:?} should be prefix of full text {:?}",
partial,
full
);
}
#[test]
fn test_text_by_run_integration() {
let lib = Library::new();
let pdf = build_pdf_with_text("Hello", 12.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
assert!(text_page.char_count() > 0, "expected extracted characters");
let run_id = text_page.text_object(0);
assert!(run_id.is_some(), "expected run ID for first character");
let run_text = text_page.text_by_object(run_id.unwrap());
assert!(
!run_text.is_empty(),
"expected non-empty text for run {:?}",
run_id
);
}
#[test]
fn test_text_char_index_conversions_integration() {
let lib = Library::new();
let pdf = build_pdf_with_text("Hello", 12.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
let count = text_page.char_count();
assert!(count > 0, "expected extracted characters");
let text_idx = text_page.text_index_from_char_index(0);
assert!(
text_idx.is_some(),
"expected Some(text_idx) for char_index 0"
);
let char_idx = text_page.char_index_from_text_index(text_idx.unwrap());
assert_eq!(
char_idx,
Some(0),
"expected round-trip to return char_index 0"
);
assert!(text_page.text_index_from_char_index(count).is_none());
assert!(text_page.char_index_from_text_index(count).is_none());
}
#[test]
fn test_text_colored_text_extraction() {
let lib = Library::new();
let pdf = build_pdf_with_colored_text("Hello", 1.0, 0.0, 0.0, 12.0);
let opts = OpenOptions {
parsing_mode: ParsingMode::Lenient,
..OpenOptions::default()
};
let doc = Document::open(&lib, pdf, &opts).unwrap();
let page = doc.page(0).unwrap();
let text_page = page.text().unwrap();
assert!(text_page.char_count() > 0, "expected extracted characters");
let color = text_page.fill_color_rgba(0);
assert!(
color.is_some(),
"expected fill color to be set, text: {:?}",
text_page.all_page_text()
);
if let Some((r, g, b, a)) = color {
assert_eq!(r, 255, "expected red=255, got {r}");
assert_eq!(g, 0, "expected green=0, got {g}");
assert_eq!(b, 0, "expected blue=0, got {b}");
assert_eq!(a, 255, "expected alpha=255, got {a}");
}
}
struct MemReader<'a>(&'a [u8]);
impl PdfReader for MemReader<'_> {
fn file_len(&self) -> u64 {
self.0.len() as u64
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> std::io::Result<usize> {
let src = self.0.get(offset as usize..).unwrap_or(&[]);
let n = buf.len().min(src.len());
buf[..n].copy_from_slice(&src[..n]);
Ok(n)
}
}
#[test]
fn test_open_custom_matches_open_page_count() {
let lib = Library::new();
let pdf = build_multi_page_pdf(3);
let opts = OpenOptions::default();
let direct = Document::open(&lib, pdf.clone(), &opts).unwrap();
let via_reader = Document::open_custom(&lib, MemReader(&pdf), &opts).unwrap();
assert_eq!(direct.page_count(), via_reader.page_count());
assert_eq!(via_reader.page_count(), 3);
}
#[test]
fn test_arc_open_custom_matches_open() {
let lib = ArcLibrary::new();
let pdf = build_multi_page_pdf(2);
let opts = OpenOptions::default();
let via_reader = ArcDocument::open_custom(&lib, MemReader(&pdf), &opts).unwrap();
assert_eq!(via_reader.page_count(), 2);
}