mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 16:36:13 -06:00
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:
co-authored by
Claude Opus 5.5
parent
79fc2e9c71
commit
d8209fcbd2
@@ -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),
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 })?;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user