feat(js): this.getOCGs lists the document's layers and ocg.state turns one (#169)

Ported from #4751 by @pyatsysh. DocInfo carries the layers; a run reports
the layers its script turned in Outcome::layers, for the viewer to show or
hide as the Layers panel does (the document isn't changed).

Co-authored-by: Peter Yatsyshin <p.yatsyshin@googlemail.com>
This commit is contained in:
FredAppsandPeter Yatsyshin committed 2026-11-11 10:40:17 +01:00
1 parent f56a146b22
commit e13d0a04ca
7 files changed
+102 -1

No files matched your search

+2 -1
View File
@@ -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)?;
+2
View File
@@ -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<usize>,
}
impl Host {
+2
View File
@@ -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};
+5
View File
@@ -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<crate::Layer>,
}
/// 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<crate::Timer>,
/// 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.
+64
View File
@@ -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<JsValue> {
let n = host(ctx)?.borrow().doc.layers.len();
if n == 0 {
return Ok(JsValue::null());
}
let items: Vec<JsValue> = (0..n).map(|i| ocg(ctx, i)).collect();
Ok(JsArray::from_iter(items, ctx).into())
}
fn layer(c: &Context, i: u32) -> JsResult<Layer> {
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<JsValue> {
Ok(s(&layer(c, *i)?.name))
}
fn state(_: &JsValue, _: &[JsValue], i: &u32, c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(layer(c, *i)?.on))
}
fn initial(_: &JsValue, _: &[JsValue], i: &u32, c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(layer(c, *i)?.initial))
}
fn set_state(_: &JsValue, args: &[JsValue], i: &u32, c: &mut Context) -> JsResult<JsValue> {
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()
}
+3
View File
@@ -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));
}
+24
View File
@@ -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"));
}