use crate::layout::text_block::TextSpan;
#[derive(Debug, Clone)]
pub(crate) struct Line {
pub spans: Vec<TextSpan>,
pub x_pt: f32,
pub y_pt: f32,
pub height_pt: f32,
pub width_pt: f32,
}
const LINE_Y_TOLERANCE_PT: f32 = 2.0;
pub(crate) fn group_spans_into_lines(spans: Vec<TextSpan>) -> Vec<Line> {
if spans.is_empty() {
return Vec::new();
}
let mut sorted = spans;
sorted.sort_by(|a, b| {
b.bbox
.y
.partial_cmp(&a.bbox.y)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
a.bbox
.x
.partial_cmp(&b.bbox.x)
.unwrap_or(std::cmp::Ordering::Equal)
})
});
let mut lines: Vec<Line> = Vec::new();
for span in sorted {
let trimmed = span.text.trim_matches('\u{0000}');
if trimmed.is_empty() {
continue;
}
let placed = lines.last_mut().and_then(|line| {
let baseline = line.y_pt;
if (span.bbox.y - baseline).abs() > LINE_Y_TOLERANCE_PT {
return None;
}
let line_max_size = line
.spans
.iter()
.map(|s| s.font_size)
.fold(0.0_f32, f32::max);
let line_min_size = line
.spans
.iter()
.map(|s| s.font_size)
.fold(f32::INFINITY, f32::min);
let combined_max = line_max_size.max(span.font_size);
let combined_min = line_min_size.min(span.font_size).max(0.1);
if combined_max / combined_min > 2.0 {
return None;
}
let line_right = line.x_pt + line.width_pt;
let gap = span.bbox.x - line_right;
let max_gap = combined_max * 4.0;
if gap > max_gap {
return None;
}
Some(line)
});
if let Some(line) = placed {
let span_right = span.bbox.x + span.bbox.width;
let line_right = line.x_pt + line.width_pt;
let new_left = line.x_pt.min(span.bbox.x);
let new_right = line_right.max(span_right);
line.x_pt = new_left;
line.width_pt = (new_right - new_left).max(0.0);
line.height_pt = line.height_pt.max(span.bbox.height);
line.spans.push(span);
} else {
let line = Line {
x_pt: span.bbox.x,
y_pt: span.bbox.y,
height_pt: span.bbox.height,
width_pt: span.bbox.width,
spans: vec![span],
};
lines.push(line);
}
}
for line in &mut lines {
line.spans.sort_by(|a, b| {
a.bbox
.x
.partial_cmp(&b.bbox.x)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
lines
}