#[derive(Debug, Clone, Copy)]
pub(crate) struct OrderBox {
pub min_x: f64,
pub max_x: f64,
pub min_y: f64,
pub max_y: f64,
pub font_size: f64,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct CutConfig {
pub horizontal_k: f64,
pub vertical_k: f64,
}
pub(crate) fn reading_order(boxes: &[OrderBox], cfg: &CutConfig) -> Vec<usize> {
let mut order = Vec::with_capacity(boxes.len());
let indices: Vec<usize> = (0..boxes.len()).collect();
cut_recursive(boxes, &indices, cfg, &mut order);
order
}
fn median_font_size(boxes: &[OrderBox], indices: &[usize]) -> Option<f64> {
if indices.is_empty() {
return None;
}
let mut sizes: Vec<f64> = indices.iter().map(|&i| boxes[i].font_size).collect();
sizes.sort_by(|a, b| a.total_cmp(b));
let median = sizes[sizes.len() / 2];
(median.is_finite() && median > 0.0).then_some(median)
}
fn cut_recursive(boxes: &[OrderBox], indices: &[usize], cfg: &CutConfig, order: &mut Vec<usize>) {
if indices.len() <= 1 {
order.extend_from_slice(indices);
return;
}
let Some(scale) = median_font_size(boxes, indices) else {
emit_leaf(indices, order);
return;
};
let column = find_column_gap(boxes, indices).filter(|c| c.ratio(scale) >= cfg.horizontal_k);
let section = find_section_gap(boxes, indices).filter(|c| c.ratio(scale) >= cfg.vertical_k);
let choose_column = match (&column, §ion) {
(Some(c), Some(s)) => c.ratio(scale) >= s.ratio(scale),
(Some(_), None) => true,
(None, Some(_)) => false,
(None, None) => {
emit_leaf(indices, order);
return;
}
};
if choose_column {
let (left, right) = partition_x(boxes, indices, column.unwrap().split);
if left.is_empty() || right.is_empty() {
emit_leaf(indices, order);
return;
}
cut_recursive(boxes, &left, cfg, order);
cut_recursive(boxes, &right, cfg, order);
} else {
let (top, bottom) = partition_y(boxes, indices, section.unwrap().split);
if top.is_empty() || bottom.is_empty() {
emit_leaf(indices, order);
return;
}
cut_recursive(boxes, &top, cfg, order);
cut_recursive(boxes, &bottom, cfg, order);
}
}
fn emit_leaf(indices: &[usize], order: &mut Vec<usize>) {
let mut leaf = indices.to_vec();
leaf.sort_unstable();
order.extend(leaf);
}
struct Gap {
width: f64,
split: f64,
}
impl Gap {
fn ratio(&self, scale: f64) -> f64 {
self.width / scale
}
}
fn find_column_gap(boxes: &[OrderBox], indices: &[usize]) -> Option<Gap> {
let mut edges: Vec<(f64, f64)> = indices
.iter()
.map(|&i| (boxes[i].min_x, boxes[i].max_x))
.collect();
edges.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut max_gap = 0.0f64;
let mut split = 0.0f64;
let mut max_right = edges[0].1;
for window in edges.windows(2) {
let next_left = window[1].0;
let gap = next_left - max_right;
if gap > max_gap {
max_gap = gap;
split = max_right + gap / 2.0;
}
max_right = max_right.max(window[1].1);
}
(max_gap > 0.0).then_some(Gap {
width: max_gap,
split,
})
}
fn find_section_gap(boxes: &[OrderBox], indices: &[usize]) -> Option<Gap> {
let mut edges: Vec<(f64, f64)> = indices
.iter()
.map(|&i| (boxes[i].min_y, boxes[i].max_y))
.collect();
edges.sort_by(|a, b| b.1.total_cmp(&a.1));
let mut max_gap = 0.0f64;
let mut split = 0.0f64;
let mut min_bottom = edges[0].0;
for window in edges.windows(2) {
let next_top = window[1].1;
let gap = min_bottom - next_top;
if gap > max_gap {
max_gap = gap;
split = next_top + gap / 2.0;
}
min_bottom = min_bottom.min(window[1].0);
}
(max_gap > 0.0).then_some(Gap {
width: max_gap,
split,
})
}
fn partition_x(boxes: &[OrderBox], indices: &[usize], split: f64) -> (Vec<usize>, Vec<usize>) {
indices
.iter()
.partition(|&&i| (boxes[i].min_x + boxes[i].max_x) / 2.0 < split)
}
fn partition_y(boxes: &[OrderBox], indices: &[usize], split: f64) -> (Vec<usize>, Vec<usize>) {
indices
.iter()
.partition(|&&i| (boxes[i].min_y + boxes[i].max_y) / 2.0 >= split)
}