Merge PR #3488: fix(forms): write each /DR font as its own object, so Acrobat keeps typed text (#3441)

crates/forms/src/tests.rs: append conflict, kept both tests.
This commit is contained in:
aiongg
2026-10-24 06:21:22 +00:00
5 changed files with 452 additions and 7 deletions
+93 -6
View File
@@ -134,10 +134,9 @@ 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();
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<ObjRef, FormError> {
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<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 && 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 {
+1
View File
@@ -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.
+308
View File
@@ -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<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 >>".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"]);
}
+7
View File
@@ -769,6 +769,13 @@ pub fn write_form(doc: &mut Document, form: &Form) -> Result<Written, XfaError>
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<u8>, 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") {
+43 -1
View File
@@ -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");
}