diff --git a/crates/js/src/globals.rs b/crates/js/src/globals.rs index 5d690f3b19..941b70d1f3 100644 --- a/crates/js/src/globals.rs +++ b/crates/js/src/globals.rs @@ -484,7 +484,7 @@ fn document_info_get(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult< /// Install the host API once; live document properties continue reading each job's snapshot. pub(crate) fn install(ctx: &mut Context) -> JsResult<()> { - let globals: [(&str, usize, NativeFunction); 12] = [ + let globals: [(&str, usize, NativeFunction); 13] = [ ("getField", 1, NativeFunction::from_fn_ptr(get_field)), ("getNthFieldName", 1, NativeFunction::from_fn_ptr(get_nth_field_name)), ("resetForm", 1, NativeFunction::from_fn_ptr(reset_form)), @@ -497,6 +497,7 @@ pub(crate) fn install(ctx: &mut Context) -> JsResult<()> { ("syncAnnotScan", 0, NativeFunction::from_fn_ptr(noop)), ("dirty", 0, NativeFunction::from_fn_ptr(noop)), ("exportDataObject", 4, NativeFunction::from_fn_ptr(export_data_object)), + ("getOCGs", 1, NativeFunction::from_fn_ptr(crate::ocg::get_ocgs)), ]; for (name, len, f) in globals { ctx.register_global_callable(JsString::from(name), len, f)?; diff --git a/crates/js/src/host.rs b/crates/js/src/host.rs index 5966e63545..6134bc4afe 100644 --- a/crates/js/src/host.rs +++ b/crates/js/src/host.rs @@ -29,6 +29,8 @@ pub(crate) struct Host { pub(crate) kept: usize, /// Alerts, console lines and requests the limits left out. pub(crate) dropped: usize, + /// Layers a script turned on or off (`ocg.state`), by their places in [`DocInfo::layers`]. + pub(crate) layers_changed: BTreeSet, } impl Host { diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index 482e6c8bd0..59be3c7db4 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -73,6 +73,8 @@ mod geometry; pub use geometry::{rotate_rect, unrotate_rect}; mod icons; +mod ocg; +pub use ocg::Layer; mod timers; pub use timers::{MAX_TIMERS, MIN_TIMER_MS, Timer}; diff --git a/crates/js/src/model.rs b/crates/js/src/model.rs index e94868d993..bd2f025786 100644 --- a/crates/js/src/model.rs +++ b/crates/js/src/model.rs @@ -181,6 +181,8 @@ pub struct DocInfo { pub xfa: bool, pub zoom: f64, pub runtime_highlight: bool, + /// The document's layers (optional content groups), for `getOCGs`, in the viewer's order. + pub layers: Vec, } /// The icon a script's message shows: `app.alert`'s `nIcon` and `xfa.host.messageBox`'s @@ -385,6 +387,9 @@ pub struct Outcome { /// The timers of a document's retained realm as they stand after the run, in the order they /// were set (`app.setInterval`, `app.setTimeOut`); none from a one-off run. pub timers: Vec, + /// Layers the script turned on or off (`ocg.state`), as they are now: their places in + /// [`DocInfo::layers`], and whether each is on. + pub layers: Vec<(usize, bool)>, } /// Bounds that keep a script from hanging the viewer. diff --git a/crates/js/src/ocg.rs b/crates/js/src/ocg.rs new file mode 100644 index 0000000000..2f94afa2da --- /dev/null +++ b/crates/js/src/ocg.rs @@ -0,0 +1,64 @@ +//! `this.getOCGs()`: the document's layers (optional content groups) as OCG objects with +//! `name`, `state` and `initState` (#169, ported from #4751). Setting `state` turns a layer on +//! or off as the Layers panel does: how the document is shown, not the document; the caller +//! reads the layers a run turned in [`crate::Outcome::layers`]. + +use crate::host::{arg, error, function, host, s}; +use crate::{Attribute, Context, JsArray, JsResult, JsValue, NativeFunction, ObjectInitializer, js_string}; + +/// A layer as `getOCGs` shows it. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Layer { + pub name: String, + /// Shown now (`ocg.state`). + pub on: bool, + /// Shown as the file opens (`ocg.initState`). + pub initial: bool, +} + +/// `this.getOCGs(nPage)`: every layer of the document, whatever the page (which pages a layer +/// is on isn't known here), or `null` when it has none. +pub(crate) fn get_ocgs(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult { + let n = host(ctx)?.borrow().doc.layers.len(); + if n == 0 { + return Ok(JsValue::null()); + } + let items: Vec = (0..n).map(|i| ocg(ctx, i)).collect(); + Ok(JsArray::from_iter(items, ctx).into()) +} + +fn layer(c: &Context, i: u32) -> JsResult { + host(c)?.borrow().doc.layers.get(i as usize).cloned().ok_or_else(|| error("the layer is no longer available")) +} + +fn ocg(ctx: &mut Context, index: usize) -> JsValue { + fn name(_: &JsValue, _: &[JsValue], i: &u32, c: &mut Context) -> JsResult { + Ok(s(&layer(c, *i)?.name)) + } + fn state(_: &JsValue, _: &[JsValue], i: &u32, c: &mut Context) -> JsResult { + Ok(JsValue::from(layer(c, *i)?.on)) + } + fn initial(_: &JsValue, _: &[JsValue], i: &u32, c: &mut Context) -> JsResult { + Ok(JsValue::from(layer(c, *i)?.initial)) + } + fn set_state(_: &JsValue, args: &[JsValue], i: &u32, c: &mut Context) -> JsResult { + let on = arg(args, 0).to_boolean(); + let mut h = host(c)?.borrow_mut(); + let l = h.doc.layers.get_mut(*i as usize).ok_or_else(|| error("the layer is no longer available"))?; + l.on = on; + h.layers_changed.insert(*i as usize); + Ok(JsValue::undefined()) + } + let i = u32::try_from(index).unwrap_or(u32::MAX); + let name = function(ctx, NativeFunction::from_copy_closure_with_captures(name, i)); + let state = function(ctx, NativeFunction::from_copy_closure_with_captures(state, i)); + let set_state = function(ctx, NativeFunction::from_copy_closure_with_captures(set_state, i)); + let initial = function(ctx, NativeFunction::from_copy_closure_with_captures(initial, i)); + let rw = Attribute::CONFIGURABLE | Attribute::ENUMERABLE; + ObjectInitializer::new(ctx) + .accessor(js_string!("name"), Some(name), None, rw) + .accessor(js_string!("state"), Some(state), Some(set_state), rw) + .accessor(js_string!("initState"), Some(initial), None, rw) + .build() + .into() +} diff --git a/crates/js/src/run.rs b/crates/js/src/run.rs index 3353fe4826..b446b2cd77 100644 --- a/crates/js/src/run.rs +++ b/crates/js/src/run.rs @@ -276,6 +276,7 @@ pub(crate) fn run_in_context( reset_limit_exceeded: false, kept: 0, dropped: 0, + layers_changed: BTreeSet::new(), })); let mut out = Outcome { rc: true, value: event.value.clone(), change: event.change.clone(), ..Default::default() }; let mut doc_errors: Vec<(usize, String)> = setup.skipped.iter().map(|(i, why)| (*i, doc_error(*i, why))).collect(); @@ -335,6 +336,8 @@ pub(crate) fn run_in_context( out.alerts = std::mem::take(&mut h.alerts); out.console = std::mem::take(&mut h.console); out.requests = std::mem::take(&mut h.requests); + let layers = std::mem::take(&mut h.layers_changed); + out.layers = layers.into_iter().filter_map(|i| h.doc.layers.get(i).map(|l| (i, l.on))).collect(); if h.dropped > 0 { out.console.push(format!("the script's messages, console lines and requests passed their limits; {} more were left out", h.dropped)); } diff --git a/crates/js/src/tests.rs b/crates/js/src/tests.rs index c79e609967..a501f463ee 100644 --- a/crates/js/src/tests.rs +++ b/crates/js/src/tests.rs @@ -529,6 +529,7 @@ fn app_alert_titles_are_clipped_and_charged() { reset_limit_exceeded: false, kept: 0, dropped: 0, + layers_changed: Default::default(), }; h.alert(Message { title: Some("Order form".into()), ..Message::plain("Saved") }); assert_eq!(h.kept, "Saved".len() + "Order form".len()); @@ -2702,3 +2703,26 @@ fn af_simple_calculate_bounds_selector_work_before_expansion() { run("AFSimple_Calculate('SUM',new Array(4096).fill('n'));", &Event::doc("Console"), &Default::default(), &fields, &[], Default::default()); assert!(out.error.as_deref().is_some_and(|e| e.contains("calculation field lookup exceeds"))); } + +/// `getOCGs` lists the document's layers; setting `state` turns one, which the run reports, and +/// a document without layers gives `null` (#169, from #4751). +#[test] +fn get_ocgs_lists_layers_and_reports_the_ones_a_script_turns() { + let layers = + vec![crate::Layer { name: "Frame 1".into(), on: true, initial: true }, crate::Layer { name: "Frame 2".into(), on: false, initial: false }]; + let layered = DocInfo { layers, ..doc() }; + let out = run( + "var o = this.getOCGs(); var before = o.map(function (l) { return l.name + '=' + l.state + '/' + l.initState; }).join(','); \ + o[0].state = false; o[1].state = true; before + ' ' + o[1].state;", + &Event::doc("Console"), + &layered, + &[], + &[], + Default::default(), + ); + assert_eq!(out.error, None); + assert_eq!(out.result.as_deref(), Some("Frame 1=true/true,Frame 2=false/false true")); + assert_eq!(out.layers, [(0, false), (1, true)]); + let none = run("this.getOCGs() === null;", &Event::doc("Console"), &doc(), &[], &[], Default::default()); + assert_eq!(none.result.as_deref(), Some("true")); +}