mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 06:28:49 -06:00
M7: a lone row aligns across the column under it, not the page's busiest
alignment_frame found the column by the left edge most of the page's other rows share. On a page of two columns, a title over the column with fewer rows took the other column's edge, failed to fit in it, and was aligned in its own box. Only rows reaching across the title now count. Tests: the two-column case; a two-colour title (the styled-region path) is framed too; a justified paragraph sent with its rows split by line breaks, as the editor sends it, still fills every row but the last.
This commit is contained in:
@@ -6742,3 +6742,68 @@ fn a_lone_row_aligns_across_the_column_it_sits_in() {
|
||||
align(&mut doc, added::Align::Right);
|
||||
assert!((title(&doc)[2] - column[1]).abs() < 1.0, "right: on the column's right edge, {:?} vs {column:?}", title(&doc));
|
||||
}
|
||||
|
||||
/// An edited paragraph's text has a line break between its source rows (`editor_text`), as the
|
||||
/// paragraph editor sends it. Those are the rows of one paragraph, not paragraph ends: justified
|
||||
/// in a font of its own (the single-style rewrite), every row but the last still fills the width.
|
||||
#[test]
|
||||
fn a_justified_paragraph_from_the_editor_fills_its_rows() {
|
||||
let mut doc =
|
||||
text_page("BT /F1 10 Tf 12 TL 100 700 Td (One two three four five six seven) Tj T* (eight nine ten eleven twelve) Tj T* (end) Tj ET");
|
||||
let block = text::text_blocks(&doc, 0).unwrap()[0].clone();
|
||||
assert_eq!(block.editor_text.matches('\n').count(), 2);
|
||||
let style = text::BlockStyle { align: Some(added::Align::Justify), family: Some((Family::Helvetica, true, false)), ..Default::default() };
|
||||
text::rewrite_block(&mut doc, 0, 0, Some(&block.editor_text), &style).unwrap();
|
||||
let lines = text::text_lines(&doc, 0).unwrap();
|
||||
let row = |t: &str| lines.iter().find(|l| l.text.starts_with(t)).unwrap().rect;
|
||||
let right = row("One")[2];
|
||||
assert!((row("eight")[2] - right).abs() < 1.0, "the second row is stretched: {:?} vs {right}", row("eight"));
|
||||
assert!(row("end")[2] < right - 20.0, "the last row isn't");
|
||||
}
|
||||
|
||||
/// The same on a page of two columns, where the other column has more rows: a title over the
|
||||
/// first column is aligned across the first column, not its own box.
|
||||
#[test]
|
||||
fn a_lone_row_aligns_across_its_own_column_of_two() {
|
||||
let mut doc = text_page(
|
||||
"BT /F1 18 Tf 1 0 0 1 120 700 Tm (Title) Tj ET \
|
||||
BT /F1 10 Tf 12 TL 50 660 Td (The first column runs to here.) Tj T* (A second row of it.) Tj ET \
|
||||
BT /F1 10 Tf 12 TL 320 660 Td (The second column has more rows.) Tj T* (One.) Tj T* (Two.) Tj T* (Three.) Tj T* (Four.) Tj ET",
|
||||
);
|
||||
let blocks = text::text_blocks(&doc, 0).unwrap();
|
||||
let column = blocks.iter().find(|b| b.text.starts_with("The first")).unwrap().rect;
|
||||
let index = blocks.iter().position(|b| b.text == "Title").unwrap();
|
||||
let style = text::BlockStyle { aligns: vec![Some(added::Align::Left)], ..Default::default() };
|
||||
text::rewrite_block(&mut doc, 0, index, Some("Title"), &style).unwrap();
|
||||
let title = text::text_lines(&doc, 0).unwrap().into_iter().find(|l| l.text == "Title").unwrap().rect;
|
||||
assert!((title[0] - column[0]).abs() < 0.5, "left: on the first column's edge, {title:?} vs {column:?}");
|
||||
}
|
||||
|
||||
/// A one-row title in two colours is rewritten by the styled-region path, which is given the
|
||||
/// same column frame: centred twice, it stays centred in the column instead of moving right.
|
||||
#[test]
|
||||
fn a_lone_row_of_two_styles_aligns_across_its_column() {
|
||||
let mut doc = text_page(
|
||||
"BT /F1 18 Tf 1 0 0 1 250 700 Tm (Big ) Tj 1 0 0 rg (Title) Tj ET \
|
||||
BT 0 g /F1 10 Tf 12 TL 72 650 Td (The body of the page runs across the whole column here.) Tj T* (It has a second row that is a little shorter.) Tj ET",
|
||||
);
|
||||
let blocks = text::text_blocks(&doc, 0).unwrap();
|
||||
assert_eq!(blocks[0].text, "Big Title");
|
||||
let column = [blocks[1].rect[0], blocks[1].rect[2]];
|
||||
let title = |doc: &Document| {
|
||||
let lines = text::text_lines(doc, 0).unwrap();
|
||||
let block = text::text_blocks(doc, 0).unwrap().into_iter().find(|b| b.text == "Big Title").unwrap();
|
||||
block.lines.iter().map(|i| lines[*i].rect).reduce(|a, b| [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]).unwrap()
|
||||
};
|
||||
let centre = |doc: &mut Document| {
|
||||
let blocks = text::text_blocks(doc, 0).unwrap();
|
||||
let (index, block) = blocks.iter().enumerate().find(|(_, b)| b.text == "Big Title").unwrap();
|
||||
let style = text::BlockStyle { aligns: vec![Some(added::Align::Center)], ..Default::default() };
|
||||
text::rewrite_block(doc, 0, index, Some(&block.editor_text), &style).unwrap();
|
||||
};
|
||||
centre(&mut doc);
|
||||
let centred = title(&doc);
|
||||
assert!(((centred[0] + centred[2]) / 2.0 - (column[0] + column[1]) / 2.0).abs() < 1.0, "centred in the column: {centred:?} vs {column:?}");
|
||||
centre(&mut doc);
|
||||
assert!((title(&doc)[0] - centred[0]).abs() < 0.5, "centring again leaves it where it is: {:?} vs {centred:?}", title(&doc));
|
||||
}
|
||||
|
||||
@@ -30,11 +30,12 @@ fn frame_row_rect(row: &[usize], lines: &[TextLine]) -> Option<[f64; 4]> {
|
||||
|
||||
/// The left edge the rest of the page's text sits on, ignoring the rows of `block`. A single row
|
||||
/// that does not share it is not merely an indented or full-width left-aligned line, so its own
|
||||
/// centre is the only remaining explanation for where it sits.
|
||||
fn neighbour_left(lines: &[TextLine], block: &TextBlock) -> Option<f64> {
|
||||
/// centre is the only remaining explanation for where it sits. With `over`, only rows reaching
|
||||
/// across that span count: the edge of the column under it, however many rows another column has.
|
||||
fn neighbour_left(lines: &[TextLine], block: &TextBlock, over: Option<[f64; 2]>) -> Option<f64> {
|
||||
let mut edges: HashMap<i64, usize> = HashMap::new();
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
if block.lines.contains(&i) {
|
||||
if block.lines.contains(&i) || over.is_some_and(|[left, right]| line.rect[2] <= left || line.rect[0] >= right) {
|
||||
continue;
|
||||
}
|
||||
// Quarter-point buckets: equal edges found through different runs still agree.
|
||||
@@ -45,8 +46,8 @@ fn neighbour_left(lines: &[TextLine], block: &TextBlock) -> Option<f64> {
|
||||
}
|
||||
|
||||
/// Where an alignment picked for a one-row paragraph is measured, as `[left, right]`: the column of
|
||||
/// text it sits in (the left edge the rest of the page's text uses, to the furthest right those
|
||||
/// rows reach) when that column holds it, otherwise its own box. A lone row's own box is no frame
|
||||
/// text it sits in (the left edge the text under or over it uses, to the furthest right those rows
|
||||
/// reach) when that column holds it, otherwise its own box. A lone row's own box is no frame
|
||||
/// to align in: left-aligning a title centred on the page left it where it was, and since a single
|
||||
/// row grows to the page's margin, centring it again moved it right each time. `None` when no
|
||||
/// alignment is asked for, or when the paragraph is several rows, turned, or given a width or a
|
||||
@@ -58,7 +59,7 @@ pub(super) fn alignment_frame(lines: &[TextLine], block: &TextBlock, style: &Blo
|
||||
}
|
||||
let own = [block.rect[0], block.rect[2]];
|
||||
let tolerance = block.size.max(1.0) * 0.5;
|
||||
let Some(left) = neighbour_left(lines, block) else { return Some(own) };
|
||||
let Some(left) = neighbour_left(lines, block, Some(own)) else { return Some(own) };
|
||||
let right = lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -85,9 +86,9 @@ pub(super) fn is_centred(block: &TextBlock, lines: &[TextLine], page_centre: f64
|
||||
return left_span > size * 0.5
|
||||
&& block.rows.iter().all(|row| row_rect(row, lines).is_some_and(|r| ((r[0] + r[2]) / 2.0 - centre).abs() < size));
|
||||
}
|
||||
lefts
|
||||
.first()
|
||||
.is_some_and(|left| (centre - page_centre).abs() < size && neighbour_left(lines, block).is_none_or(|edge| (*left - edge).abs() > size * 0.5))
|
||||
lefts.first().is_some_and(|left| {
|
||||
(centre - page_centre).abs() < size && neighbour_left(lines, block, None).is_none_or(|edge| (*left - edge).abs() > size * 0.5)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn group_blocks(lines: &[TextLine]) -> Vec<TextBlock> {
|
||||
|
||||
Reference in New Issue
Block a user