mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 05:28:09 -06:00
M6: a static XFA form whose data fills thousands of fields opens in seconds (#4638)
Opening a static XFA form gives its fields the values in its datasets (a form filled in by another application). Each value went through forms::set_value, which reads the whole field tree to find the field, writes it, and recalculates, reading the tree again. With n values that is about 2n readings of n fields: quadratic. A public 136-page form with about 6,000 fields took 261 s to open, a 95-page one with about 4,000 took 112 s; 8,000 synthetic fields took over ten minutes. forms::set_values fills a batch as set_value fills each in turn, reading the form once and recalculating once after the last value (the calculation order, ISO 32000-2 Table 224 /CO, runs once over the final values). Fields with the same fully qualified name (12.7.4.2) still share the value. A name the batch already wrote, or a twin of it, makes the form be read again first, so each write starts from what the earlier one left, as one by one. Each value reports its own result; a failed recalculation fails the batch, as it fails set_value. The engine collects the changed values and fills them with set_values, finding fields by name through a map rather than a scan. The two forms now open in 4 s and 3 s, with the same fields, values, warnings and document information as before; 8,000 synthetic fields open in 1.5 s. Tests: set_values matches set_value one by one (values, twins, the calculated total, every appearance, saved; refusals for an unknown name, a read-only field, an unknown option and clearing a no-toggle-off radio group the batch selected); it reads the form a fixed number of times; the engine takes 59 data values on a 60-field static form exactly as filling them one by one does (the read-only one refused); and opening 800 fields takes under 16 times as long as 100 (about 4x now, about 48x before). Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
737aeb4359
commit
6918d0be21
@@ -3533,9 +3533,14 @@ fn xfa_after_data_change(doc: &mut pdfcraft_cos::Document, cx: &mut EditCtx, cal
|
||||
fn xfa_values_from_datasets(doc: &mut pdfcraft_cos::Document) -> Result<Vec<String>, String> {
|
||||
let data = xfa_field_data(doc);
|
||||
let fields = pdfcraft_forms::fields(doc);
|
||||
let mut changed = Vec::new();
|
||||
// The first field of each name, as a lookup by name would find it.
|
||||
let mut by_name = std::collections::HashMap::new();
|
||||
for f in &fields {
|
||||
by_name.entry(f.name.as_str()).or_insert(f);
|
||||
}
|
||||
let mut updates = Vec::new();
|
||||
for (name, value) in pdfcraft_xfa::read_values(doc, &data) {
|
||||
let Some(f) = fields.iter().find(|f| f.name == name) else { continue };
|
||||
let Some(f) = by_name.get(name.as_str()) else { continue };
|
||||
let new = match value {
|
||||
pdfcraft_xfa::FieldData::Text(t) if matches!(f.kind, pdfcraft_forms::FieldKind::Combo | pdfcraft_forms::FieldKind::List) => {
|
||||
// Saved values, one per line for a multi-select list; shown text stands for
|
||||
@@ -3553,13 +3558,14 @@ fn xfa_values_from_datasets(doc: &mut pdfcraft_cos::Document) -> Result<Vec<Stri
|
||||
pdfcraft_xfa::FieldData::Radio(sel) => (f.value.first() != sel.as_ref()).then_some(FieldValue::Radio(sel)),
|
||||
pdfcraft_xfa::FieldData::None => None,
|
||||
};
|
||||
if let Some(v) = new
|
||||
&& pdfcraft_forms::set_value(doc, &name, &v).is_ok()
|
||||
{
|
||||
changed.push(name);
|
||||
if let Some(v) = new {
|
||||
updates.push((name, v));
|
||||
}
|
||||
}
|
||||
Ok(changed)
|
||||
// All at once: one by one, each value would read the whole form again, which took minutes
|
||||
// on a form with thousands of fields.
|
||||
let Ok(results) = pdfcraft_forms::set_values(doc, &updates) else { return Ok(Vec::new()) };
|
||||
Ok(updates.into_iter().zip(results).filter(|(_, r)| r.is_ok()).map(|((name, _), _)| name).collect())
|
||||
}
|
||||
|
||||
/// Validate the signature fields of `cos` (written as `bytes`).
|
||||
|
||||
@@ -5584,6 +5584,74 @@ fn static_xfa_forms_take_their_values_from_the_datasets_and_write_them_back() {
|
||||
assert!(!d2.info.warnings.iter().any(|w| w.contains("taken from this form's XFA data")), "nothing was rewritten");
|
||||
}
|
||||
|
||||
/// A static XFA form whose values are only in its datasets takes every one on opening, as
|
||||
/// filling the fields one at a time would leave it: values, appearances and the note; a
|
||||
/// read-only field keeps its own value.
|
||||
#[test]
|
||||
fn a_static_xfa_form_takes_many_data_values_as_one_by_one() {
|
||||
let n = 60;
|
||||
let original = Arc::new(pdfcraft_xfa::fixtures::static_shell_many(n));
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000);
|
||||
let id = s.open("many.pdf", None, original.clone(), None).expect("opens");
|
||||
let doc = s.get(id).unwrap();
|
||||
assert_eq!(doc.info.xfa, Some(pdfcraft_render::Xfa::Static));
|
||||
assert!(!doc.dirty && doc.bytes.starts_with(&original[..]), "the values are an appended revision");
|
||||
// The read-only first field keeps its own (empty) value, and is not named.
|
||||
let note = doc.info.warnings.iter().find(|w| w.contains("taken from this form's XFA data")).expect("a warning");
|
||||
assert!(note.starts_with("59 form field values") && note.contains("form1[0].page1[0].f1[0]") && note.ends_with("and 54 more"), "{note}");
|
||||
assert!(!note.contains("f0[0]"), "{note}");
|
||||
// The same values filled one at a time.
|
||||
let mut one_by_one = pdfcraft_cos::Document::open(original).unwrap();
|
||||
for i in 0..n {
|
||||
let r = pdfcraft_forms::set_value(&mut one_by_one, &format!("form1[0].page1[0].f{i}[0]"), &FieldValue::Text(format!("v{i}")));
|
||||
assert_eq!(r.is_err(), i == 0, "field {i}: {r:?}");
|
||||
}
|
||||
let opened = &doc.editor.as_ref().unwrap().cos;
|
||||
let shown = |d: &pdfcraft_cos::Document| -> Vec<(String, Vec<String>, String)> {
|
||||
pdfcraft_forms::fields(d)
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
let w = f.widgets.first().map(|w| w.obj).unwrap();
|
||||
let ap = d.get(w).as_dict().and_then(|w| w.get(b"AP").cloned()).map(|a| d.resolve(&a));
|
||||
let n = ap.and_then(|ap| ap.as_dict().and_then(|a| a.get(b"N").cloned())).map(|n| d.resolve(&n));
|
||||
let look = match n.as_deref() {
|
||||
Some(pdfcraft_cos::Object::Stream(st)) => String::from_utf8_lossy(&st.decoded().unwrap()).into_owned(),
|
||||
_ => String::new(),
|
||||
};
|
||||
(f.name, f.value, look)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let got = shown(opened);
|
||||
assert_eq!(got.len(), n);
|
||||
assert_eq!(got, shown(&one_by_one));
|
||||
assert!(got[0].1.is_empty());
|
||||
assert!(got.iter().enumerate().skip(1).all(|(i, (_, v, ap))| *v == [format!("v{i}")] && ap.contains(&format!("(v{i})"))));
|
||||
}
|
||||
|
||||
/// Opening a static XFA form whose datasets fill thousands of fields took minutes: each value
|
||||
/// read the whole form again (twice), so the time grew with the square of the fields. Eight
|
||||
/// times the fields now takes at most about eight times as long (it took about fifty).
|
||||
#[test]
|
||||
fn opening_a_static_xfa_form_grows_linearly_with_its_data_values() {
|
||||
let time = |n: usize| {
|
||||
let bytes = Arc::new(pdfcraft_xfa::fixtures::static_shell_many(n));
|
||||
(0..3)
|
||||
.map(|_| {
|
||||
let mut s = Session::new();
|
||||
let start = std::time::Instant::now();
|
||||
s.open("many.pdf", None, bytes.clone(), None).expect("opens");
|
||||
start.elapsed()
|
||||
})
|
||||
.min()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let small = time(100);
|
||||
let large = time(800);
|
||||
let ratio = large.as_secs_f64() / small.as_secs_f64().max(1e-6);
|
||||
assert!(ratio < 16.0, "100 fields: {small:?}, 800 fields: {large:?} ({ratio:.1}x)");
|
||||
}
|
||||
|
||||
/// A date goes into the XFA data in the field's data picture (`<bind><picture>`), as other
|
||||
/// viewers write and read it, and comes back from there when the form is opened again. It used
|
||||
/// to be written as an ISO date whatever the picture said.
|
||||
|
||||
@@ -14,6 +14,8 @@ set_value(&mut doc, "name", &FieldValue::Text("Ada".into()))?;
|
||||
set_value(&mut doc, "agree", &FieldValue::Check(true))?;
|
||||
set_value(&mut doc, "size", &FieldValue::Radio(Some("L".into())))?;
|
||||
set_value(&mut doc, "toppings", &FieldValue::Choice(vec!["Ham".into()]))?;
|
||||
// Many values: as set_value one by one, but reading the form once and recalculating once.
|
||||
let results: Vec<Result<(), FormError>> = set_values(&mut doc, &[("name".into(), FieldValue::Text("Ada".into()))])?;
|
||||
reset(&mut doc, None)?; // Clear form (back to /DV)
|
||||
|
||||
// Prepare a form
|
||||
|
||||
+88
-4
@@ -573,7 +573,21 @@ pub fn fields_with_adopted(doc: &Document) -> Fields {
|
||||
Fields { items, adopted_page_fields }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// Whole-form readings ([`enumerate`]) on this thread.
|
||||
static FORM_READS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// How many times this thread has read the whole form.
|
||||
#[cfg(test)]
|
||||
fn form_reads() -> usize {
|
||||
FORM_READS.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
fn enumerate(doc: &Document) -> (Vec<Field>, usize) {
|
||||
#[cfg(test)]
|
||||
FORM_READS.with(|n| n.set(n.get() + 1));
|
||||
let af = acroform(doc);
|
||||
let mut page_of = std::collections::HashMap::new();
|
||||
let mut annot_index = std::collections::HashMap::new();
|
||||
@@ -935,23 +949,93 @@ pub fn set_value_with(doc: &mut Document, name: &str, value: &FieldValue, script
|
||||
if all.is_empty() {
|
||||
return Err(FormError::NoForm);
|
||||
}
|
||||
let f = all.iter().find(|f| f.name == name).ok_or_else(|| FormError::NoSuchField(name.into()))?;
|
||||
let named: Vec<usize> = all.iter().enumerate().filter(|(_, f)| f.name == name).map(|(i, _)| i).collect();
|
||||
fill(doc, &all, &named, name, value, scripts)?;
|
||||
recalculate_with(doc, scripts)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fill several fields, as [`set_value`] fills each in turn, but reading the form once and
|
||||
/// recalculating once, after the last value. Filling fields one by one reads the whole form twice
|
||||
/// per field, which takes minutes on a form with thousands of fields.
|
||||
///
|
||||
/// Returns each value's result, in order. The outer `Err` is the recalculation failing, after
|
||||
/// which, as with [`set_value`], no value counts as filled (though each was written).
|
||||
pub fn set_values(doc: &mut Document, values: &[(String, FieldValue)]) -> Result<Vec<Result<(), FormError>>, FormError> {
|
||||
let mut scripts = NoScripts;
|
||||
let mut form = NamedFields::read(doc);
|
||||
// The field objects this batch wrote since the form was last read.
|
||||
let mut written = std::collections::HashSet::new();
|
||||
let mut results = Vec::with_capacity(values.len());
|
||||
let mut filled = false;
|
||||
for (name, value) in values {
|
||||
if form.all.is_empty() {
|
||||
results.push(Err(FormError::NoForm));
|
||||
continue;
|
||||
}
|
||||
// A field written earlier in the batch (named twice, or a twin's name) is read again,
|
||||
// so this write starts from what the earlier one left, as it would one by one.
|
||||
if form.objs(name).any(|o| written.contains(&o)) {
|
||||
form = NamedFields::read(doc);
|
||||
written.clear();
|
||||
}
|
||||
let named = form.named(name);
|
||||
let result = fill(doc, &form.all, named, name, value, &mut scripts);
|
||||
written.extend(form.objs(name));
|
||||
filled |= result.is_ok();
|
||||
results.push(result);
|
||||
}
|
||||
if filled {
|
||||
recalculate_with(doc, &mut scripts)?;
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// The form's fields, with the positions of each fully qualified name in them (in field order).
|
||||
struct NamedFields {
|
||||
all: Vec<Field>,
|
||||
by_name: std::collections::HashMap<String, Vec<usize>>,
|
||||
}
|
||||
|
||||
impl NamedFields {
|
||||
fn read(doc: &Document) -> Self {
|
||||
let all = fields(doc);
|
||||
let mut by_name: std::collections::HashMap<String, Vec<usize>> = std::collections::HashMap::new();
|
||||
for (i, f) in all.iter().enumerate() {
|
||||
by_name.entry(f.name.clone()).or_default().push(i);
|
||||
}
|
||||
NamedFields { all, by_name }
|
||||
}
|
||||
|
||||
fn named(&self, name: &str) -> &[usize] {
|
||||
self.by_name.get(name).map_or(&[], Vec::as_slice)
|
||||
}
|
||||
|
||||
/// The field objects named `name`.
|
||||
fn objs<'a>(&'a self, name: &str) -> impl Iterator<Item = ObjRef> + 'a {
|
||||
self.named(name).iter().filter_map(|&i| self.all.get(i)).map(|f| f.obj)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write `value` into the field `name`, without recalculating: `named` are the positions in
|
||||
/// `all` (the form as read before this write) of the fields with that name, in field order.
|
||||
fn fill(doc: &mut Document, all: &[Field], named: &[usize], name: &str, value: &FieldValue, scripts: &mut dyn Scripts) -> Result<(), FormError> {
|
||||
let f = named.first().and_then(|&i| all.get(i)).ok_or_else(|| FormError::NoSuchField(name.into()))?;
|
||||
if f.read_only() {
|
||||
return Err(FormError::ReadOnly(name.into()));
|
||||
}
|
||||
write_value(doc, f, value, scripts)?;
|
||||
author::form_fonts_as_objects(doc, &all)?;
|
||||
author::form_fonts_as_objects(doc, all)?;
|
||||
// Fields with the same fully qualified name are one field (§12.7.3.1). Writers that repeat a
|
||||
// name as separate field dictionaries, instead of widgets under one parent, get the value
|
||||
// the first one took after its scripts, without running them again.
|
||||
let twins: Vec<&Field> = all.iter().filter(|t| t.name == name && t.obj != f.obj && t.kind == f.kind).collect();
|
||||
let twins: Vec<&Field> = named.iter().filter_map(|&i| all.get(i)).filter(|t| t.obj != f.obj && t.kind == f.kind).collect();
|
||||
if !twins.is_empty() {
|
||||
let taken = fields(doc).into_iter().find(|x| x.obj == f.obj).map(|x| x.value).unwrap_or_default();
|
||||
for t in twins {
|
||||
share_value(doc, f.obj, t, &taken, scripts)?;
|
||||
}
|
||||
}
|
||||
recalculate_with(doc, scripts)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -4359,3 +4359,145 @@ fn comb_fields_show_their_cells() {
|
||||
let plain = saved_look(&doc, "d");
|
||||
assert!(!plain.contains(" m ") && plain.contains("(A) Tj"), "{plain}");
|
||||
}
|
||||
|
||||
/// A form for filling many values at once: two summed fields and their calculated total, a
|
||||
/// name two field dictionaries share, a check box, a radio group, a list and a read-only field.
|
||||
fn batch_form() -> Document {
|
||||
let tx = |name: &str, y: u32, rest: &str| {
|
||||
format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T ({name}) /Rect [50 {y} 250 {}] /P 3 0 R {rest} >>", y + 20)
|
||||
};
|
||||
let objs: Vec<String> = vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R /Annots [5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 12 0 R 13 0 R 14 0 R 17 0 R] >>".into(),
|
||||
"<< /Fields [5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 14 0 R 17 0 R] /CO [7 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 15 0 R >> >> >>"
|
||||
.into(),
|
||||
tx("a", 700, "/V (1)"),
|
||||
tx("b", 660, ""),
|
||||
tx("total", 620, r#"/AA << /C << /S /JavaScript /JS (AFSimple_Calculate\("SUM", new Array\("a", "b"\)\);) >> >>"#),
|
||||
tx("dup", 580, ""),
|
||||
tx("dup", 540, ""),
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree) /V /Off /AS /Off /Rect [300 700 315 715] /P 3 0 R >>".into(),
|
||||
"<< /FT /Btn /T (size) /Ff 49152 /Kids [12 0 R 13 0 R] >>".into(),
|
||||
"<< /Type /Annot /Subtype /Widget /Parent 11 0 R /AS /Off /Rect [300 650 315 665] /P 3 0 R /AP << /N << /S 16 0 R /Off 16 0 R >> >> >>"
|
||||
.into(),
|
||||
"<< /Type /Annot /Subtype /Widget /Parent 11 0 R /AS /Off /Rect [330 650 345 665] /P 3 0 R /AP << /N << /L 16 0 R /Off 16 0 R >> >> >>"
|
||||
.into(),
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Ch /T (country) /Ff 131072 /Opt [[(ca) (Canada)] [(fr) (France)]] /Rect [300 600 450 620] /P 3 0 R >>"
|
||||
.into(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(),
|
||||
"<< /Length 0 >>\nstream\n\nendstream".into(),
|
||||
tx("locked", 500, "/Ff 1 /V (kept)"),
|
||||
];
|
||||
document(&objs)
|
||||
}
|
||||
|
||||
/// What a filled form shows: every field as read, and each widget's drawn appearance.
|
||||
fn filled(doc: &Document) -> (Vec<Field>, Vec<String>) {
|
||||
let all = fields(doc);
|
||||
let looks = all
|
||||
.iter()
|
||||
.flat_map(|f| &f.widgets)
|
||||
.map(|w| {
|
||||
let wd = doc.get(w.obj).as_dict().cloned().unwrap_or_default();
|
||||
let n = wd.get(b"AP").map(|a| doc.resolve(a)).and_then(|a| a.as_dict().and_then(|a| a.get(b"N").cloned()));
|
||||
match n.map(|n| doc.resolve(&n)) {
|
||||
Some(n) => match &*n {
|
||||
Object::Stream(s) => String::from_utf8_lossy(&s.decoded().unwrap_or_default()).into_owned(),
|
||||
other => format!("{other:?}").chars().filter(|c| !c.is_ascii_digit()).collect(),
|
||||
},
|
||||
None => String::new(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
(all, looks)
|
||||
}
|
||||
|
||||
/// Filling values in one batch leaves the form as filling them one by one does: the values,
|
||||
/// the twin of a shared name, the calculated total and every widget's appearance; a name given
|
||||
/// twice ends with its last value, and is checked against the value it took earlier in the batch;
|
||||
/// and each value fails as it would on its own.
|
||||
#[test]
|
||||
fn set_values_fills_as_set_value_does_one_by_one() {
|
||||
let values: Vec<(String, FieldValue)> = vec![
|
||||
("a".into(), FieldValue::Text("2".into())),
|
||||
("b".into(), FieldValue::Text("3".into())),
|
||||
("dup".into(), FieldValue::Text("shared".into())),
|
||||
("agree".into(), FieldValue::Check(true)),
|
||||
("size".into(), FieldValue::Radio(Some("L".into()))),
|
||||
("country".into(), FieldValue::Choice(vec!["fr".into()])),
|
||||
("nowhere".into(), FieldValue::Text("x".into())),
|
||||
("locked".into(), FieldValue::Text("changed".into())),
|
||||
("size".into(), FieldValue::Radio(Some("XL".into()))),
|
||||
("dup".into(), FieldValue::Text("again".into())),
|
||||
("a".into(), FieldValue::Text("10".into())),
|
||||
// Refused only because the batch already selected L: the group keeps one selected.
|
||||
("size".into(), FieldValue::Radio(None)),
|
||||
];
|
||||
let mut one_by_one = batch_form();
|
||||
let expected: Vec<Result<(), FormError>> = values.iter().map(|(n, v)| set_value(&mut one_by_one, n, v)).collect();
|
||||
let mut batch = batch_form();
|
||||
let results = set_values(&mut batch, &values).expect("recalculates");
|
||||
assert_eq!(results, expected);
|
||||
assert_eq!(
|
||||
results.iter().filter(|r| r.is_err()).count(),
|
||||
4,
|
||||
"the unknown name, the read-only field, the unknown option and clearing the group fail: {results:?}"
|
||||
);
|
||||
let (all, looks) = filled(&batch);
|
||||
assert_eq!((all.clone(), looks), filled(&one_by_one));
|
||||
assert_eq!(field(&all, "total").value, ["13"], "recalculated once, after the last value");
|
||||
assert!(all.iter().filter(|f| f.name == "dup").all(|f| f.value == ["again"]), "both dup fields take the last value");
|
||||
// And as saved.
|
||||
assert_eq!(filled(&reopen(&batch)), filled(&reopen(&one_by_one)));
|
||||
}
|
||||
|
||||
/// A form without fields refuses each value, and an empty batch changes nothing.
|
||||
#[test]
|
||||
fn set_values_without_a_form_or_values() {
|
||||
let mut doc = document(&[
|
||||
"<< /Type /Catalog /Pages 2 0 R >>".into(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R >>".into(),
|
||||
]);
|
||||
let r = set_values(&mut doc, &[("a".into(), FieldValue::Text("1".into()))]).unwrap();
|
||||
assert_eq!(r, vec![Err(FormError::NoForm)]);
|
||||
let mut form = batch_form();
|
||||
assert_eq!(set_values(&mut form, &[]).unwrap(), Vec::new());
|
||||
assert!(!form.is_modified());
|
||||
// Nothing filled, nothing recalculated: the total stays as the file has it.
|
||||
let r = set_values(&mut form, &[("nowhere".into(), FieldValue::Text("1".into()))]).unwrap();
|
||||
assert_eq!(r, vec![Err(FormError::NoSuchField("nowhere".into()))]);
|
||||
assert!(field(&fields(&form), "total").value.is_empty() && !form.is_modified());
|
||||
}
|
||||
|
||||
/// Filling many fields reads the form a fixed number of times, not once or twice per field
|
||||
/// (which took minutes to open a form with thousands of fields whose XFA data was filled in
|
||||
/// elsewhere).
|
||||
#[test]
|
||||
fn set_values_reads_the_form_once_not_per_value() {
|
||||
let n: u32 = 60;
|
||||
let mut objs: Vec<String> =
|
||||
vec!["<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(), "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into()];
|
||||
let kids: Vec<String> = (0..n).map(|i| format!("{} 0 R", i + 5)).collect();
|
||||
objs.push(format!("<< /Type /Page /Parent 2 0 R /Annots [{}] >>", kids.join(" ")));
|
||||
objs.push(format!("<< /Fields [{}] /DA (/Helv 10 Tf 0 g) >>", kids.join(" ")));
|
||||
for i in 0..n {
|
||||
objs.push(format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T (f{i}) /Rect [50 {y} 250 {top}] /P 3 0 R >>", y = 10 * i, top = 10 * i + 10));
|
||||
}
|
||||
let mut doc = document(&objs);
|
||||
let values: Vec<(String, FieldValue)> = (0..n).map(|i| (format!("f{i}"), FieldValue::Text(format!("v{i}")))).collect();
|
||||
let before = form_reads();
|
||||
let results = set_values(&mut doc, &values).unwrap();
|
||||
let reads = form_reads() - before;
|
||||
assert!(results.iter().all(Result::is_ok));
|
||||
// One reading to fill, one to recalculate.
|
||||
assert!(reads <= 2, "{reads} readings of the form for {n} values");
|
||||
assert!(fields(&doc).iter().all(|f| f.value == [format!("v{}", &f.name[1..])]));
|
||||
// A name filled again is read again, once: f0 and f1 again cost one more reading.
|
||||
let again: Vec<(String, FieldValue)> = ["f0", "f1", "f0", "f1"].iter().map(|n| (n.to_string(), FieldValue::Text("w".into()))).collect();
|
||||
let before = form_reads();
|
||||
assert!(set_values(&mut doc, &again).unwrap().iter().all(Result::is_ok));
|
||||
let reads = form_reads() - before;
|
||||
assert_eq!(reads, 3, "one to fill, one when f0 comes again, one to recalculate");
|
||||
}
|
||||
|
||||
@@ -144,6 +144,43 @@ pub fn static_shell(data: &str) -> Vec<u8> {
|
||||
assemble(objs)
|
||||
}
|
||||
|
||||
/// A static XFA form of `n` text fields `form1[0].page1[0].f<i>[0]` on one page, whose values
|
||||
/// (`v<i>`) are only in its datasets, as when another viewer filled it in. The first field is
|
||||
/// read-only.
|
||||
pub fn static_shell_many(n: usize) -> Vec<u8> {
|
||||
let data: String = (0..n).map(|i| format!("<f{i}>v{i}</f{i}>")).collect();
|
||||
let xdp = format!(
|
||||
"<xdp:xdp xmlns:xdp=\"http://ns.adobe.com/xdp/\"><template xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\"><subform name=\"form1\"/></template><xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data><form1><page1>{data}</page1></form1></xfa:data></xfa:datasets></xdp:xdp>"
|
||||
);
|
||||
// Objects 1–7, then the widgets from 8.
|
||||
let kids: Vec<String> = (0..n).map(|i| format!("{} 0 R", i + 8)).collect();
|
||||
let kids = kids.join(" ");
|
||||
let mut objs: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R /AcroForm << /XFA 5 0 R /Fields [6 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >> >> >> >> >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||||
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Annots [{kids}] >>").into_bytes(),
|
||||
b"<< /Length 0 >>\nstream\n\nendstream".to_vec(),
|
||||
format!("<< /Length {} >>\nstream\n{xdp}\nendstream", xdp.len()).into_bytes(),
|
||||
b"<< /T (form1[0]) /Kids [7 0 R] >>".to_vec(),
|
||||
format!("<< /T (page1[0]) /Parent 6 0 R /Kids [{kids}] >>").into_bytes(),
|
||||
];
|
||||
for i in 0..n {
|
||||
let y = 760 - (i % 50) * 14;
|
||||
let x = 36 + (i / 50) * 110;
|
||||
// The first field is read-only: its data value is not taken.
|
||||
let flags = if i == 0 { " /Ff 1" } else { "" };
|
||||
objs.push(
|
||||
format!(
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (f{i}[0]){flags} /Parent 7 0 R /Rect [{x} {y} {} {}] /P 3 0 R /DA (/Helv 9 Tf 0 g) >>",
|
||||
x + 100,
|
||||
y + 12
|
||||
)
|
||||
.into_bytes(),
|
||||
);
|
||||
}
|
||||
assemble(objs)
|
||||
}
|
||||
|
||||
/// A PDF file holding `objs` as objects 1, 2, … in order, with a cross-reference table and a
|
||||
/// trailer whose root is object 1.
|
||||
fn assemble(objs: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
|
||||
Reference in New Issue
Block a user