Files
pdfcraft/crates/engine/src/xfa_form.rs
T

298 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! A document's XFA form: laying it out, keeping its datasets and fields in step, and taking it out.
use crate::edit::EditCtx;
use crate::{Document, EditError, FieldValue, XfaLayout, guard, xfa};
/// Lay a dynamic XFA form out (pages and fields) and give its widgets appearances.
pub(crate) fn xfa_layout(doc: &mut pdfcraft_cos::Document) -> Result<XfaLayout, String> {
let mut report = pdfcraft_xfa::render_into(doc).map_err(|e| e.to_string())?;
xfa_redraw(doc)?;
// The form's own values: one its field's font can't draw is drawn with `?` and reported.
report.warnings.extend(undrawn_field_warnings(doc));
Ok(report)
}
/// Give the fields of a laid-out XFA form their appearances. A signed signature box keeps the
/// picture its signer's application drew.
pub(crate) fn xfa_redraw(doc: &mut pdfcraft_cos::Document) -> Result<(), String> {
for f in pdfcraft_forms::fields(doc) {
if f.kind == pdfcraft_forms::FieldKind::Signature && doc.get(f.obj).as_dict().is_some_and(|d| d.contains(b"V")) {
continue;
}
pdfcraft_forms::redraw_field(doc, &f.name).map_err(|e| format!("{}: {e}", f.name))?;
}
Ok(())
}
/// A warning for each field whose appearance PdfCraft drew with `?` for a character its font
/// can't draw (laying out an XFA form's own values).
pub(crate) fn undrawn_field_warnings(doc: &pdfcraft_cos::Document) -> Vec<String> {
pdfcraft_forms::undrawn_fields(doc)
.into_iter()
.map(|(name, c)| {
format!("form field {name:?} holds \"{}\" (U+{:04X}), which its font can't draw: it shows ? in its place", c.escape_debug(), u32::from(c))
})
.collect()
}
/// The form's fields as the XFA data layer wants them.
fn xfa_field_data(doc: &pdfcraft_cos::Document) -> Vec<pdfcraft_xfa::FieldDatum> {
use pdfcraft_forms::FieldKind as K;
pdfcraft_forms::fields(doc)
.into_iter()
.map(|f| {
let data = match f.kind {
K::Text | K::Combo | K::List => pdfcraft_xfa::FieldData::Text(f.value.join("\n")),
K::CheckBox => pdfcraft_xfa::FieldData::Check(!f.value.is_empty()),
K::Radio => pdfcraft_xfa::FieldData::Radio(f.value.first().cloned()),
K::PushButton | K::Signature => pdfcraft_xfa::FieldData::None,
};
pdfcraft_xfa::FieldDatum { obj: f.obj, name: f.name, data }
})
.collect()
}
/// Keep the XFA datasets packet in step with the fields after an edit. Returns what could not
/// be written.
/// `datasets`: the stream this edit already wrote, replaced in place rather than added again
/// (and set to the one written).
/// `tpl`: the form's parsed template when the document has one cached (else it is parsed).
pub(crate) fn xfa_sync_datasets(
doc: &mut pdfcraft_cos::Document,
tpl: Option<&pdfcraft_xfa::model::Template>,
datasets: &mut Option<pdfcraft_cos::ObjRef>,
) -> Result<Vec<String>, String> {
let data = xfa_field_data(doc);
let r = pdfcraft_xfa::write_datasets_with(doc, tpl, &data, *datasets).map_err(|e| e.to_string())?;
if r.stream.is_some() {
*datasets = r.stream;
}
Ok(r.warnings)
}
/// What a document whose form script ran away is told.
pub(crate) const XFA_SCRIPTS_OFF: &str =
"A script of this form ran too long and was abandoned; the form's scripts are off for this document (reopen it to run them again)";
/// Most XFA warnings kept per document.
const MAX_XFA_WARNINGS: usize = 50;
/// Add `new` to `list` (no repeats, at most [`MAX_XFA_WARNINGS`]).
pub(crate) fn note_warnings(list: &mut Vec<String>, new: &[String]) {
for w in new {
if !list.contains(w) && list.len() < MAX_XFA_WARNINGS {
list.push(w.clone());
}
}
}
/// A data file's text: UTF-8 or UTF-16 (by a byte order mark, or how `<` is written), or
/// Latin-1 when its XML declaration says so. `None`: not text at all (FDF); `Err`: an
/// encoding this can't read.
pub(crate) fn decode_data_file(bytes: &[u8]) -> Option<Result<String, String>> {
if bytes.starts_with(b"%FDF") {
return None;
}
let declared = |text: &str| -> Option<String> {
let head = text.get(..text.len().min(256))?;
let decl = head.strip_prefix('\u{feff}').unwrap_or(head).trim_start().strip_prefix("<?xml")?;
let decl = decl.split_once("?>")?.0;
let enc = decl.split_once("encoding=")?.1;
let quote = enc.chars().next()?;
Some(enc.get(1..)?.split(quote).next()?.trim().to_ascii_lowercase())
};
if let Some((text, _)) = pdfcraft_xfa::packets::decode(bytes) {
match declared(&text).as_deref() {
None | Some("utf-8" | "utf8" | "utf-16" | "utf-16le" | "utf-16be" | "us-ascii" | "ascii") => return Some(Ok(text)),
Some("iso-8859-1" | "latin1" | "latin-1" | "windows-1252" | "cp1252") if !bytes.starts_with(&[0xEF, 0xBB, 0xBF]) => {
// Each byte is the character it names (the 0x80–0x9F block of Windows-1252
// differs, and is rare in form data).
return Some(Ok(bytes.iter().map(|b| char::from(*b)).collect()));
}
Some(other) => return Some(Err(format!("the data file's encoding {other:?} is not supported (UTF-8, UTF-16 and ISO-8859-1 are)"))),
}
}
// Not valid UTF-8/UTF-16: Latin-1 by declaration, else refused.
let latin1: String = bytes.iter().map(|b| char::from(*b)).collect();
match declared(&latin1).as_deref() {
Some("iso-8859-1" | "latin1" | "latin-1" | "windows-1252" | "cp1252") => Some(Ok(latin1)),
_ => Some(Err("the data file is not UTF-8, UTF-16 or ISO-8859-1 text".into())),
}
}
/// Take the XFA out of `doc` and note what went with it: the packets, data no field showed,
/// buttons that did nothing but run scripts, and fields whose value differs from the form's
/// data (data PdfCraft could not read into them, now lost).
pub(crate) fn strip_xfa(doc: &mut pdfcraft_cos::Document, cx: &mut EditCtx) -> Result<(), EditError> {
let fields = pdfcraft_forms::fields(doc);
let data = xfa_field_data(doc);
let differ = pdfcraft_xfa::read_values(doc, &data)
.into_iter()
.filter(|(name, value)| {
let Some(f) = fields.iter().find(|f| f.name == *name) else { return false };
match value {
pdfcraft_xfa::FieldData::Text(t) => f.value.join("\n") != *t && !t.trim().is_empty(),
pdfcraft_xfa::FieldData::Check(on) => f.value.is_empty() == *on,
pdfcraft_xfa::FieldData::Radio(sel) => f.value.first() != sel.as_ref(),
pdfcraft_xfa::FieldData::None => false,
}
})
.count();
let r = pdfcraft_xfa::strip(doc).map_err(|e| EditError::Write(e.to_string()))?;
cx.xfa_removed.extend(r.notes());
if differ > 0 {
cx.xfa_removed.push(format!(
"{differ} field(s) held a value other than the form's XFA data (data PdfCraft could not read into them); the data's version is gone"
));
}
Ok(())
}
/// Keep the document's XFA state in step with its file after an edit: a script may have laid
/// the form out again (rows added, subforms shown), an edit may have taken the XFA out
/// (flattened, or made static), or an undo put it back.
pub(crate) fn sync_xfa_state(doc: &mut Document) {
let Some(cos) = doc.editor.as_ref().map(|e| &e.cos) else { return };
match pdfcraft_xfa::existing_layout(cos) {
Some(report) => {
if doc.xfa.is_none() {
// Back after an undo: the XFA's own warnings apply again, the strip's don't.
doc.xfa_warnings.retain(|w| !w.starts_with(XFA_REMOVED_PREFIX));
if doc.xfa_template.is_none() && !doc.xfa_scripts_off {
doc.xfa_template = xfa::template(cos);
}
}
doc.xfa = Some(report);
}
None => {
doc.xfa = None;
doc.xfa_template = None;
// Only what the strip said stays (the XFA's own warnings no longer apply).
doc.xfa_warnings.retain(|w| w.starts_with(XFA_REMOVED_PREFIX));
}
}
}
/// How the warnings of a strip start, so they can be told from the XFA's own.
pub(crate) const XFA_REMOVED_PREFIX: &str = "XFA removed: ";
/// Is this an XFA data file (an XDP, a datasets document, a data element or a data document),
/// rather than XFDF, Acrobat's `<fields>` XML or a table? By its root element.
pub(crate) fn is_xfa_data_file(text: &str) -> bool {
let text = text.get(..text.len().min(65_536)).unwrap_or(text);
let mut rest = text.trim_start_matches('\u{feff}').trim_start();
// Past the XML declaration, comments and processing instructions.
let mut guard = 0;
while guard < 100 {
guard += 1;
if let Some(r) = rest.strip_prefix("<?") {
rest = r.split_once("?>").map_or("", |(_, r)| r).trim_start();
} else if let Some(r) = rest.strip_prefix("<!--") {
rest = r.split_once("-->").map_or("", |(_, r)| r).trim_start();
} else if let Some(r) = rest.strip_prefix("<!") {
rest = r.split_once('>').map_or("", |(_, r)| r).trim_start();
} else {
break;
}
}
let Some(tag) = rest.strip_prefix('<') else { return false };
let name = tag.split(|c: char| c.is_whitespace() || c == '>' || c == '/').next().unwrap_or("");
let local = name.rsplit(':').next().unwrap_or(name);
!local.is_empty() && !matches!(local, "xfdf" | "fields" | "html" | "body")
}
/// The content type a picture is saved under, from its bytes: the kinds a laid-out form shows.
pub(crate) fn image_content_type(bytes: &[u8]) -> Option<&'static str> {
match pdfcraft_xfa::image::kind(bytes)? {
pdfcraft_xfa::image::Kind::Jpeg => Some("image/jpeg"),
pdfcraft_xfa::image::Kind::Png => Some("image/png"),
pdfcraft_xfa::image::Kind::Gif => Some("image/gif"),
pdfcraft_xfa::image::Kind::Bmp => Some("image/bmp"),
pdfcraft_xfa::image::Kind::Tiff => Some("image/tiff"),
}
}
/// After the XFA data changed outside the fields (an import, a picture): a laid-out form is
/// laid out again from the data (then, when `calculate`, its calculations run), and the
/// fields take the data's values.
pub(crate) fn xfa_after_data_change(doc: &mut pdfcraft_cos::Document, cx: &mut EditCtx, calculate: bool) -> Result<(), EditError> {
if let Some(tpl) = cx.xfa.clone() {
let mut run =
xfa::XfaRun { page_count: cx.pages, out: &mut cx.xfa_out, datasets: cx.xfa_datasets, ran_away: false, time_scale: cx.xfa_time_scale };
let r = guard(|| xfa::on_data_replaced(doc, &tpl, calculate, &mut run))
.unwrap_or_else(|m| Err(format!("laying the form out again failed unexpectedly ({m})")));
cx.xfa_datasets = run.datasets;
cx.xfa_ran_away |= run.ran_away;
if let Err(e) = r {
cx.xfa_out.errors.push(e);
}
}
let taken = xfa_values_from_datasets(doc).map_err(EditError::Write)?;
if !taken.is_empty() {
cx.xfa_out.console.push(format!("{} field value(s) taken from the imported data", taken.len()));
}
// Not laid out again (a static form, or scripts off): fields the data no longer holds are
// emptied, so the data replaced is the data shown.
if cx.xfa.is_none() && calculate {
let data = xfa_field_data(doc);
let present: std::collections::HashSet<String> = pdfcraft_xfa::read_values(doc, &data).into_iter().map(|(n, _)| n).collect();
for f in pdfcraft_forms::fields(doc) {
if present.contains(&f.name) || f.value.is_empty() {
continue;
}
use pdfcraft_forms::FieldKind as K;
let empty = match f.kind {
K::Text => FieldValue::Text(String::new()),
K::CheckBox => FieldValue::Check(false),
K::Radio => FieldValue::Radio(None),
K::Combo | K::List => FieldValue::Choice(Vec::new()),
K::PushButton | K::Signature => continue,
};
if let Err(e) = pdfcraft_forms::set_value(doc, &f.name, &empty) {
cx.xfa_out.errors.push(format!("{}: not emptied by the import: {e}", f.name));
}
}
}
Ok(())
}
/// Give the fields the values the XFA datasets hold (a form filled in another viewer). Returns
/// the names of the fields that changed.
pub(crate) 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);
// 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) = 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
// its saved value (an older viewer may have written it).
let saved = |v: &str| f.options.iter().find(|(e, s)| e == v || s == v).map_or(v.to_string(), |(e, _)| e.clone());
let picked: Vec<String> = if f.has(pdfcraft_forms::flags::MULTI_SELECT) {
t.lines().map(str::trim).filter(|l| !l.is_empty()).map(saved).collect()
} else {
vec![saved(t.trim())]
};
(f.value != picked).then_some(FieldValue::Choice(picked))
}
pdfcraft_xfa::FieldData::Text(t) => (f.value.join("\n") != t).then_some(FieldValue::Text(t)),
pdfcraft_xfa::FieldData::Check(on) => (f.value.is_empty() == on).then_some(FieldValue::Check(on)),
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 {
updates.push((name, v));
}
}
// 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())
}