mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-01 22:19:10 -06:00
M6: Prepare a form ▸ Preview (fill the form as a reader would; Edit fields goes back) and Field Properties ▸ Locked (annotation Locked flag; properties greyed out until unlocked, values still fillable); form_set_props takes locked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4df7a73a1a
commit
c0f9c73ae9
@@ -82,6 +82,8 @@ pub struct FieldProps {
|
||||
pub quadding: Option<i64>,
|
||||
/// Default value (`/DV`): what Reset form restores.
|
||||
pub default_value: Option<Option<String>>,
|
||||
/// Lock or unlock the field (its widgets' Locked flag).
|
||||
pub locked: Option<bool>,
|
||||
}
|
||||
|
||||
fn invalid<T>(m: impl Into<String>) -> Result<T, FormError> {
|
||||
@@ -431,7 +433,15 @@ pub fn add_field(doc: &mut Document, page: usize, rect: [f64; 4], kind: &NewFiel
|
||||
kids.push(Object::Ref(widget));
|
||||
d.set(b"Kids".to_vec(), Object::Array(kids));
|
||||
})?;
|
||||
let w = Widget { obj: widget, page: Some(page), rect, on_state: Some(export.clone()), state: Some("Off".into()), tab: usize::MAX };
|
||||
let w = Widget {
|
||||
obj: widget,
|
||||
page: Some(page),
|
||||
rect,
|
||||
on_state: Some(export.clone()),
|
||||
state: Some("Off".into()),
|
||||
tab: usize::MAX,
|
||||
locked: false,
|
||||
};
|
||||
let ap = appearance::check_box_states(doc, &w, FieldKind::Radio, export);
|
||||
doc.update_dict(widget, |d| d.set(b"AP".to_vec(), Object::Dict(ap)))?;
|
||||
}
|
||||
@@ -548,6 +558,20 @@ fn empty_box(doc: &Document, w: &Widget) -> Stream {
|
||||
pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<String, FormError> {
|
||||
let all = fields(doc);
|
||||
let f = all.iter().find(|f| f.name == name).ok_or_else(|| FormError::NoSuchField(name.into()))?.clone();
|
||||
// A locked field only takes unlocking (Acrobat greys its properties out).
|
||||
if f.locked() && props.locked != Some(false) && *props != (FieldProps { locked: props.locked, ..FieldProps::default() }) {
|
||||
return invalid(format!("{name} is locked: unlock it first"));
|
||||
}
|
||||
if let Some(lock) = props.locked {
|
||||
for w in &f.widgets {
|
||||
let flags = doc.get(w.obj).as_dict().and_then(|d| d.get(b"F").and_then(|v| doc.resolve(v).as_int())).unwrap_or(0);
|
||||
let v = if lock { flags | 128 } else { flags & !128 };
|
||||
doc.update_dict(w.obj, |d| d.set(b"F".to_vec(), Object::Int(v)))?;
|
||||
}
|
||||
if *props == (FieldProps { locked: props.locked, ..FieldProps::default() }) {
|
||||
return Ok(name.to_string());
|
||||
}
|
||||
}
|
||||
let mut new_name = name.to_string();
|
||||
if let Some(n) = props.name.as_deref().map(str::trim).filter(|n| *n != f.name) {
|
||||
if n.is_empty() || n.contains('.') {
|
||||
|
||||
@@ -84,6 +84,8 @@ pub struct Widget {
|
||||
/// Position in the document's tab order (pages in order; within a page by its `/Tabs`:
|
||||
/// rows, columns, or annotation/structure order). `usize::MAX` when not on a page.
|
||||
pub tab: usize,
|
||||
/// The widget's Locked flag (`/F` bit 8): its properties can't be changed.
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
/// A terminal form field.
|
||||
@@ -122,6 +124,11 @@ impl Field {
|
||||
self.has(flags::READ_ONLY)
|
||||
}
|
||||
|
||||
/// Locked (Field Properties ▸ General ▸ Locked): its properties can't be changed.
|
||||
pub fn locked(&self) -> bool {
|
||||
self.widgets.iter().any(|w| w.locked)
|
||||
}
|
||||
|
||||
/// The value as one string: text, the state name, or the selected display texts.
|
||||
pub fn display_value(&self) -> String {
|
||||
match self.kind {
|
||||
@@ -535,6 +542,7 @@ fn walk(
|
||||
on_state: on_state.filter(|_| matches!(kind, FieldKind::CheckBox | FieldKind::Radio)),
|
||||
state: wd.name(b"AS").map(|s| String::from_utf8_lossy(s).into_owned()),
|
||||
tab: usize::MAX,
|
||||
locked: wd.get(b"F").and_then(|f| doc.resolve(f).as_int()).unwrap_or(0) & 128 != 0,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -594,3 +594,17 @@ fn duplicating_a_field_across_pages_shares_its_value() {
|
||||
assert_eq!(fields(&doc).len(), 1);
|
||||
assert!(duplicate_field(&mut doc.clone(), &name, &[7]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locked_fields_only_take_unlocking() {
|
||||
let mut doc = one_page();
|
||||
let name = add_field(&mut doc, 0, [50.0, 700.0, 250.0, 720.0], &NewField::Text { multiline: false }, Some("ID")).unwrap();
|
||||
set_props(&mut doc, &name, &FieldProps { locked: Some(true), ..FieldProps::default() }).unwrap();
|
||||
assert!(fields(&doc)[0].locked());
|
||||
assert!(set_props(&mut doc, &name, &FieldProps { required: Some(true), ..FieldProps::default() }).is_err(), "locked");
|
||||
// Values can still be filled in.
|
||||
set_value(&mut doc, &name, &FieldValue::Text("42".into())).unwrap();
|
||||
set_props(&mut doc, &name, &FieldProps { locked: Some(false), required: Some(true), ..FieldProps::default() }).unwrap();
|
||||
let f = &fields(&doc)[0];
|
||||
assert!(!f.locked() && f.has(flags::REQUIRED));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user