Merge PR #2577: M7: rewrap a paragraph at a slant at the width its rows have

This commit is contained in:
aiongg
2026-10-16 14:02:18 +00:00
3 changed files with 50 additions and 10 deletions
+25
View File
@@ -3972,6 +3972,31 @@ fn turned_paragraph_keeps_its_line_breaks_when_a_word_is_added() {
assert!(text::rewrite_block(&mut doc, 0, 0, Some("Only line"), &style).is_err());
}
/// A paragraph drawn at a slant rewraps at the width its rows have along their baseline, as the
/// same paragraph upright does. Its upright bounding box turned into the baseline's frame is wider
/// by the rows' height times |sin 2θ|, which let a word that doesn't fit stay on its row.
#[test]
fn a_paragraph_at_a_slant_rewraps_at_the_width_it_has_upright() {
let rows = ["Rotated paragraph, first row of text", "that continues on a second row and", "ends on the third row here."];
let draft = "Rotated paragraph, first row of text that continues on a second row and now carries a few extra words \
before it ends on the third row here.";
let rewrapped = |degrees: f64| {
let (sin, cos) = degrees.to_radians().sin_cos();
let shown = rows.iter().map(|r| format!("({r}) Tj")).collect::<Vec<_>>().join(" T* ");
let mut doc = text_page(&format!("BT /F1 12 Tf 14.4 TL {cos:.6} {sin:.6} {:.6} {cos:.6} 300 400 Tm {shown} ET", -sin));
let block = text::text_blocks(&doc, 0).unwrap().remove(0);
assert_eq!(block.editor_text, rows.join("\n"), "{degrees}°");
text::rewrite_block(&mut doc, 0, 0, Some(draft), &text::BlockStyle::default()).unwrap();
text::text_lines(&reopen(&doc), 0).unwrap().into_iter().map(|l| l.text).collect::<Vec<_>>()
};
let upright = rewrapped(0.0);
// The widest source row is 189.44 pt; "now carries a few extra words before" is 196.07 pt.
assert_eq!(upright[2], "now carries a few extra words", "{upright:?}");
for degrees in [30.0, 45.0, 60.0, 90.0, 135.0, 210.0, 330.0] {
assert_eq!(rewrapped(degrees), upright, "{degrees}°");
}
}
/// A paragraph in a Chrome-style subset font: `/W` leaves out the glyphs of the `/DW` width.
/// Adding a word keeps every row as it was and shows the word in the page's own font (#1443).
#[test]
+25 -6
View File
@@ -50,6 +50,10 @@ pub struct TextLine {
pub visual_order: bool,
/// The Japanese or Hebrew fallback face the font subsets, when the line is in one (see [`same_font`]).
face: Option<String>,
/// Its box along and across its baseline, `[along0, across0, along1, across1]` (see
/// [`TextLine::frame_rect`]). Upright text has `rect`; turned text has its own box, which is
/// narrower than `rect` turned into the same frame.
frame: [f64; 4],
/// The content stream piece the line starts in, and its operators (indices into the page's
/// streams parsed joined).
stream: usize,
@@ -470,12 +474,7 @@ impl TextLine {
/// Its box in the same frame, `[along0, across0, along1, across1]` (for upright text, `rect`).
fn frame_rect(&self) -> [f64; 4] {
let dir = self.direction();
let r = self.rect;
[(r[0], r[1]), (r[2], r[1]), (r[0], r[3]), (r[2], r[3])]
.into_iter()
.map(|p| to_frame(dir, p))
.fold([f64::MAX, f64::MAX, f64::MIN, f64::MIN], |b, p| [b[0].min(p.0), b[1].min(p.1), b[2].max(p.0), b[3].max(p.1)])
self.frame
}
}
@@ -493,6 +492,11 @@ fn to_frame(dir: (f64, f64), p: (f64, f64)) -> (f64, f64) {
(p.0 * dir.0 + p.1 * dir.1, p.1 * dir.0 - p.0 * dir.1)
}
/// The box covering both `[x0, y0, x1, y1]` boxes.
fn union(a: [f64; 4], b: [f64; 4]) -> [f64; 4] {
[a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]
}
/// What replacing a line did.
#[derive(Clone, Debug, PartialEq)]
pub struct LineEdit {
@@ -639,6 +643,8 @@ struct Shown {
bt_state: Ts,
text: String,
rect: [f64; 4],
/// Its box along and across its baseline (see [`TextLine::frame`]).
frame: [f64; 4],
/// The boxes of its words (see [`TextLine::ink`]).
ink: Vec<[f64; 4]>,
baseline: f64,
@@ -773,6 +779,7 @@ fn interpret(
first.rect[2].max(shown.rect[2]),
first.rect[3].max(shown.rect[3]),
];
first.frame = union(first.frame, shown.frame);
first.ink.extend(shown.ink);
}
first.extra_ops.extend([open, i]);
@@ -936,6 +943,13 @@ fn interpret(
let rect = corners
.iter()
.fold([f64::MAX, f64::MAX, f64::MIN, f64::MIN], |b, p| [b[0].min(p.0), b[1].min(p.1), b[2].max(p.0), b[3].max(p.1)]);
// The same corners along and across the baseline: the run's own box, not its
// upright bounding box turned (which is wider by the box's height on a slant).
let dir = direction(&trm0);
let frame = corners
.iter()
.map(|&p| to_frame(dir, p))
.fold([f64::MAX, f64::MAX, f64::MIN, f64::MIN], |b, p| [b[0].min(p.0), b[1].min(p.1), b[2].max(p.0), b[3].max(p.1)]);
let ink = words
.iter()
.map(|&(a, b)| {
@@ -963,6 +977,7 @@ fn interpret(
bt_state: at_bt.1.clone(),
text,
rect,
frame,
ink,
baseline: start.1,
start_x: start.0,
@@ -1040,6 +1055,7 @@ pub fn text_lines(doc: &Document, page: usize) -> Result<Vec<TextLine>, EditErro
}
l.text.push_str(&s.text);
l.rect = [l.rect[0].min(s.rect[0]), l.rect[1].min(s.rect[1]), l.rect[2].max(s.rect[2]), l.rect[3].max(s.rect[3])];
l.frame = union(l.frame, s.frame);
l.ink.extend(s.ink.iter().copied());
l.ops.push(s.op);
l.ops.extend(s.extra_ops.iter().copied());
@@ -1051,6 +1067,7 @@ pub fn text_lines(doc: &Document, page: usize) -> Result<Vec<TextLine>, EditErro
lines.push(TextLine {
text: s.text.clone(),
rect: s.rect,
frame: s.frame,
ink: s.ink.clone(),
font: String::from_utf8_lossy(&s.font).into_owned(),
base_font: s.base_font.clone(),
@@ -1128,6 +1145,7 @@ fn add_shown(lines: &mut Vec<TextLine>, last: &mut Option<Pen>, s: Shown, stream
}
l.text.push_str(&s.text);
l.rect = [l.rect[0].min(s.rect[0]), l.rect[1].min(s.rect[1]), l.rect[2].max(s.rect[2]), l.rect[3].max(s.rect[3])];
l.frame = union(l.frame, s.frame);
l.ink.extend(s.ink.iter().copied());
l.ops.push(s.op);
l.ops.extend(s.extra_ops.iter().copied());
@@ -1138,6 +1156,7 @@ fn add_shown(lines: &mut Vec<TextLine>, last: &mut Option<Pen>, s: Shown, stream
lines.push(TextLine {
text: s.text.clone(),
rect: s.rect,
frame: s.frame,
ink: s.ink.clone(),
font: String::from_utf8_lossy(&s.font).into_owned(),
base_font: s.base_font.clone(),
-4
View File
@@ -17,10 +17,6 @@ fn same_plane(a: &TextLine, b: &TextLine) -> bool {
})
}
fn union(a: [f64; 4], b: [f64; 4]) -> [f64; 4] {
[a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]
}
fn row_rect(row: &[usize], lines: &[TextLine]) -> Option<[f64; 4]> {
row.iter().filter_map(|i| lines.get(*i)).map(|l| l.rect).reduce(union)
}