diff --git a/crates/forms/src/af/script.rs b/crates/forms/src/af/script.rs index c807f2d7d7..9aed297b45 100644 --- a/crates/forms/src/af/script.rs +++ b/crates/forms/src/af/script.rs @@ -1,6 +1,6 @@ use super::calc::{js_tokens, notation_js, notation_range}; use super::format::{DATE_PRESETS, TIME_PRESETS}; -use super::*; +use super::{CalcOp, Calculate, Format, Validate}; // ── parsing scripts ───────────────────────────────────────────────────────────────────────── diff --git a/crates/forms/src/appearances.rs b/crates/forms/src/appearances.rs index d035feb4ef..64569fa3ca 100644 --- a/crates/forms/src/appearances.rs +++ b/crates/forms/src/appearances.rs @@ -1,6 +1,6 @@ use super::read::{acroform, fields}; use super::values::merged_appearance; -use super::*; +use super::{Dict, Document, FieldKind, ObjRef, Object, Widget, appearance, name_bytes}; /// Whether the form asks viewers to draw its fields' appearances themselves (`/NeedAppearances`). pub fn need_appearances(doc: &Document) -> bool { diff --git a/crates/forms/src/author/edit.rs b/crates/forms/src/author/edit.rs index dc7d91d4fc..d61cfe28f2 100644 --- a/crates/forms/src/author/edit.rs +++ b/crates/forms/src/author/edit.rs @@ -1,6 +1,6 @@ use super::look::{BorderStyle, da_string, patch_da, patch_widget_look, redraw_widgets}; use super::tree::{attach, check_place, child_named, clash, detach, ensure_form, inherited, name_parts, own_inherited}; -use super::*; +use super::{Dict, Document, Field, FieldKind, FieldProps, FormError, MIN_FIELD_SIDE, ObjRef, Object, PdfString, fields, flags, invalid}; /// Swap width and height, keeping the center. `None` when that is not a usable widget. fn swapped_around_center(r: [f64; 4]) -> Option<[f64; 4]> { @@ -70,59 +70,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result own, - _ => n, - }; - if n.contains('.') { - // A full name: the field moves under the parent fields it names (#2696). - let parts = name_parts(n)?; - if let Some(why) = all.iter().filter(|x| x.obj != f.obj).find_map(|x| clash(n, &x.name)) { - return invalid(why); - } - let af = ensure_form(doc)?; - // Checked before anything changes, so a refused move leaves the field as it was. - let add = inherited(doc, f.obj); - check_place(doc, af, f.obj, &parts, &add)?; - // From here a failure (a write that `check_place` couldn't foresee) would leave the - // field half moved in `doc`. The engine runs every edit on a copy of the document and - // keeps it only when the edit succeeds (`Session::apply`), so the open document never - // sees that. - own_inherited(doc, f.obj, &add)?; - detach(doc, f.obj)?; - attach(doc, af, f.obj, &parts)?; - new_name = n.to_string(); - } else { - if n.is_empty() { - return invalid("field names can't be empty"); - } - // Rename the terminal part (the field's own /T). The parents' part stays, so the name - // that must be free is the full one: `MASTER.a` renamed to `b` takes `MASTER.b` (ISO - // 32000-2 §12.7.4.2). The field itself is no clash when it keeps its own name. - let prefix = f.name.rsplit_once('.').map(|(p, _)| format!("{p}.")).unwrap_or_default(); - new_name = format!("{prefix}{n}"); - if let Some(why) = all.iter().filter(|x| x.obj != f.obj).find_map(|x| clash(&new_name, &x.name)) { - return invalid(why); - } - // A node of that name beside it that isn't a terminal field, such as a parent - // field left empty by a move. - let af = ensure_form(doc)?; - let parent = doc.get(f.obj).as_dict().and_then(|d| d.reference(b"Parent")); - if child_named(doc, af, parent, n).is_some_and(|r| r != f.obj) { - return invalid(format!("a field named {new_name:?} already exists")); - } - doc.update_dict(f.obj, |d| d.set(b"T".to_vec(), PdfString::text(n)))?; - } - // Calculations read the field by its new name, unless another field keeps the old one. - if all.iter().filter(|x| x.name == f.name).count() == 1 { - rename_in_calculations(doc, &all, &f.name, &new_name)?; - } - } + let new_name = rename_field(doc, &all, &f, name, props)?; // The new default appearance when the size or the look changes. let size = props.font_size.unwrap_or_else(|| crate::appearance::parse_da(&f.da).size); let new_da = match &props.look { @@ -248,51 +196,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result mk.set(b"BC".to_vec(), arr(c)), - None => { - mk.remove(b"BC"); - } - } - match l.fill { - Some(c) => mk.set(b"BG".to_vec(), arr(c)), - None => { - mk.remove(b"BG"); - } - } - d.set(b"MK".to_vec(), Object::Dict(mk)); - let mut bs = Dict::new(); - bs.set(b"W".to_vec(), Object::Real(l.width.clamp(0.0, 12.0))); - bs.set(b"S".to_vec(), Object::name(l.style.code())); - if l.style == BorderStyle::Dashed { - bs.set(b"D".to_vec(), Object::Array(vec![Object::Int(3)])); - } - d.set(b"BS".to_vec(), Object::Dict(bs)); - })?; - } - } - if let Some(style) = props.check_style { - for w in &f.widgets { - // Resolved first: an indirect /MK keeps its colours. - let mut mk = doc.get(w.obj).as_dict().and_then(|d| d.get(b"MK").map(|m| doc.resolve(m).as_dict().cloned())).flatten().unwrap_or_default(); - mk.set(b"CA".to_vec(), PdfString::text(style.code())); - doc.update_dict(w.obj, |d| d.set(b"MK".to_vec(), Object::Dict(mk)))?; - } - } - if let Some((wi, r)) = props.rect { - let w = f.widgets.get(wi).ok_or_else(|| FormError::Invalid(format!("{name} has no widget {}", wi + 1)))?; - let r = [r[0].min(r[2]), r[1].min(r[3]), r[0].max(r[2]), r[1].max(r[3])]; - if !r.iter().all(|v| v.is_finite()) || r[2] - r[0] < MIN_FIELD_SIDE || r[3] - r[1] < MIN_FIELD_SIDE { - return invalid("the field is too small"); - } - doc.update_dict(w.obj, |d| d.set(b"Rect".to_vec(), Object::Array(r.iter().map(|v| Object::Real(*v)).collect())))?; - } + apply_widget_style_props(doc, &f, name, props)?; if props.format.is_some() || props.validate.is_some() || props.calculate.is_some() { if !matches!(f.kind, FieldKind::Text | FieldKind::Combo) { return invalid("only text fields and drop-down lists have format, validate and calculate scripts"); @@ -341,6 +245,127 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result Result { + let mut new_name = name.to_string(); + if let Some(n) = props.name.as_deref().map(str::trim).filter(|n| *n != f.name) { + // A full name under the same parents only renames the field's own part (the editor + // shows nested fields by their full name): it isn't moved, so it keeps its place and + // goes on inheriting from its parents. + let n = match (n.rsplit_once('.'), f.name.rsplit_once('.')) { + (Some((parents, own)), Some((current, _))) if parents == current => own, + _ => n, + }; + if n.contains('.') { + // A full name: the field moves under the parent fields it names (#2696). + let parts = name_parts(n)?; + if let Some(why) = all.iter().filter(|x| x.obj != f.obj).find_map(|x| clash(n, &x.name)) { + return invalid(why); + } + let af = ensure_form(doc)?; + // Checked before anything changes, so a refused move leaves the field as it was. + let add = inherited(doc, f.obj); + check_place(doc, af, f.obj, &parts, &add)?; + // From here a failure (a write that `check_place` couldn't foresee) would leave the + // field half moved in `doc`. The engine runs every edit on a copy of the document and + // keeps it only when the edit succeeds (`Session::apply`), so the open document never + // sees that. + own_inherited(doc, f.obj, &add)?; + detach(doc, f.obj)?; + attach(doc, af, f.obj, &parts)?; + new_name = n.to_string(); + } else { + if n.is_empty() { + return invalid("field names can't be empty"); + } + // Rename the terminal part (the field's own /T). The parents' part stays, so the name + // that must be free is the full one: `MASTER.a` renamed to `b` takes `MASTER.b` (ISO + // 32000-2 §12.7.4.2). The field itself is no clash when it keeps its own name. + let prefix = f.name.rsplit_once('.').map(|(p, _)| format!("{p}.")).unwrap_or_default(); + new_name = format!("{prefix}{n}"); + if let Some(why) = all.iter().filter(|x| x.obj != f.obj).find_map(|x| clash(&new_name, &x.name)) { + return invalid(why); + } + // A node of that name beside it that isn't a terminal field, such as a parent + // field left empty by a move. + let af = ensure_form(doc)?; + let parent = doc.get(f.obj).as_dict().and_then(|d| d.reference(b"Parent")); + if child_named(doc, af, parent, n).is_some_and(|r| r != f.obj) { + return invalid(format!("a field named {new_name:?} already exists")); + } + doc.update_dict(f.obj, |d| d.set(b"T".to_vec(), PdfString::text(n)))?; + } + // Calculations read the field by its new name, unless another field keeps the old one. + if all.iter().filter(|x| x.name == f.name).count() == 1 { + rename_in_calculations(doc, all, &f.name, &new_name)?; + } + } + Ok(new_name) +} + +fn apply_widget_style_props(doc: &mut Document, f: &Field, name: &str, props: &FieldProps) -> Result<(), FormError> { + if let Some(l) = &props.look { + let arr = |c: [f64; 3]| Object::Array(c.iter().map(|v| Object::Real(v.clamp(0.0, 1.0))).collect()); + for w in &f.widgets { + // Resolved first: an indirect /MK keeps its mark, rotation and other entries. + let mut mk = doc.get(w.obj).as_dict().and_then(|d| d.get(b"MK").map(|m| doc.resolve(m).as_dict().cloned())).flatten().unwrap_or_default(); + doc.update_dict(w.obj, |d| { + match l.border { + Some(c) => mk.set(b"BC".to_vec(), arr(c)), + None => { + mk.remove(b"BC"); + } + } + match l.fill { + Some(c) => mk.set(b"BG".to_vec(), arr(c)), + None => { + mk.remove(b"BG"); + } + } + d.set(b"MK".to_vec(), Object::Dict(mk)); + let mut bs = Dict::new(); + bs.set(b"W".to_vec(), Object::Real(l.width.clamp(0.0, 12.0))); + bs.set(b"S".to_vec(), Object::name(l.style.code())); + if l.style == BorderStyle::Dashed { + bs.set(b"D".to_vec(), Object::Array(vec![Object::Int(3)])); + } + d.set(b"BS".to_vec(), Object::Dict(bs)); + })?; + } + } + if let Some(style) = props.check_style { + for w in &f.widgets { + // Resolved first: an indirect /MK keeps its colours. + let mut mk = doc.get(w.obj).as_dict().and_then(|d| d.get(b"MK").map(|m| doc.resolve(m).as_dict().cloned())).flatten().unwrap_or_default(); + mk.set(b"CA".to_vec(), PdfString::text(style.code())); + doc.update_dict(w.obj, |d| d.set(b"MK".to_vec(), Object::Dict(mk)))?; + } + } + if let Some((wi, r)) = props.rect { + let w = f.widgets.get(wi).ok_or_else(|| FormError::Invalid(format!("{name} has no widget {}", wi + 1)))?; + let r = [r[0].min(r[2]), r[1].min(r[3]), r[0].max(r[2]), r[1].max(r[3])]; + if !r.iter().all(|v| v.is_finite()) || r[2] - r[0] < MIN_FIELD_SIDE || r[3] - r[1] < MIN_FIELD_SIDE { + return invalid("the field is too small"); + } + doc.update_dict(w.obj, |d| d.set(b"Rect".to_vec(), Object::Array(r.iter().map(|v| Object::Real(*v)).collect())))?; + } + Ok(()) +} + +fn apply_late_widget_props( + doc: &mut Document, + f: &Field, + props: &FieldProps, + new_da: &str, + rotated_rect: Option>, +) -> Result, FormError> { // A complete look replaces widget /DA overrides. Partial changes were patched above // against each override, including size-only changes that keep its font and colour. if props.look.is_some() { @@ -348,7 +373,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result Result Dict { let mut d = Dict::new(); diff --git a/crates/forms/src/author/look.rs b/crates/forms/src/author/look.rs index 6044bdc70b..23661ed70a 100644 --- a/crates/forms/src/author/look.rs +++ b/crates/forms/src/author/look.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Dict, Document, Field, FieldKind, FormError, ObjRef, Object, Stream, Widget, appearance, fields, invalid, rgb}; /// The Appearance tab: borders, colours and text. #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/crates/forms/src/author/tree.rs b/crates/forms/src/author/tree.rs index 4f7db4d8e1..d8ca67ffb7 100644 --- a/crates/forms/src/author/tree.rs +++ b/crates/forms/src/author/tree.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Dict, Document, Field, FormError, ObjRef, Object, PdfString, invalid}; /// The AcroForm dictionary's reference, creating the form (with `/Helv` in `/DR`) if needed. pub(crate) fn ensure_form(doc: &mut Document) -> Result { diff --git a/crates/forms/src/read.rs b/crates/forms/src/read.rs index a962af91bd..e689c0e813 100644 --- a/crates/forms/src/read.rs +++ b/crates/forms/src/read.rs @@ -1,5 +1,5 @@ use super::tab_order::rank_tabs; -use super::*; +use super::{DecodeBudget, Dict, Document, Field, FieldKind, ObjRef, Object, PdfString, Widget, af, appearance, flags, name_text, text_of}; /// At most this many `/State` entries of a set-layer-visibility action are read. const MAX_LAYER_STATE: usize = 1024; diff --git a/crates/forms/src/tab_order.rs b/crates/forms/src/tab_order.rs index 9c5c33b694..77234ee094 100644 --- a/crates/forms/src/tab_order.rs +++ b/crates/forms/src/tab_order.rs @@ -1,5 +1,5 @@ use super::read::{fields, page_refs}; -use super::*; +use super::{Document, Field, FormError, ObjRef, Object, invalid}; /// A page's tab order (`/Tabs`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 61dab131e0..d8199f15d3 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -3601,13 +3601,17 @@ fn dotted_field_names_are_made_with_parent_fields() { }) .unwrap(); + invalid_dotted_field_names_are_rejected(&mut doc, &text); +} + +fn invalid_dotted_field_names_are_rejected(doc: &mut Document, text: &NewField) { for bad in ["MASTER", "MASTER.street1.x", "MASTER..x", ".x", "x.", "MASTER. "] { - assert!(add_field(&mut doc, 0, [300.0, 200.0, 450.0, 220.0], &text, Some(bad)).is_err(), "{bad:?}"); - assert!(set_props(&mut doc, "name", &FieldProps { name: Some(bad.into()), ..FieldProps::default() }).is_err(), "{bad:?}"); + assert!(add_field(doc, 0, [300.0, 200.0, 450.0, 220.0], text, Some(bad)).is_err(), "{bad:?}"); + assert!(set_props(doc, "name", &FieldProps { name: Some(bad.into()), ..FieldProps::default() }).is_err(), "{bad:?}"); } let deep = vec!["p"; 33].join("."); - assert!(add_field(&mut doc, 0, [300.0, 200.0, 450.0, 220.0], &text, Some(&deep)).is_err(), "too deep"); - assert!(set_props(&mut doc, "name", &FieldProps { name: Some("MASTER.street1".into()), ..FieldProps::default() }).is_err(), "taken"); + assert!(add_field(doc, 0, [300.0, 200.0, 450.0, 220.0], text, Some(&deep)).is_err(), "too deep"); + assert!(set_props(doc, "name", &FieldProps { name: Some("MASTER.street1".into()), ..FieldProps::default() }).is_err(), "taken"); } /// Review on #2957: a parent left empty by a move keeps its name from the next default name @@ -3801,90 +3805,17 @@ fn existing_form_fonts_are_kept_and_an_unchanged_resource_dictionary_is_not_rewr /// - 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") { + let case = FontShapeCase::new(dr_shape, font_shape, helv, mine); + if !case.is_valid() { 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_font_shape_case(&case); } } } @@ -3892,6 +3823,118 @@ fn every_shape_of_form_fonts_ends_as_objects() { assert_eq!(cases, 1 + 2 * (2 + 2 * 6 * 3), "every shape was tried"); } +struct FontShapeCase<'a> { + dr: &'a str, + fonts: &'a str, + helv: &'a str, + mine: &'a str, + description: String, +} + +impl<'a> FontShapeCase<'a> { + fn new(dr: &'a str, fonts: &'a str, helv: &'a str, mine: &'a str) -> Self { + Self { dr, fonts, helv, mine, description: format!("/DR {dr}, /Font {fonts}, /Helv {helv}, /Mine {mine}") } + } + + fn is_valid(&self) -> bool { + let only_default_dr = self.dr == "missing" && self.fonts == "missing" && self.helv == "missing" && self.mine == "missing"; + let invalid_fonts = matches!(self.fonts, "missing" | "number") && (self.helv != "missing" || self.mine != "missing"); + !(self.dr == "missing" && !only_default_dr || invalid_fonts) + } +} + +fn assert_font_shape_case(case: &FontShapeCase<'_>) { + let mut doc = font_shape_document(case); + add_field(&mut doc, 0, [40.0, 600.0, 300.0, 630.0], &NewField::Text { multiline: false }, Some("name")) + .unwrap_or_else(|e| panic!("{}: {e}", case.description)); + let doc = reopen(&doc); + let acro = acroform(&doc).unwrap(); + let dr = doc.resolve(acro.get(b"DR").unwrap_or_else(|| panic!("{}: no /DR", case.description))); + let fonts = dr_fonts(&doc); + assert_font_entries_are_references(&fonts, &case.description); + assert_required_form_fonts(&fonts, &case.description); + assert_requested_font_shapes(&doc, &acro, &dr, &fonts, case); +} + +fn font_shape_document(case: &FontShapeCase<'_>) -> Document { + let mut entries = String::new(); + match case.helv { + "inline" => entries += &format!("/Helv {} ", form_font("Arial")), + "object" => entries += "/Helv 7 0 R ", + "dangling" => entries += "/Helv 99 0 R ", + "null" => entries += "/Helv null ", + "number" => entries += "/Helv 3 ", + _ => {} + } + match case.mine { + "inline" => entries += &format!("/Mine {} ", form_font("Symbol")), + "object" => entries += "/Mine 8 0 R ", + _ => {} + } + let fonts_value = match case.fonts { + "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 case.dr { + "inline" => dr_dict.clone(), + "object" => "5 0 R".into(), + _ => String::new(), + }; + let acroform = if dr_value.is_empty() { "<< /Fields [] >>".to_string() } else { format!("<< /Fields [] /DR {dr_value} >>") }; + 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(), + acroform, + if case.dr == "object" { dr_dict } else { "null".into() }, + if case.fonts == "object" { format!("<< {entries}>>") } else { "null".into() }, + form_font("Helvetica-Oblique"), + form_font("ZapfDingbats"), + ]) +} + +fn form_font(base: &str) -> String { + format!("<< /Type /Font /Subtype /Type1 /BaseFont /{base} >>") +} + +fn assert_font_entries_are_references(fonts: &Dict, description: &str) { + for (name, value) in fonts.iter() { + assert!(value.as_dict().is_none(), "{description}: {} is inline", String::from_utf8_lossy(name)); + } +} + +fn assert_required_form_fonts(fonts: &Dict, description: &str) { + for name in ["Helv", "TiRo", "Cour"] { + assert!(fonts.contains(name.as_bytes()), "{description}: no {name}"); + } +} + +fn assert_requested_font_shapes(doc: &Document, acro: &Dict, dr: &Object, fonts: &Dict, case: &FontShapeCase<'_>) { + 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 case.helv { + "inline" => assert_eq!(base(b"Helv").as_deref(), Some(&b"Arial"[..]), "{}", case.description), + "object" => assert_eq!(fonts.reference(b"Helv"), Some(pdfcraft_cos::ObjRef::new(7, 0)), "{}", case.description), + "dangling" => assert_eq!(fonts.reference(b"Helv"), Some(pdfcraft_cos::ObjRef::new(99, 0)), "{}", case.description), + "null" => assert_eq!(fonts.get(b"Helv"), Some(&Object::Null), "{}", case.description), + "number" => assert_eq!(fonts.get(b"Helv"), Some(&Object::Int(3)), "{}", case.description), + _ => assert_eq!(base(b"Helv").as_deref(), Some(&b"Helvetica"[..]), "{}", case.description), + } + match case.mine { + "inline" => assert_eq!(base(b"Mine").as_deref(), Some(&b"Symbol"[..]), "{}", case.description), + "object" => assert_eq!(fonts.reference(b"Mine"), Some(pdfcraft_cos::ObjRef::new(8, 0)), "{}", case.description), + _ => assert!(!fonts.contains(b"Mine"), "{}", case.description), + } + if case.dr == "object" { + assert_eq!(acro.reference(b"DR"), Some(pdfcraft_cos::ObjRef::new(5, 0)), "{}: /DR stays its object", case.description); + } + if case.fonts == "object" { + assert_eq!(dr.as_dict().unwrap().reference(b"Font"), Some(pdfcraft_cos::ObjRef::new(6, 0)), "{}: /Font stays its object", case.description); + } +} + /// #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 diff --git a/crates/forms/src/values.rs b/crates/forms/src/values.rs index 9dd79f0a70..37df5502d1 100644 --- a/crates/forms/src/values.rs +++ b/crates/forms/src/values.rs @@ -1,6 +1,9 @@ use super::appearances::{declares_pdfa, need_appearances}; use super::read::{acroform, field_flags, fields, page_refs}; -use super::*; +use super::{ + Dict, Document, Field, FieldEvent, FieldKind, FieldValue, FormError, NoScripts, ObjRef, Object, PdfString, Scripts, af, appearance, author, + calc_order, flags, invalid, name_obj, scripting, +}; // ── writing ─────────────────────────────────────────────────────────────────────────────────