use super::PdfDocument;
use crate::extractors::auto::ImageCodecClass;
use crate::extractors::images::decode_call_count;
fn build_single_page_pdf_with_raw_image(page_side: u32, image_side: u32, inset: u32) -> Vec<u8> {
let mut buf = Vec::<u8>::new();
buf.extend_from_slice(b"%PDF-1.4\n");
let mut offsets = Vec::new();
offsets.push(buf.len());
buf.extend_from_slice(b"1 0 obj\n<</Type /Catalog /Pages 2 0 R>>\nendobj\n");
offsets.push(buf.len());
buf.extend_from_slice(b"2 0 obj\n<</Type /Pages /Kids [3 0 R] /Count 1>>\nendobj\n");
offsets.push(buf.len());
buf.extend_from_slice(
format!(
"3 0 obj\n<</Type /Page /Parent 2 0 R /MediaBox [0 0 {page_side} {page_side}] \
/Resources <</XObject <</Im0 5 0 R>>>> /Contents 4 0 R>>\nendobj\n"
)
.as_bytes(),
);
let content = format!("q {image_side} 0 0 {image_side} {inset} {inset} cm /Im0 Do Q");
offsets.push(buf.len());
buf.extend_from_slice(format!("4 0 obj\n<</Length {}>>\nstream\n", content.len()).as_bytes());
buf.extend_from_slice(content.as_bytes());
buf.extend_from_slice(b"\nendstream\nendobj\n");
let pixel_count = (image_side as usize) * (image_side as usize) * 3;
offsets.push(buf.len());
buf.extend_from_slice(
format!(
"5 0 obj\n<</Type /XObject /Subtype /Image /Width {image_side} /Height {image_side} \
/ColorSpace /DeviceRGB /BitsPerComponent 8 /Length {pixel_count}>>\nstream\n"
)
.as_bytes(),
);
buf.extend((0..pixel_count).map(|i| ((i * 37 + i / 7) % 251) as u8));
buf.extend_from_slice(b"\nendstream\nendobj\n");
let xref_offset = buf.len();
let total_objs = 6;
buf.extend_from_slice(b"xref\n");
buf.extend_from_slice(format!("0 {total_objs}\n").as_bytes());
buf.extend_from_slice(b"0000000000 65535 f \n");
for off in &offsets {
buf.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
}
buf.extend_from_slice(format!("trailer\n<</Size {total_objs} /Root 1 0 R>>\n").as_bytes());
buf.extend_from_slice(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
buf
}
#[test]
fn classify_page_does_not_decode_embedded_images() {
let pdf = build_single_page_pdf_with_raw_image(200, 150, 25);
let doc = PdfDocument::from_bytes(pdf).expect("fixture must open");
let before_classify = decode_call_count();
let classification = doc.classify_page(0).expect("classify_page must succeed");
let after_classify = decode_call_count();
assert_eq!(
after_classify,
before_classify,
"classify_page must not decode any embedded image, got a decode-count delta of {}",
after_classify - before_classify
);
assert_eq!(classification.signals.codec, ImageCodecClass::Other);
let before_extract = decode_call_count();
let images = doc.extract_images(0).expect("extract_images must succeed");
let after_extract = decode_call_count();
assert_eq!(images.len(), 1, "fixture has exactly one embedded image");
assert_eq!(
after_extract - before_extract,
1,
"extract_images must decode exactly the one embedded image on the page \
(proves decode_call_count is wired to real decode work, not vacuously 0)"
);
}
#[test]
fn classify_page_image_area_ratio_matches_expected_bbox_intersection() {
let pdf = build_single_page_pdf_with_raw_image(200, 150, 25);
let doc = PdfDocument::from_bytes(pdf).expect("fixture must open");
let classification = doc.classify_page(0).expect("classify_page must succeed");
let expected_ratio = (150.0 * 150.0) / (200.0 * 200.0);
assert!(
(classification.signals.image_area_ratio - expected_ratio).abs() < 1e-5,
"expected image_area_ratio {expected_ratio}, got {}",
classification.signals.image_area_ratio
);
}
fn build_single_page_pdf_without_images(page_side: u32) -> Vec<u8> {
let mut buf = Vec::<u8>::new();
buf.extend_from_slice(b"%PDF-1.4\n");
let mut offsets = Vec::new();
offsets.push(buf.len());
buf.extend_from_slice(b"1 0 obj\n<</Type /Catalog /Pages 2 0 R>>\nendobj\n");
offsets.push(buf.len());
buf.extend_from_slice(b"2 0 obj\n<</Type /Pages /Kids [3 0 R] /Count 1>>\nendobj\n");
offsets.push(buf.len());
buf.extend_from_slice(
format!(
"3 0 obj\n<</Type /Page /Parent 2 0 R /MediaBox [0 0 {page_side} {page_side}] \
/Resources <<>> /Contents 4 0 R>>\nendobj\n"
)
.as_bytes(),
);
offsets.push(buf.len());
buf.extend_from_slice(b"4 0 obj\n<</Length 0>>\nstream\n\nendstream\nendobj\n");
let xref_offset = buf.len();
let total_objs = 5;
buf.extend_from_slice(b"xref\n");
buf.extend_from_slice(format!("0 {total_objs}\n").as_bytes());
buf.extend_from_slice(b"0000000000 65535 f \n");
for off in &offsets {
buf.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
}
buf.extend_from_slice(format!("trailer\n<</Size {total_objs} /Root 1 0 R>>\n").as_bytes());
buf.extend_from_slice(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
buf
}
#[test]
fn classify_page_with_no_images_reports_none_codec() {
let pdf = build_single_page_pdf_without_images(200);
let doc = PdfDocument::from_bytes(pdf).expect("fixture must open");
let before = decode_call_count();
let classification = doc.classify_page(0).expect("classify_page must succeed");
let after = decode_call_count();
assert_eq!(after, before, "a page with no images must still decode nothing");
assert_eq!(classification.signals.codec, ImageCodecClass::None);
assert_eq!(classification.signals.image_area_ratio, 0.0);
}