diff --git a/crates/forms/src/author.rs b/crates/forms/src/author.rs index 8a08bb3d5..1d21cf2e7 100644 --- a/crates/forms/src/author.rs +++ b/crates/forms/src/author.rs @@ -134,10 +134,9 @@ pub(crate) fn ensure_form(doc: &mut Document) -> Result { 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(); + 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. - let mut added = false; for (name, base) in [("Helv", "Helvetica"), ("TiRo", "Times-Roman"), ("Cour", "Courier")] { if fonts.contains(name.as_bytes()) { continue; @@ -147,17 +146,105 @@ pub(crate) fn ensure_form(doc: &mut Document) -> Result { f.set(b"Subtype".to_vec(), Object::name("Type1")); f.set(b"BaseFont".to_vec(), Object::name(base)); f.set(b"Encoding".to_vec(), Object::name("WinAnsiEncoding")); - fonts.set(name.as_bytes().to_vec(), Object::Dict(f)); + let r = doc.add(Object::Dict(f)); + fonts.set(name.as_bytes().to_vec(), Object::Ref(r)); added = true; } if added { - dr.set(b"Font".to_vec(), Object::Dict(fonts)); - 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, 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 && holds_value(doc, f.obj)); + if signed { + return Ok(()); + } + let Some(root) = doc.root() else { return Ok(()) }; + let Some(entry) = doc.get(root).as_dict().and_then(|c| c.get(b"AcroForm").cloned()) else { return Ok(()) }; + match entry { + Object::Ref(r) => { + 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)); + } + } + // A form dictionary written directly in the catalog (the catalog's /AcroForm need not be + // an indirect reference) is written back there. + Object::Dict(mut af) => { + 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.update_dict(root, |c| c.set(b"AcroForm".to_vec(), Object::Dict(af)))?; + } + } + _ => {} + } + Ok(()) +} + +/// Whether the field `r`, or the nearest ancestor that has one, holds a `/V` (the entry is +/// inheritable, ISO 32000-2 §12.7.4). The `/Parent` chain is followed at most 64 deep, as deep +/// as the field walk goes, so a cyclic chain ends. +fn holds_value(doc: &Document, r: ObjRef) -> bool { + let mut at = r; + for _ in 0..=64 { + let obj = doc.get(at); + let Some(d) = obj.as_dict() else { return false }; + if d.contains(b"V") { + return true; + } + match d.reference(b"Parent") { + Some(p) => at = p, + None => return false, + } + } + false +} + /// The Appearance tab: borders, colours and text. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Look { diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index d120c3cd3..ec53f73f5 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -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. diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 4a88273f1..961ccc116 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -3332,3 +3332,311 @@ fn a_typed_name_loses_its_ends_but_keeps_spaces_beside_periods() { // A part that is only spaces is still empty. assert!(add_field(&mut doc, 0, [300.0, 300.0, 450.0, 320.0], &text, Some("g. .h")).is_err()); } + +/// The form's `/DR /Font` entries, resolved through an indirect `/DR` or `/Font`. +fn dr_fonts(doc: &Document) -> Dict { + let af = acroform(doc).unwrap(); + let dr = doc.resolve(af.get(b"DR").unwrap()); + doc.resolve(dr.as_dict().unwrap().get(b"Font").unwrap()).as_dict().cloned().unwrap() +} + +/// #3441: Acrobat shows nothing in a text field it redraws after typing when the field's `/DR` +/// font is a dictionary written inline. A form PdfCraft makes holds each font as its own object. +#[test] +fn a_new_form_holds_its_fonts_as_objects() { + 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(), + ]); + add_field(&mut doc, 0, [40.0, 600.0, 300.0, 630.0], &NewField::Text { multiline: false }, Some("name")).unwrap(); + let doc = reopen(&doc); + let fonts = dr_fonts(&doc); + for (name, base) in [("Helv", "Helvetica"), ("TiRo", "Times-Roman"), ("Cour", "Courier")] { + let r = fonts.reference(name.as_bytes()).unwrap_or_else(|| panic!("{name} is an object: {fonts:?}")); + assert_eq!(doc.get(r).as_dict().and_then(|f| f.name(b"BaseFont")), Some(base.as_bytes())); + } +} + +/// #3441: a form saved earlier with inline `/DR` fonts gets them as objects once a field is +/// added, saying the same; an indirect `/Font` dictionary stays the object it was. +#[test] +fn inline_form_fonts_become_objects_and_an_indirect_font_dictionary_stays_one() { + for indirect in [false, true] { + let font = "/Helv << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >> /Mine << /Type /Font /Subtype /Type1 /BaseFont /Symbol >>"; + let (fonts_entry, extra) = + if indirect { ("6 0 R".to_string(), vec![format!("<< {font} >>")]) } else { (format!("<< {font} >>"), Vec::new()) }; + let mut objs: Vec = 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 >>".into(), + format!("<< /Fields [] /DA (/Helv 0 Tf 0 g) /DR << /Font {fonts_entry} >> >>"), + "null".into(), + ]; + objs.extend(extra); + let mut doc = document(&objs); + add_field(&mut doc, 0, [40.0, 600.0, 300.0, 630.0], &NewField::Text { multiline: false }, Some("name")).unwrap(); + let doc = reopen(&doc); + let fonts = dr_fonts(&doc); + for (name, base) in [("Helv", "Helvetica"), ("Mine", "Symbol"), ("TiRo", "Times-Roman")] { + let r = fonts.reference(name.as_bytes()).unwrap_or_else(|| panic!("{name} is an object (indirect {indirect}): {fonts:?}")); + assert_eq!(doc.get(r).as_dict().and_then(|f| f.name(b"BaseFont")), Some(base.as_bytes()), "{name} says the same"); + } + let dr = doc.resolve(acroform(&doc).unwrap().get(b"DR").unwrap()); + assert_eq!(dr.as_dict().unwrap().reference(b"Font").is_some(), indirect, "the /Font dictionary keeps its form"); + } +} + +/// #3441: an indirect `/DR` stays the object it is, and a form whose three standard fonts are +/// all already there (inline) still gets them as objects, though none is added. +#[test] +fn an_indirect_resource_dictionary_and_a_complete_inline_set_are_handled() { + let inline = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>"; + let all = format!( + "/Helv {inline} /TiRo << /Type /Font /Subtype /Type1 /BaseFont /Times-Roman >> /Cour << /Type /Font /Subtype /Type1 /BaseFont /Courier >>" + ); + for indirect_dr in [false, true] { + let dr = if indirect_dr { "5 0 R".to_string() } else { format!("<< /Font << {all} >> >>") }; + let doc = 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 >>".into(), + format!("<< /Fields [] /DA (/Helv 0 Tf 0 g) /DR {dr} >>"), + format!("<< /Font << {all} >> >>"), + ]); + let mut doc = doc; + add_field(&mut doc, 0, [40.0, 600.0, 300.0, 630.0], &NewField::Text { multiline: false }, Some("name")).unwrap(); + let doc = reopen(&doc); + let fonts = dr_fonts(&doc); + for name in ["Helv", "TiRo", "Cour"] { + assert!(fonts.reference(name.as_bytes()).is_some(), "{name} is an object (indirect /DR {indirect_dr}): {fonts:?}"); + } + assert_eq!(acroform(&doc).unwrap().reference(b"DR").is_some(), indirect_dr, "/DR keeps its form"); + } +} + +/// #3441: fonts the form already has are its own, not replaced by the standard ones: an inline +/// `/Helv` that is Arial becomes an object that is still Arial, a `/TiRo` object stays that +/// object, and a form needing no change leaves its `/DR` object unwritten. The fonts PdfCraft +/// adds use WinAnsi, as Acrobat's do. +#[test] +fn existing_form_fonts_are_kept_and_an_unchanged_resource_dictionary_is_not_rewritten() { + let mut doc = 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 >>".into(), + "<< /Fields [] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv << /Type /Font /Subtype /Type1 /BaseFont /Arial >> /TiRo 5 0 R >> >> >>".into(), + "<< /Type /Font /Subtype /Type1 /BaseFont /Times-Bold >>".into(), + ]); + add_field(&mut doc, 0, [40.0, 600.0, 300.0, 630.0], &NewField::Text { multiline: false }, Some("name")).unwrap(); + let doc = reopen(&doc); + let fonts = dr_fonts(&doc); + let font = |name: &str| doc.get(fonts.reference(name.as_bytes()).unwrap()).as_dict().cloned().unwrap(); + assert_eq!(font("Helv").name(b"BaseFont"), Some(&b"Arial"[..]), "the form's own Helv"); + assert_eq!(fonts.reference(b"TiRo"), Some(pdfcraft_cos::ObjRef::new(5, 0)), "the same object"); + assert_eq!(font("Cour").name(b"Encoding"), Some(&b"WinAnsiEncoding"[..])); + + // Every font an object already, /DR an object: adding a field leaves /DR as it was. + let mut doc = 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 >>".into(), + "<< /Fields [] /DA (/Helv 0 Tf 0 g) /DR 5 0 R >>".into(), + "<< /Font << /Helv 6 0 R /TiRo 6 0 R /Cour 6 0 R >> >>".into(), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(), + ]); + add_field(&mut doc, 0, [40.0, 600.0, 300.0, 630.0], &NewField::Text { multiline: false }, Some("name")).unwrap(); + assert!(!doc.modified_objects().contains(&5), "/DR unchanged: {:?}", doc.modified_objects()); +} + +/// #3441, exhaustively: every way a form's `/DR` may hold its fonts. `/DR` missing, inline or an +/// object; its `/Font` missing, inline, an object or not a dictionary; `/Helv` missing, inline, +/// an object, a reference to nothing, `null` or a number; and a font of the form's own, inline or +/// an object. After a field is added, whatever the shape: +/// - no font is a dictionary written inline; +/// - a font that was an object is the same object, and one that was inline says the same as an +/// object; +/// - a `/DR` or `/Font` that was an object (and a dictionary) still is one; +/// - an entry that is not a dictionary (`null`, a number, a reference to nothing) is left alone; +/// - Helv, TiRo and Cour are there. +#[test] +fn every_shape_of_form_fonts_ends_as_objects() { + let font = |base: &str| format!("<< /Type /Font /Subtype /Type1 /BaseFont /{base} >>"); + let mut cases = 0; + for dr_shape in ["missing", "inline", "object"] { + for font_shape in ["missing", "inline", "object", "number"] { + for helv in ["missing", "inline", "object", "dangling", "null", "number"] { + for mine in ["missing", "inline", "object"] { + if dr_shape == "missing" && (font_shape != "missing" || helv != "missing" || mine != "missing") { + continue; + } + if matches!(font_shape, "missing" | "number") && (helv != "missing" || mine != "missing") { + continue; + } + cases += 1; + let what = format!("/DR {dr_shape}, /Font {font_shape}, /Helv {helv}, /Mine {mine}"); + let mut entries = String::new(); + match helv { + "inline" => entries += &format!("/Helv {} ", font("Arial")), + "object" => entries += "/Helv 7 0 R ", + "dangling" => entries += "/Helv 99 0 R ", + "null" => entries += "/Helv null ", + "number" => entries += "/Helv 3 ", + _ => {} + } + match mine { + "inline" => entries += &format!("/Mine {} ", font("Symbol")), + "object" => entries += "/Mine 8 0 R ", + _ => {} + } + let fonts_value = match font_shape { + "inline" => format!("<< {entries}>>"), + "object" => "6 0 R".into(), + "number" => "7".into(), + _ => String::new(), + }; + let dr_dict = if fonts_value.is_empty() { "<< >>".to_string() } else { format!("<< /Font {fonts_value} >>") }; + let dr_value = match dr_shape { + "inline" => dr_dict.clone(), + "object" => "5 0 R".into(), + _ => String::new(), + }; + let af = if dr_value.is_empty() { "<< /Fields [] >>".to_string() } else { format!("<< /Fields [] /DR {dr_value} >>") }; + let mut doc = 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 >>".into(), + af, + if dr_shape == "object" { dr_dict } else { "null".into() }, + if font_shape == "object" { format!("<< {entries}>>") } else { "null".into() }, + font("Helvetica-Oblique"), + font("ZapfDingbats"), + ]); + add_field(&mut doc, 0, [40.0, 600.0, 300.0, 630.0], &NewField::Text { multiline: false }, Some("name")) + .unwrap_or_else(|e| panic!("{what}: {e}")); + let doc = reopen(&doc); + let af = acroform(&doc).unwrap(); + let dr = doc.resolve(af.get(b"DR").unwrap_or_else(|| panic!("{what}: no /DR"))); + let fonts = dr_fonts(&doc); + for (name, value) in fonts.iter() { + assert!(value.as_dict().is_none(), "{what}: {} is inline", String::from_utf8_lossy(name)); + } + for name in ["Helv", "TiRo", "Cour"] { + assert!(fonts.contains(name.as_bytes()), "{what}: no {name}"); + } + let base = + |name: &[u8]| fonts.reference(name).and_then(|r| doc.get(r).as_dict().and_then(|f| f.name(b"BaseFont").map(<[u8]>::to_vec))); + match helv { + "inline" => assert_eq!(base(b"Helv").as_deref(), Some(&b"Arial"[..]), "{what}"), + "object" => assert_eq!(fonts.reference(b"Helv"), Some(pdfcraft_cos::ObjRef::new(7, 0)), "{what}"), + "dangling" => assert_eq!(fonts.reference(b"Helv"), Some(pdfcraft_cos::ObjRef::new(99, 0)), "{what}"), + "null" => assert_eq!(fonts.get(b"Helv"), Some(&Object::Null), "{what}"), + "number" => assert_eq!(fonts.get(b"Helv"), Some(&Object::Int(3)), "{what}"), + _ => assert_eq!(base(b"Helv").as_deref(), Some(&b"Helvetica"[..]), "{what}"), + } + match mine { + "inline" => assert_eq!(base(b"Mine").as_deref(), Some(&b"Symbol"[..]), "{what}"), + "object" => assert_eq!(fonts.reference(b"Mine"), Some(pdfcraft_cos::ObjRef::new(8, 0)), "{what}"), + _ => assert!(!fonts.contains(b"Mine"), "{what}"), + } + if dr_shape == "object" { + assert_eq!(af.reference(b"DR"), Some(pdfcraft_cos::ObjRef::new(5, 0)), "{what}: /DR stays its object"); + } + if font_shape == "object" { + assert_eq!(dr.as_dict().unwrap().reference(b"Font"), Some(pdfcraft_cos::ObjRef::new(6, 0)), "{what}: /Font stays its object"); + } + } + } + } + } + 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()); +} + +/// #3441 (review follow-up): a signature value held by a parent field signs its kid too (`/V` is +/// inheritable, ISO 32000-2 Table 226), so the form still counts as signed and keeps its `/DR`. +#[test] +fn a_signature_value_on_a_parent_field_keeps_the_form_fonts_as_signed() { + let mut doc = 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 8 0 R] >>".into(), + "<< /Fields [5 0 R 6 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >> >> >> >>".into(), + "<< /Type /Annot /Subtype /Widget /FT /Tx /T (name) /DA (/Helv 12 Tf 0 g) /Rect [50 700 300 720] /P 3 0 R >>".into(), + "<< /FT /Sig /T (sig) /V 7 0 R /Kids [8 0 R] >>".into(), + "<< /Type /Sig /Filter /Adobe.PPKLite /Contents <00> >>".into(), + "<< /Type /Annot /Subtype /Widget /T (s1) /Parent 6 0 R /Rect [0 0 0 0] /P 3 0 R >>".into(), + ]); + let kid = pdfcraft_cos::ObjRef::new(8, 0); + assert!(fields(&doc).iter().any(|f| f.obj == kid && f.kind == FieldKind::Signature), "the kid is the signature field"); + 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()), "an inherited signature value keeps /DR as it was"); + assert_eq!(field(&fields(&doc), "name").value, ["Ada"]); +} + +/// #3441 (review follow-up): a catalog that holds `/AcroForm` as a direct dictionary gets its +/// inline `/DR` fonts converted too when a field is filled. +#[test] +fn filling_a_field_converts_inline_fonts_of_a_direct_form_dictionary() { + let mut doc = document(&[ + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >> >> >> >> >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(), + "<< /Type /Page /Parent 2 0 R /Annots [4 0 R] >>".into(), + "<< /Type /Annot /Subtype /Widget /FT /Tx /T (name) /DA (/Helv 12 Tf 0 g) /Rect [50 700 300 720] /P 3 0 R >>".into(), + ]); + 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"]); +} diff --git a/crates/xfa/src/pdf.rs b/crates/xfa/src/pdf.rs index 8e0c47d88..1fcb904e0 100644 --- a/crates/xfa/src/pdf.rs +++ b/crates/xfa/src/pdf.rs @@ -769,6 +769,13 @@ pub fn write_form(doc: &mut Document, form: &Form) -> Result z.set(b"BaseFont".to_vec(), Object::name("ZapfDingbats")); dr_fonts.set(b"ZaDb".to_vec(), Object::Dict(z)); } + // Each font its own object: Acrobat shows nothing in a text field it redraws after typing + // when the field's /DR font is a dictionary written inline (#3441). + let inline: Vec<(Vec, Dict)> = dr_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)); + dr_fonts.set(name, Object::Ref(r)); + } dr.set(b"Font".to_vec(), Object::Dict(dr_fonts)); acro.set(b"DR".to_vec(), Object::Dict(dr)); if !acro.contains(b"DA") { diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 411529b17..3ffb115aa 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use pdfcraft_cos::{Document, Object, SaveOptions, write_incremental}; +use pdfcraft_cos::{Dict, Document, Object, SaveOptions, write_incremental}; use super::*; use crate::data::{ @@ -324,6 +324,12 @@ fn rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields() { let dr = doc.resolve(acro.get(b"DR").unwrap()); let fonts = doc.resolve(dr.as_dict().unwrap().get(b"Font").unwrap()); assert!(fonts.as_dict().unwrap().contains(b"Cour") && fonts.as_dict().unwrap().contains(b"ZaDb")); + // Each one its own object: Acrobat shows nothing in a text field it redraws after typing + // when the /DR font is written inline (#3441). + for (name, font) in fonts.as_dict().unwrap().iter() { + let r = font.as_ref().unwrap_or_else(|| panic!("{} is an object", String::from_utf8_lossy(name))); + assert_eq!(doc.get(r).as_dict().and_then(|f| f.name(b"Type")), Some(&b"Font"[..])); + } // A field carries its SOM path for data round trips. let first = doc.resolve(&doc.resolve(acro.get(b"Fields").unwrap()).as_array().unwrap()[0]); assert!(first.as_dict().unwrap().contains(SOM_KEY)); @@ -2832,3 +2838,39 @@ fn long_names_repeated_by_an_index_are_capped() { let data = build_data(&doc, &fields); assert!(data.len() < 2_000_000, "{} bytes of data built", data.len()); } + +/// #3441: a font the form already holds as an object stays that object when the XFA layout +/// writes its fields' fonts into `/DR`. +#[test] +fn rendering_keeps_the_forms_own_font_objects() { + let mut doc = Document::open(Arc::new(shell(&template(2)))).unwrap(); + let mut font = Dict::new(); + font.set(b"Type".to_vec(), Object::name("Font")); + font.set(b"Subtype".to_vec(), Object::name("Type1")); + font.set(b"BaseFont".to_vec(), Object::name("Symbol")); + let mine = doc.add(Object::Dict(font)); + let mut fonts = Dict::new(); + fonts.set(b"Mine".to_vec(), Object::Ref(mine)); + let mut dr = Dict::new(); + dr.set(b"Font".to_vec(), Object::Dict(fonts)); + let root = doc.root().unwrap(); + match doc.get(root).as_dict().unwrap().get(b"AcroForm").cloned().unwrap() { + Object::Ref(af) => doc.update_dict(af, |d| d.set(b"DR".to_vec(), Object::Dict(dr))).unwrap(), + _ => doc + .update_dict(root, |c| { + if let Some(Object::Dict(af)) = c.get(b"AcroForm").cloned() { + let mut af = af; + af.set(b"DR".to_vec(), Object::Dict(dr)); + c.set(b"AcroForm".to_vec(), Object::Dict(af)); + } + }) + .unwrap(), + } + render_into(&mut doc).unwrap(); + let catalog = doc.get(root); + let acro = doc.resolve(catalog.as_dict().unwrap().get(b"AcroForm").unwrap()); + let dr = doc.resolve(acro.as_dict().unwrap().get(b"DR").unwrap()); + let fonts = doc.resolve(dr.as_dict().unwrap().get(b"Font").unwrap()); + assert_eq!(fonts.as_dict().unwrap().reference(b"Mine"), Some(mine), "the form's own font object"); + assert!(fonts.as_dict().unwrap().reference(b"Helv").is_some(), "and the layout's fonts as objects"); +}