mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 11:37:59 -06:00
fix(xfa): break a long word in linear time
Text layout broke a word wider than its line by measuring the whole growing piece after every character, and counting its characters again each time. A long unbroken word in a wide box with a small font made that quadratic. The piece's advance is kept as a running sum of character widths instead.
This commit is contained in:
@@ -2657,3 +2657,16 @@ fn trailing_spaces_do_not_shift_centered_or_right_aligned_text() {
|
||||
assert!((left_edge("center", "TITLE ") - (36.0 + (w - ink) / 2.0)).abs() < 1e-6);
|
||||
assert!((left_edge("right", "TITLE ") - (36.0 + w - ink)).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// A word longer than its line is broken by character in work linear in its length.
|
||||
#[test]
|
||||
fn a_long_word_breaks_in_linear_work() {
|
||||
let face = text::Face::of(&Font { typeface: "Arial".into(), size: 1.0, ..Font::default() });
|
||||
let word = "i".repeat(20_000);
|
||||
text::MEASURED.with(|m| m.set(0));
|
||||
let b = text::layout(&text::plain(&word), Rect::new(0.0, 0.0, 1000.0, 0.0), &Para::default(), &face, &|_| String::new());
|
||||
let measured = text::MEASURED.with(|m| m.get());
|
||||
assert!(b.spans.len() > 2, "{} lines", b.spans.len());
|
||||
assert_eq!(b.spans.iter().map(|s| s.text.len()).sum::<usize>(), 20_000);
|
||||
assert!(measured < 200_000, "{measured} bytes measured for a 20000-byte word");
|
||||
}
|
||||
|
||||
+16
-2
@@ -123,6 +123,8 @@ impl Face {
|
||||
|
||||
/// Advance of `s` at this face's size.
|
||||
pub fn width(&self, s: &str) -> f64 {
|
||||
#[cfg(test)]
|
||||
MEASURED.with(|m| m.set(m.get().saturating_add(s.len() as u64)));
|
||||
let designed = match self.family {
|
||||
Family::Courier => s.chars().count() as f64 * 0.6 * self.size,
|
||||
Family::Helvetica => pdfcraft_fonts::helvetica_width(s, self.size) * if self.bold { 1.04 } else { 1.0 },
|
||||
@@ -132,6 +134,12 @@ impl Face {
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes of text measured on this thread, for tests that bound layout work.
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
pub(crate) static MEASURED: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// Default line height: XFA's font leading for the standard faces.
|
||||
pub fn line_height(size: f64) -> f64 {
|
||||
size * 1.15
|
||||
@@ -233,11 +241,16 @@ pub fn layout(rich: &RichText, rect: Rect, para: &Para, base: &Face, embed: &dyn
|
||||
lines.push((done, p.h_align));
|
||||
}
|
||||
if ink > avail - indent(line.first) && line.words.is_empty() {
|
||||
// A word longer than a line: break it by character.
|
||||
// A word longer than a line: break it by character. The piece's advance grows by
|
||||
// each character's (widths add up), so no prefix is measured again.
|
||||
let mut piece = String::new();
|
||||
let mut advance = 0.0;
|
||||
let mut buf = [0u8; 4];
|
||||
for ch in w.text.chars() {
|
||||
let ch_width = w.face.width(ch.encode_utf8(&mut buf));
|
||||
piece.push(ch);
|
||||
if w.face.width(&piece) > avail - indent(line.first) && piece.chars().count() > 1 {
|
||||
advance += ch_width;
|
||||
if advance > avail - indent(line.first) && piece.len() > ch.len_utf8() {
|
||||
piece.pop();
|
||||
let width = w.face.width(&piece);
|
||||
line.words.push(Word { text: std::mem::take(&mut piece), face: w.face.clone(), width });
|
||||
@@ -246,6 +259,7 @@ pub fn layout(rich: &RichText, rect: Rect, para: &Para, base: &Face, embed: &dyn
|
||||
let done = std::mem::replace(&mut line, Line { words: Vec::new(), width: 0.0, size: base.size, first: false });
|
||||
lines.push((done, p.h_align));
|
||||
piece.push(ch);
|
||||
advance = ch_width;
|
||||
}
|
||||
}
|
||||
if !piece.is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user