Fix #5211: persist JavaScript field colours (#5499)

* Fix #5211: persist JavaScript field colours

* Read current document state in field scripts

* Redraw form fields after validation changes

* Format validation redraw regression test

* Keep field list alive in redraw test

* Refresh fields only after same-field scripts

* Format conditional field refresh

* Borrow accepted field value for storage

* Redraw calculated fields from current appearance

* Format JavaScript field state merge

* Restore JavaScript runner document state

* Group JavaScript runner form inputs

* Read current field display state in scripts

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
This commit is contained in:
Harrison BoothandFredApps authored and GitHub committed 2026-11-10 23:43:44 +01:00
1 parent d0b17ef95f
commit 832a51e6d7
8 files changed
+335 -20

No files matched your search

+2 -2
View File
@@ -452,8 +452,8 @@ pub enum Edit {
RemoveStandardId {
standard: Standard,
},
/// What a script (button or console) changed in form fields: values, read-only, required
/// and visibility.
/// What a script (button or console) changed in form fields: values, appearance colours,
/// read-only, required and visibility.
ApplyScriptChanges {
changes: Vec<pdfcraft_forms::FieldChange>,
},
+68 -6
View File
@@ -208,9 +208,22 @@ fn display(doc: &pdfcraft_cos::Document, f: &Field) -> i32 {
/// A form field as scripts see it.
pub fn field_state(doc: &pdfcraft_cos::Document, f: &Field) -> pdfcraft_js::FieldState {
field_state_with_display(f, display(doc, f))
let mut s = field_state_with_display(f, display(doc, f));
(s.text_color, s.fill_color) = colours(doc, f);
s
}
/// `field.textColor` and `field.fillColor` as Acrobat colour arrays, from the field's look
/// (`None` for a transparent fill, which scripts read as `["T"]`).
fn colours(doc: &pdfcraft_cos::Document, f: &Field) -> Colours {
let rgb = |c: [f64; 3]| std::iter::once("RGB".to_string()).chain(c.map(|v| v.to_string())).collect::<Vec<_>>();
let look = pdfcraft_forms::look(doc, f);
(Some(rgb(look.text)), look.fill.map(rgb))
}
/// A field's text and fill colours as scripts read them.
type Colours = (Option<Vec<String>>, Option<Vec<String>>);
fn field_state_with_display(f: &Field, display: i32) -> pdfcraft_js::FieldState {
let mut s = pdfcraft_js::FieldState::new(f.name.clone(), kind(f.kind), f.value.clone());
s.default = f.default.clone();
@@ -255,9 +268,18 @@ fn changes(
display: (a.display != b.display).then_some(a.display),
caption: a.caption_was_set.then(|| a.caption.clone()).flatten(),
rect: (a.rect != b.rect).then_some(a.rect).flatten(),
text_color: (a.text_color != b.text_color).then(|| a.text_color.clone()).flatten(),
fill_color: (a.fill_color != b.fill_color).then(|| a.fill_color.clone()).flatten(),
};
(c.value.is_some() || c.read_only.is_some() || c.required.is_some() || c.display.is_some() || c.caption.is_some() || c.rect.is_some())
.then_some(c)
(c.value.is_some()
|| c.read_only.is_some()
|| c.required.is_some()
|| c.display.is_some()
|| c.caption.is_some()
|| c.rect.is_some()
|| c.text_color.is_some()
|| c.fill_color.is_some())
.then_some(c)
})
.collect()
}
@@ -274,6 +296,14 @@ pub struct JsRunner {
// otherwise reparse every widget for every calculation. Values/flags still come from
// the live `fields` snapshot on each call. A new edit gets a new runner and cache.
displays: std::collections::HashMap<pdfcraft_cos::ObjRef, i32>,
/// Field colours read from `cos`, cached for the same reason as `displays`.
colours: std::collections::HashMap<pdfcraft_cos::ObjRef, Colours>,
/// Colours this runner's scripts wrote, by field name: they reach the document only once
/// the edit applies each script's changes, after `cos` was taken, so a later script in the
/// same edit reads them from here.
written_colours: std::collections::HashMap<String, Colours>,
/// Whether a document-level script names a colour property (`fillColor`, `textColor`, …).
doc_scripts_use_colours: bool,
calculations: Option<std::sync::Arc<Vec<(String, String)>>>,
}
@@ -300,7 +330,18 @@ impl JsRunner {
// Packets alone are not enough: static XFA and an unlaid-out shell keep their scripts.
let xfa_active = xfa && pdfcraft_xfa::existing_layout(cos).is_some();
let doc_scripts = if xfa_active { Vec::new() } else { pdfcraft_forms::document_scripts(cos) };
JsRunner { doc, doc_scripts, output: JsOutput::default(), cos: cos.clone(), displays: std::collections::HashMap::new(), calculations: None }
let doc_scripts_use_colours = doc_scripts.iter().any(|source| source.contains("Color"));
JsRunner {
doc,
doc_scripts,
output: JsOutput::default(),
cos: cos.clone(),
displays: std::collections::HashMap::new(),
colours: std::collections::HashMap::new(),
written_colours: std::collections::HashMap::new(),
doc_scripts_use_colours,
calculations: None,
}
}
fn calculations(&mut self, fields: &[Field]) -> std::sync::Arc<Vec<(String, String)>> {
@@ -315,6 +356,8 @@ impl JsRunner {
pub(crate) fn invalidate_calculations(&mut self, cos: &pdfcraft_cos::Document) {
self.cos = cos.clone();
self.displays.clear();
self.colours.clear();
self.written_colours.clear();
self.calculations = None;
}
}
@@ -399,6 +442,12 @@ impl JsRunner {
/// [`Scripts::run`], keeping what the script showed and asked for only when `log`; `None`
/// when the script failed.
fn run_logged(&mut self, event: FieldEvent, script: &str, target: &Field, value: &str, fields: &[Field], log: bool) -> Option<ScriptResult> {
// Field colours are read (and their arrays built for every field) only for a script
// that can reach them: one that names a colour property, or any script when a document
// script does. A large form's calculations then cost what they did before colours were
// readable. A property name built at run time (`f["fill" + "Color"]`) reads as
// transparent in a script that names no colour.
let reads_colours = self.doc_scripts_use_colours || script.contains("Color");
let states: Vec<_> = fields
.iter()
.map(|f| {
@@ -406,7 +455,14 @@ impl JsRunner {
Some(w) => *self.displays.entry(w.obj).or_insert_with(|| display(&self.cos, f)),
None => pdfcraft_js::DISPLAY_VISIBLE,
};
field_state_with_display(f, display)
let mut state = field_state_with_display(f, display);
if reads_colours {
(state.text_color, state.fill_color) = match self.written_colours.get(&f.name) {
Some(written) => written.clone(),
None => self.colours.entry(f.obj).or_insert_with(|| colours(&self.cos, f)).clone(),
};
}
state
})
.collect();
let states = std::sync::Arc::new(states);
@@ -431,11 +487,17 @@ impl JsRunner {
if o.error.is_some() {
return None;
}
let changes = changes(&states, &o.changed, &o.formatted);
for c in changes.iter().filter(|c| c.text_color.is_some() || c.fill_color.is_some()) {
if let Some(state) = o.changed.iter().find(|s| s.name == c.name) {
self.written_colours.insert(c.name.clone(), (state.text_color.clone(), state.fill_color.clone()));
}
}
Some(ScriptResult {
rc: o.rc,
value: o.value.clone(),
value_set: o.value_set,
changes: changes(&states, &o.changed, &o.formatted),
changes,
message: (!o.rc).then(|| o.alerts.last().map(|m| m.text.clone())).flatten(),
})
}
+144
View File
@@ -6669,6 +6669,89 @@ fn form_javascript_validates_calculates_and_formats() {
assert!(s.run_javascript(id, "1", None).is_err());
}
#[test]
fn javascript_field_colours_persist_and_undo_without_changing_other_appearance_properties() {
let mut s = Session::new().with_clock(|| 1_700_000_000);
let id = s.open("order.pdf", None, Arc::new(scripted_form()), None).unwrap();
s.apply(
id,
Edit::SetFieldProps {
name: "price".into(),
props: Box::new(FieldProps {
appearance: Some(pdfcraft_forms::LookPatch {
border: Some(Some([0.1, 0.2, 0.3])),
font: Some(pdfcraft_forms::FieldFont::Courier),
..Default::default()
}),
..Default::default()
}),
},
)
.unwrap();
let before = s.get(id).unwrap().form.iter().find(|f| f.name == "price").unwrap().value.clone();
let out = s.run_javascript(id, "var f = getField('price'); f.fillColor = color.red; f.textColor = color.blue;", None).unwrap();
assert_eq!(out.error, None);
assert_eq!(s.get(id).unwrap().can_undo(), Some("Run JavaScript"));
let inspect = |bytes: Arc<Vec<u8>>| {
let doc = pdfcraft_cos::Document::open(bytes).unwrap();
let f = pdfcraft_forms::fields(&doc).into_iter().find(|f| f.name == "price").unwrap();
let look = pdfcraft_forms::look(&doc, &f);
assert_eq!(look.fill, Some([1.0, 0.0, 0.0]));
assert_eq!(look.text, [0.0, 0.0, 1.0]);
assert_eq!(look.border, Some([0.1, 0.2, 0.3]));
assert_eq!(look.font, pdfcraft_forms::FieldFont::Courier);
assert_eq!(f.value, before);
};
inspect(s.save_bytes(id).unwrap());
s.undo(id).unwrap();
let undone = pdfcraft_cos::Document::open(s.save_bytes(id).unwrap()).unwrap();
let f = pdfcraft_forms::fields(&undone).into_iter().find(|f| f.name == "price").unwrap();
assert_eq!(pdfcraft_forms::look(&undone, &f).fill, None);
s.redo(id).unwrap();
inspect(s.save_bytes(id).unwrap());
let reopened = s.open("saved.pdf", None, s.save_bytes(id).unwrap(), None).unwrap();
let reopened_doc = pdfcraft_cos::Document::open(s.save_bytes(reopened).unwrap()).unwrap();
let f = pdfcraft_forms::fields(&reopened_doc).into_iter().find(|f| f.name == "price").unwrap();
assert_eq!(pdfcraft_forms::look(&reopened_doc, &f).fill, Some([1.0, 0.0, 0.0]));
assert_eq!(pdfcraft_forms::look(&reopened_doc, &f).text, [0.0, 0.0, 1.0]);
}
#[test]
fn javascript_invalid_field_colour_array_is_refused_without_partial_changes() {
let mut s = Session::new().with_clock(|| 1_700_000_000);
let id = s.open("order.pdf", None, Arc::new(scripted_form()), None).unwrap();
let before = s.save_bytes(id).unwrap();
let err = s.run_javascript(id, "var f = getField('price'); f.value = '9'; f.fillColor = ['RGB', 2, 0, 0];", None).unwrap_err();
assert!(err.to_string().contains("finite values between 0 and 1"), "{err}");
assert_eq!(s.save_bytes(id).unwrap(), before, "a refused script changes nothing");
}
#[test]
fn javascript_field_colours_respect_document_fill_permissions() {
let mut plain = pdfcraft_cos::Document::open(Arc::new(scripted_form())).unwrap();
plain
.set_encryption(&pdfcraft_cos::NewEncryption {
algorithm: pdfcraft_cos::Algorithm::Aes256,
user_password: "",
owner_password: "owner",
permissions: 0,
encrypt_metadata: true,
seed: [7; 32],
})
.unwrap();
let bytes = Arc::new(pdfcraft_cos::write_full(&plain, &Default::default()).unwrap());
let mut s = Session::new().with_clock(|| 1_700_000_000);
let id = s.open("locked.pdf", None, bytes, None).unwrap();
assert!(!s.get(id).unwrap().allows_form_filling());
assert_eq!(
s.run_javascript(id, "getField('price').fillColor = color.red;", None).unwrap_err(),
EditError::NotPermitted("filling in form fields")
);
assert!(!s.get(id).unwrap().dirty);
}
#[test]
fn calculate_now_updates_live_fields_in_one_undo_step() {
let mut s = Session::new().with_clock(|| 1_700_000_000);
@@ -13859,6 +13942,67 @@ fn a_document_encrypting_only_its_embedded_files_opens_and_saves_without_a_passw
assert!(reopened.embedded_files_locked(), "the attachment stays encrypted");
}
#[test]
fn javascript_non_rgb_field_colours_convert_and_keep_the_scripts_other_changes() {
let fill_after = |colour: &str| {
let mut s = Session::new().with_clock(|| 1_700_000_000);
let id = s.open("order.pdf", None, Arc::new(scripted_form()), None).unwrap();
s.run_javascript(id, "getField('price').fillColor = color.red;", None).unwrap();
let out = s.run_javascript(id, &format!("var f = getField('price'); f.value = '9'; f.fillColor = {colour}; f.textColor = {colour};"), None);
assert!(out.is_ok(), "{colour}: {out:?}");
let f = s.get(id).unwrap().form.iter().find(|f| f.name == "price").unwrap().clone();
assert_eq!(f.value, ["9"], "{colour}: the value write is kept");
let doc = pdfcraft_cos::Document::open(s.save_bytes(id).unwrap()).unwrap();
let f = pdfcraft_forms::fields(&doc).into_iter().find(|f| f.name == "price").unwrap();
let look = pdfcraft_forms::look(&doc, &f);
(look.fill, look.text)
};
assert_eq!(fill_after("color.white"), (Some([1.0, 1.0, 1.0]), [1.0, 1.0, 1.0]));
assert_eq!(fill_after("color.gray"), (Some([0.5, 0.5, 0.5]), [0.5, 0.5, 0.5]));
assert_eq!(fill_after("color.cyan"), (Some([0.0, 1.0, 1.0]), [0.0, 1.0, 1.0]));
assert_eq!(fill_after("['CMYK', 0, 0, 0, 1]"), (Some([0.0, 0.0, 0.0]), [0.0, 0.0, 0.0]));
// Transparent clears the fill; text can't be transparent, so its colour stays.
let (fill, text) = fill_after("color.transparent");
assert_eq!(fill, None);
assert_eq!(text, [0.0, 0.0, 0.0]);
let mut s = Session::new().with_clock(|| 1_700_000_000);
let id = s.open("order.pdf", None, Arc::new(scripted_form()), None).unwrap();
let before = s.save_bytes(id).unwrap();
for bad in ["['CMYK', 1]", "['G']", "['T', 0]", "['Lab', 0, 0, 0]", "['G', 'x']"] {
let err = s.run_javascript(id, &format!("var f = getField('price'); f.value = '9'; f.fillColor = {bad};"), None).unwrap_err();
assert!(err.to_string().contains("T, G, RGB or CMYK"), "{bad}: {err}");
assert_eq!(s.save_bytes(id).unwrap(), before, "{bad}: a malformed colour still refuses the script");
}
}
#[test]
fn a_later_field_script_in_the_same_edit_reads_a_colour_an_earlier_one_wrote() {
let mut s = Session::new().with_clock(|| 1_700_000_000);
let id = s.open("order.pdf", None, Arc::new(scripted_form()), None).unwrap();
for (name, script) in
[("qty", "getField('price').fillColor = color.green; event.value = 1;"), ("total", "event.value = getField('price').fillColor.join(',');")]
{
s.apply(id, Edit::SetFieldScript { name: name.into(), event: "calculate".into(), script: Some(script.into()) }).unwrap();
}
s.apply(id, Edit::SetFieldScript { name: "total".into(), event: "format".into(), script: None }).unwrap();
s.apply(id, Edit::SetCalculationOrder { names: vec!["qty".into(), "total".into()] }).unwrap();
s.apply(id, Edit::SetFieldValue { name: "price".into(), value: FieldValue::Text("3".into()) }).unwrap();
let total = s.get(id).unwrap().form.iter().find(|f| f.name == "total").unwrap().value.clone();
assert_eq!(total, ["RGB,0,1,0"]);
}
#[test]
fn a_document_script_helper_can_set_field_colours() {
let helpers = "function mark\\(name\\) { getField\\(name\\).fillColor = color.red; }";
let mut s = Session::new().with_clock(|| 1_700_000_000);
let id = s.open("helpers.pdf", None, Arc::new(scripted_form_with(helpers)), None).unwrap();
s.run_javascript(id, "mark('price');", None).unwrap();
let doc = pdfcraft_cos::Document::open(s.save_bytes(id).unwrap()).unwrap();
let f = pdfcraft_forms::fields(&doc).into_iter().find(|f| f.name == "price").unwrap();
assert_eq!(pdfcraft_forms::look(&doc, &f).fill, Some([1.0, 0.0, 0.0]));
}
#[test]
fn calculate_now_simple_calculation_reads_formatted_values_and_groups_as_the_native_path_does() {
// amount (4), group b (5) with kids b.c (6) and b.d (7), total (8) = SUM(amount, b).
+51 -2
View File
@@ -44,6 +44,10 @@ pub struct FieldChange {
pub required: Option<bool>,
/// `display.visible` 0, `hidden` 1, `noPrint` 2, `noView` 3.
pub display: Option<i32>,
/// Acrobat `textColor` / `fillColor` colour arrays: transparent `["T"]`, gray `["G", g]`,
/// `["RGB", r, g, b]` or `["CMYK", c, m, y, k]`, components between 0 and 1.
pub text_color: Option<Vec<String>>,
pub fill_color: Option<Vec<String>>,
pub caption: Option<String>,
/// Move/resize the first widget, in PDF user space.
pub rect: Option<[f64; 4]>,
@@ -95,8 +99,9 @@ impl Scripts for NoScripts {
/// Run the Keystroke (commit) and Validate scripts for a new text value. Returns the value to
/// store, or why it is rejected.
pub(crate) fn accept(doc: &mut Document, f: &Field, value: &str, scripts: &mut dyn Scripts) -> Result<String, FormError> {
pub(crate) fn accept(doc: &mut Document, f: &Field, value: &str, scripts: &mut dyn Scripts) -> Result<(String, bool), FormError> {
let mut v = value.to_string();
let mut refresh_field = false;
for (event, script, default) in [
(FieldEvent::Keystroke, &f.actions.scripts.keystroke, "The value entered does not match the format of the field"),
(FieldEvent::Validate, &f.actions.scripts.validate, "The value entered is not valid for the field"),
@@ -104,13 +109,14 @@ pub(crate) fn accept(doc: &mut Document, f: &Field, value: &str, scripts: &mut d
let Some(js) = script else { continue };
let all = fields(doc);
let r = scripts.run(event, js, f, &v, &all);
refresh_field |= r.changes.iter().any(|change| change.name == f.name);
apply_changes(doc, &r.changes, &f.name, scripts)?;
if !r.rc {
return Err(FormError::Invalid(r.message.unwrap_or_else(|| format!("{default} [ {} ]", f.name))));
}
v = r.value;
}
Ok(v)
Ok((v, refresh_field))
}
/// The text a custom Format script shows for `value`, if the field has one.
@@ -134,6 +140,12 @@ pub(crate) fn apply_changes(doc: &mut Document, changes: &[FieldChange], except:
if changes.is_empty() {
return Ok(());
}
// Validate every colour before applying any part of the script's changes.
for c in changes {
for color in [&c.text_color, &c.fill_color].into_iter().flatten() {
js_color(color)?;
}
}
let all = fields(doc);
for c in changes {
let Some(f) = all.iter().find(|f| f.name == c.name) else { continue };
@@ -185,6 +197,17 @@ pub(crate) fn apply_changes(doc: &mut Document, changes: &[FieldChange], except:
}
}
}
// Text can't be transparent: `["T"]` leaves its colour as it is. A transparent fill
// clears the background.
let text = c.text_color.as_deref().map(js_color).transpose()?.flatten();
let fill = c.fill_color.as_deref().map(js_color).transpose()?;
if text.is_some() || fill.is_some() {
crate::set_props(
doc,
&c.name,
&crate::FieldProps { appearance: Some(crate::LookPatch { text, fill, ..Default::default() }), ..Default::default() },
)?;
}
if let Some(caption) = &c.caption {
if f.kind != FieldKind::PushButton {
return Err(FormError::Invalid(format!("{} is not a push button", f.name)));
@@ -198,6 +221,32 @@ pub(crate) fn apply_changes(doc: &mut Document, changes: &[FieldChange], except:
Ok(())
}
/// An Acrobat colour array as the RGB the look model stores, `None` for transparent (`["T"]`).
/// Gray and CMYK convert as Acrobat's `color.convert` does. Anything else (an unknown colour
/// space, the wrong number of components, or a component that is not a finite number between
/// 0 and 1) is refused, and with it the script's changes.
fn js_color(values: &[String]) -> Result<Option<[f64; 3]>, FormError> {
let invalid = || FormError::Invalid("JavaScript field colours must be T, G, RGB or CMYK arrays with finite values between 0 and 1".into());
let (space, parts) = values.split_first().ok_or_else(invalid)?;
let mut numbers = [0.0; 4];
if parts.len() > numbers.len() {
return Err(invalid());
}
for (slot, part) in numbers.iter_mut().zip(parts) {
*slot = part.trim().parse::<f64>().ok().filter(|v| v.is_finite() && (0.0..=1.0).contains(v)).ok_or_else(invalid)?;
}
match (space.as_str(), parts.len()) {
("T", 0) => Ok(None),
("G", 1) => Ok(Some([numbers[0]; 3])),
("RGB", 3) => Ok(Some([numbers[0], numbers[1], numbers[2]])),
("CMYK", 4) => {
let [c, m, y, k] = numbers;
Ok(Some([1.0 - (c + k).min(1.0), 1.0 - (m + k).min(1.0), 1.0 - (y + k).min(1.0)]))
}
_ => Err(invalid()),
}
}
/// Apply what a button or console script changed (values, read-only, required, visibility),
/// then recalculate.
pub fn apply_script_changes(doc: &mut Document, changes: &[FieldChange], scripts: &mut dyn Scripts) -> Result<(), FormError> {
+60 -2
View File
@@ -3657,8 +3657,7 @@ fn recalc_chain_where_script_changes_checkbox() {
read_only: None,
required: None,
display: None,
caption: None,
rect: None,
..Default::default()
}],
message: None,
}
@@ -3742,6 +3741,65 @@ fn an_export_value_wins_over_an_earlier_options_display_text() {
}
}
#[test]
fn validation_script_colour_is_used_when_the_field_redraws() {
struct TestScripts;
impl Scripts for TestScripts {
fn run(&mut self, _event: FieldEvent, _script: &str, _target: &Field, value: &str, _fields: &[Field]) -> ScriptResult {
ScriptResult {
rc: true,
value: value.into(),
value_set: false,
changes: vec![FieldChange {
name: "name".into(),
text_color: Some(vec!["RGB".into(), "1".into(), "0".into(), "0".into()]),
..Default::default()
}],
message: None,
}
}
}
let mut doc = fixture();
set_field_script(&mut doc, "name", "validate", Some("set_red")).unwrap();
set_value_with(&mut doc, "name", &FieldValue::Text("Grace".into()), &mut TestScripts).unwrap();
let all = fields(&doc);
let f = field(&all, "name");
assert!(f.da.contains("1 0 0 rg"));
assert!(ap(&doc, &f.widgets[0]).contains("1 0 0 rg"), "redraw uses the Validate script's updated appearance");
}
#[test]
fn calculation_script_colour_is_used_when_the_field_redraws() {
struct TestScripts;
impl Scripts for TestScripts {
fn run(&mut self, _event: FieldEvent, _script: &str, _target: &Field, _value: &str, _fields: &[Field]) -> ScriptResult {
ScriptResult {
rc: true,
value: "Grace".into(),
value_set: true,
changes: vec![FieldChange {
name: "name".into(),
text_color: Some(vec!["RGB".into(), "1".into(), "0".into(), "0".into()]),
..Default::default()
}],
message: None,
}
}
}
let mut doc = fixture();
set_field_script(&mut doc, "name", "calculate", Some("set_red")).unwrap();
recalculate_with(&mut doc, &mut TestScripts).unwrap();
let all = fields(&doc);
let f = field(&all, "name");
assert_eq!(f.value, ["Grace"]);
assert!(f.da.contains("1 0 0 rg"));
assert!(ap(&doc, &f.widgets[0]).contains("1 0 0 rg"), "redraw uses the Calculate script's updated appearance");
}
#[test]
fn partial_appearance_keeps_custom_default_appearance_and_font_resources() {
let mut doc = fixture();
+7 -4
View File
@@ -262,7 +262,8 @@ pub fn recalculate_with(doc: &mut Document, scripts: &mut dyn Scripts) -> Result
_ => {}
}
})?;
redraw(doc, &f, std::slice::from_ref(&v), scripts)?;
let fresh = all.iter().find(|field| field.obj == obj).cloned().unwrap_or_else(|| f.clone());
redraw(doc, &fresh, std::slice::from_ref(&v), scripts)?;
if let Some(target) = all.iter_mut().find(|x| x.obj == obj) {
target.value = vec![v];
}
@@ -305,17 +306,19 @@ pub(crate) fn write_value(doc: &mut Document, f: &Field, value: &FieldValue, scr
// Keystroke (on commit) and Validate, as Acrobat runs them before accepting a value.
let t = &f.actions.keystroke(&f.name, t).map_err(FormError::Format)?;
af::validate(&f.actions.validate, t).map_err(FormError::Invalid)?;
let t = &scripting::accept(doc, f, t, scripts)?;
let (t, refresh_field) = scripting::accept(doc, f, t, scripts)?;
let shadow = t.is_empty() && crate::author::ancestor_sets(doc, f.obj, b"V");
doc.update_dict(f.obj, |d| {
if t.is_empty() && !shadow {
d.remove(b"V");
} else {
d.set(b"V".to_vec(), PdfString::text(t));
d.set(b"V".to_vec(), PdfString::text(&t));
}
d.remove(b"RV");
})?;
redraw(doc, f, std::slice::from_ref(t), scripts)
let current =
if refresh_field { fields(doc).into_iter().find(|current| current.obj == f.obj).unwrap_or_else(|| f.clone()) } else { f.clone() };
redraw(doc, &current, std::slice::from_ref(&t), scripts)
}
(FieldKind::CheckBox | FieldKind::Radio, FieldValue::Widget(i)) => {
// A button can be read-only on its own (its own `/Ff`), though the field isn't.
+1 -2
View File
@@ -205,8 +205,7 @@ pub(super) fn run_button(app: &mut PdfCraftApp, index: usize, name: &str, action
read_only: None,
required: None,
display: Some(if hide { 1 } else { 0 }),
caption: None,
rect: None,
..Default::default()
})
.collect();
if !changes.is_empty() {
+2 -2
View File
@@ -5375,8 +5375,8 @@ milestone = "M6"
status = "shipped"
commands = ["tools.js_console"]
tools = ["js_run"]
evidence = ["crates/ui-egui/tests/it/javascript.rs::console_runs_scripts_against_the_document", "crates/automation/tests/it/automation.rs::javascript_through_tools", "crates/engine/src/tests.rs::calculate_now_updates_live_fields_in_one_undo_step"]
notes = "\u2318J: run JavaScript against the document, with console output, results and errors. calculateNow() runs calculated fields synchronously in /CO order against writes already made by the script."
evidence = ["crates/ui-egui/tests/it/javascript.rs::console_runs_scripts_against_the_document", "crates/automation/tests/it/automation.rs::javascript_through_tools", "crates/engine/src/tests.rs::javascript_field_colours_persist_and_undo_without_changing_other_appearance_properties", "crates/engine/src/tests.rs::javascript_invalid_field_colour_array_is_refused_without_partial_changes", "crates/engine/src/tests.rs::calculate_now_updates_live_fields_in_one_undo_step"]
notes = "\u2318J: run JavaScript against the document, with console output, results and errors. `fillColor` and `textColor` changes (transparent, gray, RGB or CMYK) persist through save/reopen and participate in undo/redo; malformed or out-of-range colour arrays are refused without applying partial script changes. calculateNow() runs calculated fields synchronously in /CO order against writes already made by the script."
[[feature]]
id = "form.js-debugger"