fix(xfa): bound the embedded field text one text block expands

Rich text resolved every embedded-field reference to a fresh copy of the
field's value, so a template repeating one reference to a long value many
times laid out a copy per reference. One text block expands at most as many
bytes of embedded values as a text of its own may hold; past that the
values are cut.
This commit is contained in:
aiongg
2026-10-22 17:22:50 +00:00
parent 17f5d80f44
commit 3412437191
2 changed files with 35 additions and 4 deletions
+14
View File
@@ -2670,3 +2670,17 @@ fn a_long_word_breaks_in_linear_work() {
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");
}
/// Rich text that embeds one long field value many times lays out a bounded amount of it.
#[test]
fn embedded_field_values_expand_to_a_bounded_amount_of_text() {
let face = text::Face::of(&Font { typeface: "Arial".into(), size: 10.0, ..Font::default() });
let value = "word ".repeat(3_300);
let run = Run { embed: Some("f".into()), ..Run::default() };
let rich = RichText { paragraphs: vec![Paragraph { runs: vec![run; 100], h_align: None }] };
let b = text::layout(&rich, Rect::new(0.0, 0.0, 500.0, 0.0), &Para::default(), &face, &|_| value.clone());
let kept: usize = b.spans.iter().map(|s| s.text.len()).sum();
assert!(kept > 0);
assert!(kept <= text::MAX_EMBEDDED, "{kept} bytes of embedded text laid out");
assert!(text::natural_width(&rich, &Para::default(), &face, &|_| value.clone()) <= face.width(&value) * 70.0);
}
+21 -4
View File
@@ -177,13 +177,27 @@ fn ink_width(w: &Word) -> f64 {
if ink.len() == w.text.len() { w.width } else { w.face.width(ink) }
}
/// Most bytes of embedded field values one text block shows, as much as a text of its own
/// holds: a value embedded over and over is expanded only so far.
pub(crate) const MAX_EMBEDDED: usize = 1 << 20;
/// Break the runs of a paragraph into words (spaces stay attached to the word before them).
fn words(runs: &[Run], base: &Face, embed: &dyn Fn(&str) -> String) -> Vec<Word> {
/// Embedded values take from `embed_left` bytes and are cut when it runs out.
fn words(runs: &[Run], base: &Face, embed: &dyn Fn(&str) -> String, embed_left: &mut usize) -> Vec<Word> {
let mut out = Vec::new();
for run in runs {
let face = base.with_run(run);
let text: String = match &run.embed {
Some(id) => embed(id),
Some(_) if *embed_left == 0 => String::new(),
Some(id) => {
let mut v = embed(id);
if v.len() > *embed_left {
let end = v.char_indices().map(|(i, _)| i).take_while(|i| *i <= *embed_left).last().unwrap_or(0);
v.truncate(end);
}
*embed_left = embed_left.saturating_sub(v.len());
v
}
None => run
.text
.chars()
@@ -224,8 +238,9 @@ pub fn layout(rich: &RichText, rect: Rect, para: &Para, base: &Face, embed: &dyn
let avail = (rect.w - para.margin_left - para.margin_right).max(1.0);
let mut lines: Vec<(Line, Option<HAlign>)> = Vec::new();
let mut total = 0.0;
let mut embed_left = MAX_EMBEDDED;
for (pi, p) in rich.paragraphs.iter().enumerate() {
let ws = words(&p.runs, base, embed);
let ws = words(&p.runs, base, embed, &mut embed_left);
let mut line = Line { words: Vec::new(), width: 0.0, size: base.size, first: true };
let mut first_in_para = true;
let indent = |first: bool| if first { para.text_indent.max(0.0) } else { 0.0 };
@@ -336,7 +351,9 @@ pub fn layout(rich: &RichText, rect: Rect, para: &Para, base: &Face, embed: &dyn
/// margins and first-line indent): how wide a box without an explicit width grows.
pub fn natural_width(rich: &RichText, para: &Para, base: &Face, embed: &dyn Fn(&str) -> String) -> f64 {
let indent = para.text_indent.max(0.0);
let widest = rich.paragraphs.iter().map(|p| words(&p.runs, base, embed).iter().map(|w| w.width).sum::<f64>()).fold(0.0, f64::max);
let mut embed_left = MAX_EMBEDDED;
let widest =
rich.paragraphs.iter().map(|p| words(&p.runs, base, embed, &mut embed_left).iter().map(|w| w.width).sum::<f64>()).fold(0.0, f64::max);
(para.margin_left + indent + widest + para.margin_right).max(0.0)
}