M6.6: Prepare a form: add text, date, check box, radio (new group or join the selected one), drop-down, list box, button and signature fields with Acrobat's names (Text1, Check Box1, Group1…) by click or drag; fields shown with blue fill and name tags, select/move/resize/Delete; Field Properties (General, Appearance font size, Position, Options); engine AddField/SetFieldProps/DeleteField; form_add_field/form_set_props/form_delete_field tools (54 tools). Fix: check box appearance states were direct streams that read back as null after saving

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-02 00:04:25 -04:00
co-authored by Claude Opus 5.5
parent 79fc2e9c71
commit d8209fcbd2
21 changed files with 1814 additions and 86 deletions
+10 -9
View File
@@ -311,27 +311,28 @@ pub static TOOL_GROUPS: &[ToolGroup] = &[
icon: "text-cursor-input",
hue: PURPLE,
badge: None,
availability: Planned("M6"),
availability: Ready,
sections: &[
ToolSection {
title: "Fields",
items: &[
item("Field list", "list", "form.fields", Ready),
item("Clear form", "eraser", "form.clear", Ready),
item("Field properties", "settings-2", "form.field.properties", Ready),
item("Flatten form fields", "layers", "form.flatten", Ready),
],
},
ToolSection {
title: "Add form components",
items: &[
item("Text field", "text-cursor-input", "form.add.text", Planned("M6")),
item("Checkbox", "check-circle-2", "form.add.checkbox", Planned("M6")),
item("Radio button", "circle", "form.add.radio", Planned("M6")),
item("Drop-down list", "chevron-down", "form.add.combo", Planned("M6")),
item("List box", "list", "form.add.list", Planned("M6")),
item("Button", "square", "form.add.button", Planned("M6")),
item("Date field", "clock-3", "form.add.date", Planned("M6")),
item("Digital signature", "signature", "form.add.signature", Planned("M6")),
item("Text field", "text-cursor-input", "form.add.text", Ready),
item("Checkbox", "check-circle-2", "form.add.checkbox", Ready),
item("Radio button", "circle", "form.add.radio", Ready),
item("Drop-down list", "chevron-down", "form.add.combo", Ready),
item("List box", "list", "form.add.list", Ready),
item("Button", "square", "form.add.button", Ready),
item("Date field", "clock-3", "form.add.date", Ready),
item("Digital signature", "signature", "form.add.signature", Ready),
],
},
],
+10
View File
@@ -174,6 +174,16 @@ pub const COMMANDS: &[CommandSpec] = &[
c("form.clear", "Clear form", EDIT, None, FillForms, "eraser"),
c("comment.flatten", "Flatten comments", None, None, HasComments, "layers"),
c("form.flatten", "Flatten form fields", None, None, HasFields, "layers"),
c("form.prepare", "Prepare a form", None, None, Modification, "text-cursor-input"),
c("form.field.properties", "Field properties…", None, None, HasFields, "settings-2"),
c("form.add.text", "Add a text field", None, None, Modification, "text-cursor-input"),
c("form.add.checkbox", "Add a checkbox", None, None, Modification, "check-circle-2"),
c("form.add.radio", "Add a radio button", None, None, Modification, "circle"),
c("form.add.combo", "Add a drop-down list", None, None, Modification, "chevron-down"),
c("form.add.list", "Add a list box", None, None, Modification, "list"),
c("form.add.button", "Add a button", None, None, Modification, "square"),
c("form.add.date", "Add a date field", None, None, Modification, "clock-3"),
c("form.add.signature", "Add a digital signature field", None, None, Modification, "signature"),
c("sign.fill.text", "Fill & Sign: add text", None, None, Annotate, "type"),
c("sign.fill.check", "Fill & Sign: checkmark", None, None, Annotate, "check"),
c("sign.fill.cross", "Fill & Sign: cross", None, None, Annotate, "x"),
+33 -1
View File
@@ -22,7 +22,9 @@ pub use printcraft_organize::LabelStyle;
pub use printcraft_cos::Algorithm;
pub use printcraft_edit::{Background, HeaderFooter, MarkKind, Watermark};
pub use printcraft_forms::{Field as FormField, FieldKind as FormFieldKind, FieldValue, Widget as FormWidget, flags as field_flags};
pub use printcraft_forms::{
Field as FormField, FieldKind as FormFieldKind, FieldProps, FieldValue, NewField, Widget as FormWidget, flags as field_flags,
};
/// Comment geometry helpers (text-box line breaking) for frontends.
pub use printcraft_annot::appearance as annot_text;
@@ -498,6 +500,23 @@ pub enum Edit {
ResetForm {
names: Option<Vec<String>>,
},
/// Prepare a form: add a field on a page (`rect` in PDF user space); `name` defaults to
/// Acrobat's next free name ("Text1", "Check Box2", …).
AddField {
page: usize,
rect: [f64; 4],
kind: NewField,
name: Option<String>,
},
/// Field Properties ▸ General / Options.
SetFieldProps {
name: String,
props: FieldProps,
},
/// Delete a field and all its widgets.
DeleteField {
name: String,
},
/// Add a header and footer (with `replace`, existing ones on those pages go first).
AddHeaderFooter {
pages: Vec<usize>,
@@ -564,6 +583,9 @@ impl Edit {
Edit::StyleAnnotation { .. } | Edit::SetAnnotationInfo { .. } => "Change comment properties".into(),
Edit::SetFieldValue { name, .. } => format!("Fill in {name}"),
Edit::ResetForm { .. } => "Clear form".into(),
Edit::AddField { .. } => "Add field".into(),
Edit::SetFieldProps { .. } => "Change field properties".into(),
Edit::DeleteField { .. } => "Delete field".into(),
Edit::AddHeaderFooter { replace: false, .. } => "Add header & footer".into(),
Edit::AddHeaderFooter { .. } => "Update header & footer".into(),
Edit::AddWatermark { replace: false, .. } => "Add watermark".into(),
@@ -671,6 +693,9 @@ fn check_permission(edit: &Edit, p: &printcraft_cos::Permissions) -> Result<(),
| Edit::AddWatermark { .. }
| Edit::AddBackground { .. }
| Edit::RemoveMarks { .. }
| Edit::AddField { .. }
| Edit::SetFieldProps { .. }
| Edit::DeleteField { .. }
| Edit::Flatten { .. } => {
if p.modify() {
Ok(())
@@ -799,6 +824,13 @@ fn run_edit(doc: &mut printcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -
Edit::ResetForm { names } => {
printcraft_forms::reset(doc, names.as_deref())?;
}
Edit::AddField { page, rect, kind, name } => {
printcraft_forms::add_field(doc, *page, *rect, kind, name.as_deref())?;
}
Edit::SetFieldProps { name, props } => {
printcraft_forms::set_props(doc, name, props)?;
}
Edit::DeleteField { name } => printcraft_forms::delete_field(doc, name)?,
Edit::AddHeaderFooter { pages, settings, replace } => {
let date = cx.today;
printcraft_edit::add_header_footer(doc, pages, settings, *replace, &printcraft_edit::Context { date })?;
+28
View File
@@ -662,6 +662,34 @@ fn form_edits_refresh_field_values_without_a_full_inspection() {
assert_eq!(values(&d.info.fields), [("name".to_string(), Some("Ada".to_string())), ("ok".to_string(), Some("Yes".to_string()))]);
}
#[test]
fn preparing_a_form_adds_renames_and_deletes_fields() {
let (mut s, id) = session_with(1);
assert!(s.get(id).unwrap().form.is_empty());
let add = |kind: NewField, y: f64| Edit::AddField { page: 0, rect: [20.0, y, 180.0, y + 22.0], kind, name: None };
s.apply(id, add(NewField::Text { multiline: false }, 250.0)).unwrap();
s.apply(id, add(NewField::CheckBox, 200.0)).unwrap();
s.apply(id, add(NewField::Text { multiline: false }, 150.0)).unwrap();
let names = |s: &Session| s.get(id).unwrap().form.iter().map(|f| f.name.clone()).collect::<Vec<_>>();
assert_eq!(names(&s), ["Text1", "Check Box1", "Text2"]);
assert_eq!(s.get(id).unwrap().can_undo(), Some("Add field"));
let props = FieldProps { name: Some("email".into()), tooltip: Some("Your e-mail".into()), required: Some(true), ..Default::default() };
s.apply(id, Edit::SetFieldProps { name: "Text2".into(), props }).unwrap();
s.apply(id, Edit::DeleteField { name: "Check Box1".into() }).unwrap();
assert_eq!(names(&s), ["Text1", "email"]);
s.apply(id, Edit::SetFieldValue { name: "email".into(), value: FieldValue::Text("ada@example.org".into()) }).unwrap();
let saved = s.save_bytes(id).unwrap();
let mut s2 = Session::new();
let id2 = s2.open("f.pdf", None, saved, None).unwrap();
let d = s2.get(id2).unwrap();
assert_eq!(d.form.iter().map(|f| f.name.as_str()).collect::<Vec<_>>(), ["Text1", "email"]);
assert_eq!(d.form[1].value, ["ada@example.org"]);
assert!(page_texts(&s2, id2).iter().any(|t| t.contains("ada@example.org")));
s.undo(id).unwrap();
s.undo(id).unwrap();
assert_eq!(names(&s), ["Text1", "Check Box1", "email"]);
}
#[test]
fn crop_and_duplicate_pages_show_in_the_viewer() {
let (mut s, id) = session_with(2);