#![cfg(feature = "layout-detection")]
use crate::layout::{DetectionResult, LayoutClass};
use crate::pdf::structure::types::{LayoutHint, LayoutHintClass};
fn map_class(class: LayoutClass) -> LayoutHintClass {
match class {
LayoutClass::Title => LayoutHintClass::Title,
LayoutClass::SectionHeader => LayoutHintClass::SectionHeader,
LayoutClass::Code => LayoutHintClass::Code,
LayoutClass::Formula => LayoutHintClass::Formula,
LayoutClass::ListItem => LayoutHintClass::ListItem,
LayoutClass::Caption => LayoutHintClass::Caption,
LayoutClass::Footnote => LayoutHintClass::Footnote,
LayoutClass::PageHeader => LayoutHintClass::PageHeader,
LayoutClass::PageFooter => LayoutHintClass::PageFooter,
LayoutClass::Table => LayoutHintClass::Table,
LayoutClass::Picture | LayoutClass::Chart => LayoutHintClass::Picture,
LayoutClass::DocumentIndex => LayoutHintClass::DocumentIndex,
LayoutClass::Form => LayoutHintClass::Form,
LayoutClass::KeyValueRegion => LayoutHintClass::KeyValueRegion,
LayoutClass::Text => LayoutHintClass::Text,
_ => LayoutHintClass::Other,
}
}
#[cfg(feature = "layout-detection")]
pub(crate) fn pixel_detection_to_layout_hints_pdf_space(
detection: &DetectionResult,
image_width_px: u32,
image_height_px: u32,
page_width_pts: f32,
page_height_pts: f32,
) -> Vec<LayoutHint> {
let sx = page_width_pts / image_width_px.max(1) as f32;
let sy = page_height_pts / image_height_px.max(1) as f32;
detection
.detections
.iter()
.map(|det| LayoutHint {
class_name: map_class(det.class_name),
confidence: det.confidence,
left: det.bbox.x1 * sx,
right: det.bbox.x2 * sx,
top: page_height_pts - det.bbox.y1 * sy,
bottom: page_height_pts - det.bbox.y2 * sy,
})
.collect()
}
#[cfg(all(feature = "ocr", feature = "layout-detection"))]
pub(crate) fn detection_to_layout_hints_pixel_space(
detection: &DetectionResult,
page_height_px: f32,
) -> Vec<LayoutHint> {
detection
.detections
.iter()
.map(|det| LayoutHint {
class_name: map_class(det.class_name),
confidence: det.confidence,
left: det.bbox.x1,
right: det.bbox.x2,
top: page_height_px - det.bbox.y1,
bottom: page_height_px - det.bbox.y2,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::types::{BBox, LayoutDetection};
fn detection_at(class: LayoutClass, x1: f32, y1: f32, x2: f32, y2: f32) -> LayoutDetection {
LayoutDetection::new(class, 0.9, BBox::new(x1, y1, x2, y2))
}
#[test]
fn pixel_to_pdf_scales_and_flips_y_for_a4_at_150dpi() {
let det = DetectionResult::new(
1240,
1754,
vec![detection_at(LayoutClass::SectionHeader, 124.0, 100.0, 1116.0, 200.0)],
);
let hints = pixel_detection_to_layout_hints_pdf_space(&det, 1240, 1754, 595.0, 842.0);
assert_eq!(hints.len(), 1);
let h = &hints[0];
let sx = 595.0_f32 / 1240.0;
let sy = 842.0_f32 / 1754.0;
assert!((h.left - 124.0 * sx).abs() < 0.01, "left scaling: got {}", h.left);
assert!((h.right - 1116.0 * sx).abs() < 0.01, "right scaling: got {}", h.right);
let expected_top = 842.0 - 100.0 * sy;
let expected_bottom = 842.0 - 200.0 * sy;
assert!(
(h.top - expected_top).abs() < 0.01,
"top scale+flip: got {}, want {}",
h.top,
expected_top
);
assert!(
(h.bottom - expected_bottom).abs() < 0.01,
"bottom scale+flip: got {}, want {}",
h.bottom,
expected_bottom
);
assert!(
h.top > 700.0,
"heading at image y=100 should map to PDF y near top (≈ {}), got {}",
expected_top,
h.top
);
}
#[test]
fn pixel_to_pdf_no_scaling_when_dims_equal() {
let det = DetectionResult::new(
612,
792,
vec![detection_at(LayoutClass::Title, 50.0, 50.0, 562.0, 100.0)],
);
let hints = pixel_detection_to_layout_hints_pdf_space(&det, 612, 792, 612.0, 792.0);
assert_eq!(hints.len(), 1);
let h = &hints[0];
assert!((h.left - 50.0).abs() < 0.001);
assert!((h.right - 562.0).abs() < 0.001);
assert!((h.top - (792.0 - 50.0)).abs() < 0.001);
assert!((h.bottom - (792.0 - 100.0)).abs() < 0.001);
}
#[test]
fn class_mapping_preserves_heading_classes() {
let det = DetectionResult::new(
100,
100,
vec![
detection_at(LayoutClass::Title, 0.0, 0.0, 50.0, 10.0),
detection_at(LayoutClass::SectionHeader, 0.0, 20.0, 50.0, 30.0),
],
);
let hints = pixel_detection_to_layout_hints_pdf_space(&det, 100, 100, 100.0, 100.0);
assert_eq!(hints.len(), 2);
assert_eq!(hints[0].class_name, LayoutHintClass::Title);
assert_eq!(hints[1].class_name, LayoutHintClass::SectionHeader);
}
#[test]
fn class_mapping_preserves_docling_wrapper_classes() {
assert_eq!(map_class(LayoutClass::DocumentIndex), LayoutHintClass::DocumentIndex);
assert_eq!(map_class(LayoutClass::Form), LayoutHintClass::Form);
assert_eq!(map_class(LayoutClass::KeyValueRegion), LayoutHintClass::KeyValueRegion);
assert_eq!(map_class(LayoutClass::Table), LayoutHintClass::Table);
assert_eq!(map_class(LayoutClass::Picture), LayoutHintClass::Picture);
}
#[test]
fn pixel_to_pdf_zero_dim_does_not_panic() {
let det = DetectionResult::new(0, 0, vec![detection_at(LayoutClass::Text, 0.0, 0.0, 10.0, 10.0)]);
let _ = pixel_detection_to_layout_hints_pdf_space(&det, 0, 0, 595.0, 842.0);
}
}