mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 11:37:59 -06:00
fix(forms): filling a field also stores inline /DR fonts as objects (#3441)
Review follow-up (CodeRabbit on #3488): a form an earlier PdfCraft made should work in Acrobat once it has been filled here too, not only once a field is added or edited. set_value now runs the same conversion (shared with ensure_form through inline_fonts_as_objects and store_form_fonts); it adds nothing, only changes where the existing fonts are stored. Not in a form a signature covers (a signature field with /V): there the file stays as the signer saw it apart from the value. PdfCraft's own change check counts a /DR change after a form-fill certification as form fill, but another validator may not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
941077715a
commit
fa146bbecd
+59
-21
@@ -134,18 +134,8 @@ pub(crate) fn ensure_form(doc: &mut Document) -> Result<ObjRef, FormError> {
|
||||
if !af.contains(b"DA") {
|
||||
af.set(b"DA".to_vec(), PdfString::literal(b"/Helv 0 Tf 0 g".to_vec()));
|
||||
}
|
||||
let mut dr = af.get(b"DR").map(|o| doc.resolve(o)).and_then(|o| o.as_dict().cloned()).unwrap_or_default();
|
||||
let mut fonts = dr.get(b"Font").map(|o| doc.resolve(o)).and_then(|o| o.as_dict().cloned()).unwrap_or_default();
|
||||
// Each /DR font is its own object: Acrobat, redrawing a text field after it is typed in,
|
||||
// shows nothing when the font is a dictionary written inline in /DR (#3441). Fonts an
|
||||
// earlier save wrote inline become objects too; what they say is unchanged.
|
||||
let mut added = false;
|
||||
let inline: Vec<(Vec<u8>, Dict)> = fonts.iter().filter_map(|(k, v)| Some((k.clone(), v.as_dict()?.clone()))).collect();
|
||||
for (name, font) in inline {
|
||||
let r = doc.add(Object::Dict(font));
|
||||
fonts.set(name, Object::Ref(r));
|
||||
added = true;
|
||||
}
|
||||
let (dr, mut fonts) = form_fonts(doc, &af);
|
||||
let mut added = inline_fonts_as_objects(doc, &mut fonts);
|
||||
// Acrobat's resource names for the standard fonts its Appearance tab offers.
|
||||
for (name, base) in [("Helv", "Helvetica"), ("TiRo", "Times-Roman"), ("Cour", "Courier")] {
|
||||
if fonts.contains(name.as_bytes()) {
|
||||
@@ -161,20 +151,68 @@ pub(crate) fn ensure_form(doc: &mut Document) -> Result<ObjRef, FormError> {
|
||||
added = true;
|
||||
}
|
||||
if added {
|
||||
// An indirect /DR or /Font stays the object it is.
|
||||
match dr.get(b"Font").and_then(Object::as_ref) {
|
||||
Some(fr) => doc.set(fr, Object::Dict(fonts)),
|
||||
None => dr.set(b"Font".to_vec(), Object::Dict(fonts)),
|
||||
}
|
||||
match af.get(b"DR").and_then(Object::as_ref) {
|
||||
Some(dr_ref) => doc.set(dr_ref, Object::Dict(dr)),
|
||||
None => af.set(b"DR".to_vec(), Object::Dict(dr)),
|
||||
}
|
||||
store_form_fonts(doc, &mut af, dr, fonts);
|
||||
}
|
||||
doc.set(r, Object::Dict(af));
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// The form dictionary's `/DR` and its `/Font` dictionary, resolved (empty where missing).
|
||||
fn form_fonts(doc: &Document, af: &Dict) -> (Dict, Dict) {
|
||||
let dr = af.get(b"DR").map(|o| doc.resolve(o)).and_then(|o| o.as_dict().cloned()).unwrap_or_default();
|
||||
let fonts = dr.get(b"Font").map(|o| doc.resolve(o)).and_then(|o| o.as_dict().cloned()).unwrap_or_default();
|
||||
(dr, fonts)
|
||||
}
|
||||
|
||||
/// Each /DR font its own object: Acrobat, redrawing a text field after it is typed in, shows
|
||||
/// nothing when the font is a dictionary written inline in /DR (#3441). Fonts written inline
|
||||
/// (by an earlier save, or another writer) become objects saying the same; a font that is an
|
||||
/// object already, or an entry that isn't a dictionary, is left alone. Whether any changed.
|
||||
fn inline_fonts_as_objects(doc: &mut Document, fonts: &mut Dict) -> bool {
|
||||
let inline: Vec<(Vec<u8>, Dict)> = fonts.iter().filter_map(|(k, v)| Some((k.clone(), v.as_dict()?.clone()))).collect();
|
||||
let changed = !inline.is_empty();
|
||||
for (name, font) in inline {
|
||||
let r = doc.add(Object::Dict(font));
|
||||
fonts.set(name, Object::Ref(r));
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
/// Write `fonts` back as the `/DR /Font` of the form dictionary `af`. An indirect `/DR` or
|
||||
/// `/Font` stays the object it is.
|
||||
fn store_form_fonts(doc: &mut Document, af: &mut Dict, mut dr: Dict, fonts: Dict) {
|
||||
match dr.get(b"Font").and_then(Object::as_ref) {
|
||||
Some(fr) => doc.set(fr, Object::Dict(fonts)),
|
||||
None => dr.set(b"Font".to_vec(), Object::Dict(fonts)),
|
||||
}
|
||||
match af.get(b"DR").and_then(Object::as_ref) {
|
||||
Some(dr_ref) => doc.set(dr_ref, Object::Dict(dr)),
|
||||
None => af.set(b"DR".to_vec(), Object::Dict(dr)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Filling a field: the form's inline `/DR` fonts become objects (see
|
||||
/// [`inline_fonts_as_objects`]), so a form an earlier version of PdfCraft made works in Acrobat
|
||||
/// once it has been filled here too (#3441). Only the font storage changes; nothing is added.
|
||||
/// Not in a form a signature covers: there the file stays as the signer saw it, apart from the
|
||||
/// value itself.
|
||||
pub(crate) fn form_fonts_as_objects(doc: &mut Document, all: &[crate::Field]) -> Result<(), FormError> {
|
||||
let signed = all.iter().any(|f| f.kind == crate::FieldKind::Signature && doc.get(f.obj).as_dict().is_some_and(|d| d.contains(b"V")));
|
||||
if signed {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(root) = doc.root() else { return Ok(()) };
|
||||
// A form dictionary that isn't an object of its own is left as it is.
|
||||
let Some(r) = doc.get(root).as_dict().and_then(|c| c.reference(b"AcroForm")) else { return Ok(()) };
|
||||
let Some(mut af) = doc.get(r).as_dict().cloned() else { return Ok(()) };
|
||||
let (dr, mut fonts) = form_fonts(doc, &af);
|
||||
if inline_fonts_as_objects(doc, &mut fonts) {
|
||||
store_form_fonts(doc, &mut af, dr, fonts);
|
||||
doc.set(r, Object::Dict(af));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The Appearance tab: borders, colours and text.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Look {
|
||||
|
||||
@@ -932,6 +932,7 @@ pub fn set_value_with(doc: &mut Document, name: &str, value: &FieldValue, script
|
||||
return Err(FormError::ReadOnly(name.into()));
|
||||
}
|
||||
write_value(doc, f, value, scripts)?;
|
||||
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.
|
||||
|
||||
@@ -3292,3 +3292,52 @@ fn every_shape_of_form_fonts_ends_as_objects() {
|
||||
}
|
||||
assert_eq!(cases, 1 + 2 * (2 + 2 * 6 * 3), "every shape was tried");
|
||||
}
|
||||
|
||||
/// #3441 (review follow-up): filling a field converts the form's inline `/DR` fonts, so a form
|
||||
/// an earlier PdfCraft made works in Acrobat once it has been filled here. Nothing is added. A
|
||||
/// form a signature covers keeps `/DR` as the signer saw it, and a form whose fonts are objects
|
||||
/// already leaves its `/DR` object unwritten.
|
||||
#[test]
|
||||
fn filling_a_field_makes_inline_form_fonts_objects_unless_the_form_is_signed() {
|
||||
let inline = "/Helv << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>";
|
||||
let form = |signed: bool, dr: &str| {
|
||||
let sig = if signed { "<< /Type /Annot /Subtype /Widget /FT /Sig /T (sig) /V 7 0 R /Rect [0 0 0 0] /P 3 0 R >>" } else { "null" };
|
||||
let fields = if signed { "[5 0 R 6 0 R]" } else { "[5 0 R]" };
|
||||
document(&[
|
||||
"<< /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] >>".into(),
|
||||
format!("<< /Fields {fields} /DA (/Helv 0 Tf 0 g) /DR {dr} >>"),
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (name) /DA (/Helv 12 Tf 0 g) /Rect [50 700 300 720] /P 3 0 R >>".into(),
|
||||
sig.into(),
|
||||
"<< /Type /Sig /Filter /Adobe.PPKLite /Contents <00> >>".into(),
|
||||
"<< /Font << /Helv 9 0 R >> >>".into(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(),
|
||||
])
|
||||
};
|
||||
let mut doc = form(false, &format!("<< /Font << {inline} >> >>"));
|
||||
set_value(&mut doc, "name", &FieldValue::Text("Ada".into())).unwrap();
|
||||
let doc = reopen(&doc);
|
||||
let fonts = dr_fonts(&doc);
|
||||
assert!(fonts.reference(b"Helv").is_some(), "Helv became an object: {fonts:?}");
|
||||
assert_eq!(fonts.len(), 1, "nothing added: {fonts:?}");
|
||||
assert_eq!(field(&fields(&doc), "name").value, ["Ada"]);
|
||||
|
||||
let mut doc = form(true, &format!("<< /Font << {inline} >> >>"));
|
||||
set_value(&mut doc, "name", &FieldValue::Text("Ada".into())).unwrap();
|
||||
let doc = reopen(&doc);
|
||||
assert!(dr_fonts(&doc).get(b"Helv").is_some_and(|h| h.as_dict().is_some()), "a signed form keeps /DR as it was");
|
||||
|
||||
// An empty signature field signs nothing: the form is converted as an unsigned one.
|
||||
let mut doc = form(true, &format!("<< /Font << {inline} >> >>"));
|
||||
doc.update_dict(pdfcraft_cos::ObjRef::new(6, 0), |d| {
|
||||
d.remove(b"V");
|
||||
})
|
||||
.unwrap();
|
||||
set_value(&mut doc, "name", &FieldValue::Text("Ada".into())).unwrap();
|
||||
assert!(dr_fonts(&reopen(&doc)).reference(b"Helv").is_some(), "an unsigned signature field doesn't hold /DR back");
|
||||
|
||||
let mut doc = form(false, "8 0 R");
|
||||
set_value(&mut doc, "name", &FieldValue::Text("Ada".into())).unwrap();
|
||||
assert!(!doc.modified_objects().contains(&8), "/DR unchanged: {:?}", doc.modified_objects());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user