mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-28 12:08:46 -06:00
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:
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user