mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 22:02:33 +00:00
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:
1 parent
f56a146b22
commit
e13d0a04ca
7 files changed
+102
-1
No files matched your search
@@ -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)?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
Reference in new issue
Block a user