mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 06:28:49 -06:00
Refactor forms for strict Clippy checks
This commit is contained in:
@@ -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 ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+126
-105
@@ -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<S
|
||||
if let Some(acts) = &props.actions {
|
||||
crate::set_field_actions(doc, name, acts)?;
|
||||
}
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
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<S
|
||||
}
|
||||
}
|
||||
}
|
||||
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())))?;
|
||||
}
|
||||
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<S
|
||||
}
|
||||
})?;
|
||||
}
|
||||
let only = apply_late_widget_props(doc, &f, props, &new_da, rotated_rect)?;
|
||||
redraw_widgets(doc, &new_name, only)?;
|
||||
if props.calculate.is_some() {
|
||||
crate::recalculate(doc)?;
|
||||
}
|
||||
Ok(new_name)
|
||||
}
|
||||
|
||||
fn rename_field(doc: &mut Document, all: &[Field], f: &Field, name: &str, props: &FieldProps) -> Result<String, FormError> {
|
||||
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<Option<[f64; 4]>>,
|
||||
) -> Result<Option<usize>, 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<S
|
||||
if w.obj != f.obj {
|
||||
doc.update_dict(w.obj, |d| {
|
||||
if d.contains(b"DA") {
|
||||
d.set(b"DA".to_vec(), PdfString::literal(new_da.clone().into_bytes()));
|
||||
d.set(b"DA".to_vec(), PdfString::literal(new_da.to_owned().into_bytes()));
|
||||
}
|
||||
})?;
|
||||
}
|
||||
@@ -385,11 +410,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
|
||||
}
|
||||
// A label-only edit must not replace another widget's custom appearances.
|
||||
let only = props.caption.as_ref().filter(|_| *props == (FieldProps { caption: props.caption.clone(), ..Default::default() })).map(|(wi, _)| *wi);
|
||||
redraw_widgets(doc, &new_name, only)?;
|
||||
if props.calculate.is_some() {
|
||||
crate::recalculate(doc)?;
|
||||
}
|
||||
Ok(new_name)
|
||||
Ok(only)
|
||||
}
|
||||
|
||||
/// Name field `old` `new` in each field's Calculate script that reads it
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use super::look::redraw_field;
|
||||
use super::tree::{add_to_page, attach, clash, detach, ensure_form, free_name, name_parts, top_names};
|
||||
use super::*;
|
||||
use super::{
|
||||
Dict, Document, Field, FieldKind, FormError, MIN_FIELD_SIDE, NewField, ObjRef, Object, PdfString, Widget, appearance, fields, flags, invalid,
|
||||
page_refs, rgb,
|
||||
};
|
||||
|
||||
fn widget_dict(page: ObjRef, rect: [f64; 4]) -> Dict {
|
||||
let mut d = Dict::new();
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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<ObjRef, FormError> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
+123
-80
@@ -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
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user