use super::content::Glyph;
use super::lines::{self, Line};
const MIN_GUTTER: f32 = 6.0;
const GUTTER_EMS: f32 = 0.91;
const BAND_GAP_EMS: f32 = 0.80;
const MIN_LINES_PER_COLUMN: usize = 2;
const FULL_LINE_SLACK: f32 = 0.04;
const JUSTIFIED_SHARE: f32 = 0.5;
const MAX_DEPTH: usize = 10;
pub(crate) type Region = Vec<Line>;
pub(crate) fn split(glyphs: &[Glyph]) -> Vec<Region> {
let mut turns: Vec<u8> = glyphs.iter().map(|g| g.turn).collect();
turns.sort_unstable();
turns.dedup();
let mut out = Vec::new();
for turn in turns {
let frame: Vec<&Glyph> = glyphs.iter().filter(|g| g.turn == turn).collect();
cut(&frame, 0, true, &mut out);
}
out
}
fn cut(glyphs: &[&Glyph], depth: usize, horizontal_first: bool, out: &mut Vec<Region>) {
if glyphs.is_empty() {
return;
}
if depth >= MAX_DEPTH {
emit(glyphs, out);
return;
}
let order: [bool; 2] = if horizontal_first { [true, false] } else { [false, true] };
for horizontal in order {
let parts = if horizontal { bands(glyphs) } else { columns(glyphs) };
if parts.len() > 1 {
for part in parts {
cut(&part, depth + 1, !horizontal, out);
}
return;
}
}
emit(glyphs, out);
}
fn emit(glyphs: &[&Glyph], out: &mut Vec<Region>) {
let owned: Vec<Glyph> = glyphs.iter().map(|g| (*g).clone()).collect();
let region = lines::build(&owned);
if !region.is_empty() {
out.push(region);
}
}
fn bands<'a>(glyphs: &[&'a Glyph]) -> Vec<Vec<&'a Glyph>> {
let em = median_size(glyphs);
let extents: Vec<(f32, f32)> = glyphs.iter().map(|g| (g.y - 0.22 * g.size, g.y + 0.78 * g.size)).collect();
let gaps = empty_runs(&extents, (BAND_GAP_EMS * em).max(1.0));
if gaps.is_empty() {
return vec![glyphs.to_vec()];
}
let mut parts: Vec<Vec<&Glyph>> = vec![Vec::new(); gaps.len() + 1];
for g in glyphs {
let index = gaps.iter().filter(|edge| g.y < **edge).count();
parts[index].push(g);
}
parts
}
fn columns<'a>(glyphs: &[&'a Glyph]) -> Vec<Vec<&'a Glyph>> {
let single = || vec![glyphs.to_vec()];
let em = median_size(glyphs);
let extents: Vec<(f32, f32)> = glyphs.iter().map(|g| (g.x, g.x + g.width)).collect();
let edges = empty_runs(&extents, MIN_GUTTER.max(GUTTER_EMS * em));
if edges.is_empty() {
return single();
}
let mut parts: Vec<Vec<&Glyph>> = vec![Vec::new(); edges.len() + 1];
for g in glyphs {
let index = edges.iter().filter(|edge| g.x >= **edge).count();
parts[index].push(g);
}
for part in &parts {
if !reads_as_a_column(part) {
return single();
}
}
parts
}
fn reads_as_a_column(glyphs: &[&Glyph]) -> bool {
let owned: Vec<Glyph> = glyphs.iter().map(|g| (*g).clone()).collect();
let lines = lines::build(&owned);
if lines.len() < MIN_LINES_PER_COLUMN {
return false;
}
let measure = lines.iter().map(|l| l.right).fold(f32::NEG_INFINITY, f32::max);
let left = lines.iter().map(|l| l.left).fold(f32::INFINITY, f32::min);
let width = measure - left;
if width <= 0.0 {
return false;
}
let full = lines.iter().filter(|l| l.right >= measure - width * FULL_LINE_SLACK).count();
full as f32 >= lines.len() as f32 * JUSTIFIED_SHARE
}
fn empty_runs(extents: &[(f32, f32)], min_width: f32) -> Vec<f32> {
let mut spans: Vec<(f32, f32)> =
extents.iter().copied().filter(|(start, end)| start.is_finite() && end >= start).collect();
if spans.is_empty() {
return Vec::new();
}
spans.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut out = Vec::new();
let mut reach = spans[0].1;
for &(start, end) in &spans[1..] {
if start - reach >= min_width {
out.push((reach + start) / 2.0);
}
reach = reach.max(end);
}
out
}
fn median_size(glyphs: &[&Glyph]) -> f32 {
let mut sizes: Vec<f32> = glyphs.iter().map(|g| g.size).filter(|s| *s > 0.0).collect();
if sizes.is_empty() {
return 10.0;
}
sizes.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
sizes[sizes.len() / 2]
}
#[cfg(test)]
mod tests {
use super::*;
fn run(text: &str, x: f32, y: f32) -> Vec<Glyph> {
text.chars()
.enumerate()
.map(|(i, c)| Glyph {
text: c.to_string(),
x: x + i as f32 * 5.0,
y,
width: 5.0,
size: 10.0,
turn: 0,
bold: false,
italic: false,
link: None,
})
.collect()
}
fn texts(regions: Vec<Region>) -> Vec<String> {
regions.into_iter().flatten().map(|l| l.text).collect()
}
fn two_columns() -> Vec<Glyph> {
let mut glyphs = Vec::new();
for i in 0..6 {
let y = 600.0 - i as f32 * 12.0;
glyphs.extend(run(&format!("left{i} filling out the whole measure"), 40.0, y));
glyphs.extend(run(&format!("right{i} filling out the whole measur"), 320.0, y));
}
glyphs
}
#[test]
fn a_two_column_page_is_read_down_then_across() {
let out = texts(split(&two_columns()));
assert_eq!(out.len(), 12);
assert!(out[..6].iter().all(|t| t.starts_with("left")), "{out:?}");
assert!(out[6..].iter().all(|t| t.starts_with("right")), "{out:?}");
}
#[test]
fn a_single_column_page_keeps_its_order() {
let mut glyphs = Vec::new();
for i in 0..6 {
glyphs.extend(run(&format!("line{i} of a single column of prose"), 40.0, 600.0 - i as f32 * 12.0));
}
let out = texts(split(&glyphs));
assert_eq!(out.len(), 6);
assert!(out[0].starts_with("line0") && out[5].starts_with("line5"), "{out:?}");
}
#[test]
fn a_ragged_grid_is_not_cut_into_columns() {
let mut glyphs = Vec::new();
for (i, (a, b)) in [("Recurrent", "O(n)"), ("Convolutional", "O(k)"), ("Self-Attention", "O(1)")]
.iter()
.enumerate()
{
let y = 600.0 - i as f32 * 12.0;
glyphs.extend(run(a, 40.0, y));
glyphs.extend(run(b, 300.0, y));
}
let out = texts(split(&glyphs));
assert_eq!(out.len(), 3);
assert!(out[0].starts_with("Recurrent") && out[0].ends_with("O(n)"), "{out:?}");
}
#[test]
fn a_full_width_line_ends_the_columns_above_it() {
let mut glyphs = two_columns();
glyphs.extend(run("A FULL WIDTH HEADING ACROSS THE ENTIRE PAGE WIDTH", 40.0, 700.0));
let out = texts(split(&glyphs));
assert!(out[0].starts_with("A FULL WIDTH"), "{out:?}");
}
#[test]
fn sideways_text_is_kept_out_of_the_upright_flow() {
let mut glyphs = Vec::new();
for i in 0..4 {
glyphs.extend(run(&format!("body{i} of an ordinary upright page"), 40.0, 600.0 - i as f32 * 12.0));
}
let mut stamp = run("arXiv:1706.03762", 0.0, 300.0);
for g in &mut stamp {
g.turn = 1;
}
glyphs.extend(stamp);
let out = texts(split(&glyphs));
assert_eq!(out.last().unwrap(), "arXiv:1706.03762");
}
}