Files
pdfcraft/crates/engine/src/live_scripts_tests.rs
T
2026-11-11 11:37:07 +01:00

223 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::sync::Arc;
use super::screen_config;
use crate::{DocId, Session};
/// A PDF of `objs` (object 1 the catalog), with its cross-reference table.
fn pdf(objs: &[String]) -> Vec<u8> {
let mut out = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, o) in objs.iter().enumerate() {
offsets.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
}
let xref = out.len();
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
for o in offsets {
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
out
}
/// A form XObject filling 100 × 100 with colour `rgb`.
fn fill(rgb: &str) -> String {
let body = format!("{rgb} rg 0 0 100 100 re f");
format!("<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] /Length {} >>\nstream\n{body}\nendstream", body.len())
}
/// A push button widget on page 3 0 R at [50 100 150 200].
fn widget(name: &str, flags: u32, extra: &str) -> String {
format!("<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T ({name}) /F {flags} /Rect [50 100 150 200] /P 3 0 R {extra} >>")
}
/// Two pages; on the first, an animation as the LaTeX `animate` package builds one: three
/// frames as push buttons over one another (red showing, green and blue hidden), and a widget
/// whose page-open action defines the player and starts an interval and whose page-close
/// action stops it.
fn animation_pdf() -> Vec<u8> {
let open = "if (typeof on == 'undefined') { var on = 0; var t = null; } \
function show(i) { getField('f' + on).display = display.hidden; on = i; getField('f' + on).display = display.visible; } \
function step() { show((on + 1) % 3); } t = app.setInterval('step()', 100);";
pdf(&[
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R 6 0 R 7 0 R] >> >>".into(), // 1
"<< /Type /Pages /Kids [3 0 R 12 0 R] /Count 2 /MediaBox [0 0 200 300] >>".into(), // 2
"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R 6 0 R 7 0 R] >>".into(), // 3
widget("f0", 4, "/AP << /N 8 0 R >>"), // 4
widget("f1", 6, "/AP << /N 9 0 R >>"), // 5
widget("f2", 6, "/AP << /N 10 0 R >>"), // 6
widget("anim", 4, "/AA << /PO << /S /JavaScript /JS 11 0 R >> /PC << /S /JavaScript /JS (app.clearInterval\\(t\\);) >> >>"), // 7
fill("1 0 0"), // 8
fill("0 1 0"), // 9
fill("0 0 1"), // 10
format!("<< /Length {} >>\nstream\n{open}\nendstream", open.len()), // 11
"<< /Type /Page /Parent 2 0 R >>".into(), // 12
])
}
const RED: [u8; 4] = [255, 0, 0, 255];
const GREEN: [u8; 4] = [0, 255, 0, 255];
const BLUE: [u8; 4] = [0, 0, 255, 255];
/// RGBA at PDF point (x, y) on `page` as the viewer draws it (1 px/pt, page height 300).
fn shown(s: &Session, id: DocId, page: usize, x: u32, y: u32) -> [u8; 4] {
let doc = s.get(id).unwrap();
let mut r = pdfcraft_render::PageRenderer::new(doc.display.clone(), screen_config(doc));
let out = r.render(pdfcraft_render::RenderRequest { page, scale: 1.0, ..Default::default() });
assert!(out.error.is_none(), "{:?}", out.error);
let i = (((300 - y) * out.width + x) * 4) as usize;
out.rgba[i..i + 4].try_into().unwrap()
}
/// RGBA at (x, y) on `page` as it prints or exports: the document itself.
fn printed(s: &Session, id: DocId, page: usize, x: u32, y: u32) -> [u8; 4] {
let doc = s.get(id).unwrap();
let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), doc.config.clone());
let out = r.render(pdfcraft_render::RenderRequest { page, scale: 1.0, ..Default::default() });
let i = (((300 - y) * out.width + x) * 4) as usize;
out.rgba[i..i + 4].try_into().unwrap()
}
#[test]
fn page_actions_start_timers_that_turn_an_animations_frames_without_changing_the_document() {
let mut s = Session::new();
let id = s.open("animation.pdf", None, Arc::new(animation_pdf()), None).unwrap();
let bytes = s.get(id).unwrap().bytes.clone();
assert_eq!(shown(&s, id, 0, 100, 150), RED);
assert_eq!(s.next_timer(id), None);
let run = s.show_page(id, 0, 1000.0);
assert!(run.output.errors.is_empty(), "{run:?}");
assert_eq!(s.next_timer(id), Some(1100.0));
assert!(s.show_page(id, 0, 1010.0).is_empty(), "the same page again runs nothing");
assert!(s.run_timers(id, 1050.0).is_empty(), "not due yet");
let run = s.run_timers(id, 1100.0);
assert_eq!((run.pages.as_slice(), run.edited, run.ran), ([0].as_slice(), false, 1), "{run:?}");
assert_eq!(shown(&s, id, 0, 100, 150), GREEN);
assert_eq!(printed(&s, id, 0, 100, 150), RED, "printing and export draw the document");
assert_eq!(s.next_timer(id), Some(1200.0));
let doc = s.get(id).unwrap();
assert!(Arc::ptr_eq(&doc.bytes, &bytes) && !doc.dirty && doc.undo_depth() == 0, "the frames turn in the view, not in the document");
// An interval that fell behind runs once and counts on from now.
assert_eq!(s.run_timers(id, 1450.0).ran, 1);
assert_eq!(shown(&s, id, 0, 100, 150), BLUE);
assert_eq!(s.next_timer(id), Some(1550.0));
// A button runs in the same realm: the page action's functions and state are there.
let o = s.run_javascript(id, "step();", Some("f0")).unwrap();
assert_eq!(o.error, None);
let o = s.run_javascript(id, "on", None).unwrap();
assert_eq!(o.result.as_deref(), Some("0"));
// Leaving the page runs its close action, which clears the interval.
s.show_page(id, 1, 1500.0);
assert_eq!(s.next_timer(id), None);
// With JavaScript off, page actions don't run.
s.set_javascript(false);
assert!(s.show_page(id, 0, 1600.0).is_empty());
assert_eq!(s.next_timer(id), None);
}
#[test]
fn a_button_that_shows_a_frame_changes_the_document_and_the_views_frame_gives_way() {
let mut s = Session::new();
let id = s.open("animation.pdf", None, Arc::new(animation_pdf()), None).unwrap();
s.show_page(id, 0, 0.0);
s.run_timers(id, 100.0);
assert_eq!(shown(&s, id, 0, 100, 150), GREEN);
s.run_javascript(id, "step();", Some("f0")).unwrap();
assert_eq!(shown(&s, id, 0, 100, 150), BLUE);
assert!(s.get(id).unwrap().dirty, "a button's change is the document's");
assert_eq!(printed(&s, id, 0, 100, 150), BLUE);
s.run_timers(id, 200.0);
assert_eq!(shown(&s, id, 0, 100, 150), RED);
}
#[test]
fn a_timer_runs_unasked_so_its_alerts_and_requests_go_to_the_console() {
let open = "app.setTimeOut('app.alert(\"hi\"); this.print(); app.launchURL(\"https://example.com\");', 50);";
let mut s = Session::new();
let id = s.open("t.pdf", None, Arc::new(timer_pdf(open)), None).unwrap();
assert!(s.show_page(id, 0, 0.0).output.errors.is_empty());
let run = s.run_timers(id, 50.0);
assert_eq!(run.ran, 1);
assert!(run.output.alerts.is_empty() && run.output.requests.is_empty(), "{run:?}");
assert!(run.output.console.iter().any(|l| l.contains("hi")), "{run:?}");
assert_eq!(s.next_timer(id), None, "a time-out runs once");
}
#[test]
fn a_fresh_realm_has_no_timers_and_a_timer_writing_a_value_is_one_undoable_edit() {
let open = "app.setInterval('getField(\"f0\").value = \"x\";', 10);";
let mut s = Session::new();
let id = s.open("t.pdf", None, Arc::new(timer_pdf(open)), None).unwrap();
s.show_page(id, 0, 0.0);
let run = s.run_timers(id, 10.0);
assert!(run.edited && run.output.errors.is_empty(), "{run:?}");
assert_eq!(s.get(id).unwrap().undo_depth(), 1);
assert!(s.next_timer(id).is_some());
s.invalidate_javascript(id);
assert_eq!(s.next_timer(id), None, "the timers went with the realm");
}
/// One page with a text field f0 whose page-open action is `open`.
fn timer_pdf(open: &str) -> Vec<u8> {
pdf(&[
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R] >> >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 300] >>".into(),
"<< /Type /Page /Parent 2 0 R /Annots [4 0 R] /AA << /O 5 0 R >> >>".into(),
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (f0) /Rect [50 100 150 200] /P 3 0 R >>".into(),
"<< /S /JavaScript /JS 6 0 R >>".into(),
format!("<< /Length {} >>\nstream\n{open}\nendstream", open.len()),
])
}
/// An animation as `animate` builds one by default (method=icon): hidden frames are buttons
/// holding the icons, and the player gives the one showing button a frame's icon.
#[test]
fn a_timer_giving_a_button_another_icon_draws_that_icon_in_its_place() {
let open = "if (typeof on == 'undefined') { var on = 0; var fr = [getField('f0').buttonGetIcon(), getField('f1').buttonGetIcon()]; } \
app.setInterval('on = 1 - on; getField(\"anim\").buttonSetIcon(fr[on]);', 100);";
let out = pdf(&[
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R 6 0 R] >> >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 300] >>".into(),
"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R 6 0 R] >>".into(),
widget("anim", 4, "/AP << /N 7 0 R >> /MK << /I 7 0 R >> /AA << /PO << /S /JavaScript /JS 9 0 R >> >>"),
widget("f0", 6, "/AP << /N 7 0 R >> /MK << /I 7 0 R >>"),
widget("f1", 6, "/AP << /N 8 0 R >> /MK << /I 8 0 R >>"),
fill("1 0 0"),
fill("0 1 0"),
format!("<< /Length {} >>\nstream\n{open}\nendstream", open.len()),
]);
let mut s = Session::new();
let id = s.open("icons.pdf", None, Arc::new(out), None).unwrap();
assert_eq!(shown(&s, id, 0, 100, 150), RED);
assert!(s.show_page(id, 0, 0.0).output.errors.is_empty());
let run = s.run_timers(id, 100.0);
assert_eq!((run.pages.as_slice(), run.output.errors.len()), ([0].as_slice(), 0), "{run:?}");
assert_eq!(shown(&s, id, 0, 100, 150), GREEN);
assert_eq!(printed(&s, id, 0, 100, 150), RED);
s.run_timers(id, 200.0);
assert_eq!(shown(&s, id, 0, 100, 150), RED, "its own icon again");
assert!(!s.get(id).unwrap().dirty);
}
#[test]
fn get_ocgs_turns_a_layer_in_the_view() {
let open = "this.getOCGs()[0].state = false;";
let out = pdf(&[
"<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [5 0 R] /D << /Order [5 0 R] >> >> >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 300] >>".into(),
"<< /Type /Page /Parent 2 0 R /Contents 4 0 R /Resources << /Properties << /L1 5 0 R >> >> /AA << /O << /S /JavaScript /JS 6 0 R >> >> >>"
.into(),
"<< /Length 41 >>\nstream\n/OC /L1 BDC 1 0 0 rg 0 0 200 300 re f EMC\nendstream".into(),
"<< /Type /OCG /Name (Red) >>".into(),
format!("<< /Length {} >>\nstream\n{open}\nendstream", open.len()),
]);
let mut s = Session::new();
let id = s.open("layers.pdf", None, Arc::new(out), None).unwrap();
assert_eq!(shown(&s, id, 0, 100, 150), RED);
let run = s.show_page(id, 0, 0.0);
assert!(run.layers && run.output.errors.is_empty(), "{run:?}");
assert!(!s.get(id).unwrap().info.layers[0].visible);
assert_eq!(shown(&s, id, 0, 100, 150), [255, 255, 255, 255]);
assert!(!s.get(id).unwrap().dirty);
}