use super::heading_lines::{heading_line_features, page_rows, WIDTH};
use super::BlockVectors;
pub const PICTURE_FEATURES: [&str; 4] = [
"picture_distance_em",
"picture_above",
"picture_below",
"picture_under",
];
const NEAR_EM: f64 = 3.0;
pub const CAPTION_WIDTH: usize = WIDTH + PICTURE_FEATURES.len();
#[doc(hidden)]
pub fn caption_line_features() -> Vec<String> {
let mut out = heading_line_features();
out.extend(PICTURE_FEATURES.iter().map(|s| s.to_string()));
out
}
pub fn picture_features(bbox: [f64; 4], size: f64, images: &[[f64; 4]]) -> [f64; 4] {
if images.is_empty() {
return [f64::NAN, 0.0, 0.0, 0.0];
}
let [x0, y0, x1, y1] = bbox;
let em = if size > 0.0 { size } else { 1.0 };
let (mut best, mut above, mut below, mut under) = (f64::INFINITY, 0.0, 0.0, 0.0);
for &[a0, b0, a1, b1] in images {
let dx = (a0 - x1).max(x0 - a1).max(0.0);
let dy = (b0 - y1).max(y0 - b1).max(0.0);
best = best.min((dx * dx + dy * dy).sqrt() / em);
let across = x1.min(a1) - x0.max(a0) > 0.0;
if across && (0.0..=NEAR_EM * em).contains(&(b0 - y1)) {
above = 1.0;
}
if across && (0.0..=NEAR_EM * em).contains(&(y0 - b1)) {
below = 1.0;
}
if dx == 0.0 && dy == 0.0 {
under = 1.0;
}
}
[best, above, below, under]
}
pub fn page_caption_rows(
page: &[BlockVectors],
images: &[[f64; 4]],
) -> Vec<Vec<[f64; CAPTION_WIDTH]>> {
page_caption_rows_on_rows(page, images, &page_rows(page))
}
pub fn page_caption_rows_on_rows(
page: &[BlockVectors],
images: &[[f64; 4]],
rows: &[Vec<[f64; WIDTH]>],
) -> Vec<Vec<[f64; CAPTION_WIDTH]>> {
rows.iter()
.zip(page)
.map(|(rows, v)| {
rows.iter()
.zip(&v.line_boxes)
.map(|(row, &(bbox, size))| {
let mut out = [f64::NAN; CAPTION_WIDTH];
out[..WIDTH].copy_from_slice(row);
out[WIDTH..].copy_from_slice(&picture_features(bbox, size, images));
out
})
.collect()
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_caption_pages_equal(
left: &[Vec<[f64; CAPTION_WIDTH]>],
right: &[Vec<[f64; CAPTION_WIDTH]>],
) {
assert_eq!(left.len(), right.len(), "page count differs");
for (lines, want) in left.iter().zip(right.iter()) {
assert_eq!(lines.len(), want.len(), "row count differs");
for (row, expected) in lines.iter().zip(want.iter()) {
for (got, ask) in row.iter().zip(expected.iter()) {
assert!(
got == ask || (got.is_nan() && ask.is_nan()),
"rows differ: {row:?} vs {expected:?}"
);
}
}
}
}
#[test]
fn caption_rows_over_shared_rows_match_page_caption_rows() {
let block = |kind: &'static str, lines: usize| BlockVectors {
page: 0,
kind,
block: [1.0; 30],
lines: vec![[2.0; 23]; lines],
line_boxes: (0..lines)
.map(|k| {
(
[
72.0,
700.0 - k as f64 * 12.0,
200.0,
710.0 - k as f64 * 12.0,
],
10.0,
)
})
.collect(),
style: Vec::new(),
};
let page = [block("paragraph", 2), block("heading", 1)];
let images: [[f64; 4]; 1] = [[72.0, 500.0, 272.0, 600.0]];
let rows = page_rows(&page);
assert_caption_pages_equal(
&page_caption_rows_on_rows(&page, &images, &rows),
&page_caption_rows(&page, &images),
);
assert_caption_pages_equal(
&page_caption_rows_on_rows(&page, &[], &rows),
&page_caption_rows(&page, &[]),
);
}
#[test]
fn a_line_under_a_picture_has_the_picture_above_it() {
let got = picture_features(
[72.0, 485.0, 200.0, 495.0],
10.0,
&[[72.0, 500.0, 272.0, 600.0]],
);
assert!((got[0] - 0.5).abs() < 1e-9, "{got:?}");
assert_eq!(got[1..], [1.0, 0.0, 0.0], "{got:?}");
}
#[test]
fn a_page_with_no_picture_says_so() {
let got = picture_features([0.0, 0.0, 1.0, 1.0], 10.0, &[]);
assert!(got[0].is_nan());
assert_eq!(got[1..], [0.0, 0.0, 0.0]);
}
#[test]
fn the_names_are_as_wide_as_a_row() {
assert_eq!(caption_line_features().len(), CAPTION_WIDTH);
}
}