Files
designcraft/crates/engine/src/cmd/transitions.rs
T
2026-10-03 07:36:50 -04:00

117 lines
5.6 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.
//! Window › Interactive › Page Transitions: a transition per spread for interactive PDF.
use std::sync::Arc;
use designcraft_doc::{PageTransition, TransitionKind};
use serde_json::{Value, json};
use super::{CommandSpec, bad, cmd, has_doc};
use crate::Result;
fn spreads_of(d: &designcraft_doc::Document, p: &Value) -> Result<Vec<usize>> {
if p.get("all").and_then(Value::as_bool) == Some(true) {
return Ok((0..d.spreads.len()).collect());
}
if let Some(si) = p.get("spread").and_then(Value::as_u64) {
return Ok(vec![si as usize]);
}
let pages: Vec<usize> =
p.get("pages").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(|n| n as usize).collect()).unwrap_or_default();
if pages.is_empty() {
return Err(bad("page.transition", "give `pages` (1-based), `spread` or `all`"));
}
let mut v: Vec<usize> = pages.iter().filter_map(|n| n.checked_sub(1).and_then(|i| d.page_loc(i)).map(|(si, _)| si)).collect();
v.dedup();
Ok(v)
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
"page.transition",
"Page Transition",
[],
None,
"{pages?: [1-based] | spread? | all?: bool, kind: blinds|box|comb|cover|dissolve|fade|push|split|uncover|wipe|zoomIn|zoomOut|none, duration?: seconds (1), horizontal?: bool} — for interactive PDF",
has_doc,
|s, p| {
let kind = p.get("kind").and_then(Value::as_str).unwrap_or("none");
let t = if kind == "none" {
None
} else {
let k: TransitionKind =
serde_json::from_value(json!(kind)).map_err(|_| bad("page.transition", format!("unknown kind `{kind}`")))?;
Some(PageTransition {
kind: k,
duration: p.get("duration").and_then(Value::as_f64).unwrap_or(1.0).clamp(0.0, 60.0),
horizontal: p.get("horizontal").and_then(Value::as_bool).unwrap_or(false),
})
};
let list = spreads_of(&s.doc()?.doc, p)?;
s.edit(|d, _| {
for si in &list {
let sp = d.spreads.get_mut(*si).ok_or_else(|| bad("page.transition", format!("no spread {si}")))?;
if let Some(pg) = Arc::make_mut(sp).pages.first_mut() {
pg.transition = t;
}
}
Ok(json!({"spreads": list.len()}))
})
}
),
cmd!(query "page.transitions", "Page Transitions", [], None, "{} → [{spread, kind, duration, horizontal}]", has_doc, |s, _| {
let d = &s.doc()?.doc;
Ok(Value::Array(
d.spreads
.iter()
.enumerate()
.filter_map(|(si, sp)| {
let t = sp.pages.first()?.transition?;
Some(json!({"spread": si, "kind": t.kind, "duration": t.duration, "horizontal": t.horizontal}))
})
.collect(),
))
}),
]
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::Session;
#[test]
fn transitions_go_into_the_pdf() {
let mut s = Session::new();
s.execute("file.new", &json!({"pages": 3})).unwrap();
s.execute("page.transition", &json!({"pages": [2], "kind": "wipe", "duration": 2})).unwrap();
assert_eq!(s.execute("page.transitions", &json!({})).unwrap()[0]["kind"], "wipe");
let r = s.execute("file.exportPdf", &json!({})).unwrap();
let bytes = super::super::file::base64_decode(r["base64"].as_str().unwrap());
let text = String::from_utf8_lossy(&bytes);
// Pages 2 and 3 share a spread, and the transition is the spread's.
assert_eq!(text.matches("/Trans<<").count(), 2);
assert!(text.contains("/S/Wipe") && text.contains("/D 2.00"));
// The updated file still reads, with all its pages.
assert_eq!(designcraft_render::pdf_page_count(&bytes), Some(3));
// Exported as spreads: one PDF page.
let r = s.execute("file.exportPdf", &json!({"spreads": true})).unwrap();
let bytes = super::super::file::base64_decode(r["base64"].as_str().unwrap());
assert_eq!(String::from_utf8_lossy(&bytes).matches("/Trans<<").count(), 1);
// With transparency too, the page keeps both its group and its transition.
let id = s.execute("frame.create", &json!({"rect": [100, 100, 200, 200]})).unwrap()["id"].clone();
s.execute("object.opacity", &json!({"ids": [id], "opacity": 0.5})).unwrap();
let r = s.execute("file.exportPdf", &json!({"pages": [1]})).unwrap();
let bytes = super::super::file::base64_decode(r["base64"].as_str().unwrap());
assert_eq!(designcraft_render::pdf_page_count(&bytes), Some(1));
s.execute("page.transition", &json!({"pages": [1], "kind": "fade"})).unwrap();
let r = s.execute("file.exportPdf", &json!({"pages": [1]})).unwrap();
let text = String::from_utf8_lossy(&super::super::file::base64_decode(r["base64"].as_str().unwrap())).to_string();
let last = &text[text.rfind("/Type/Page/").unwrap()..];
assert!(last.contains("/S/Fade") && last[..last.find("endobj").unwrap()].contains("/S/Transparency"), "{last}");
s.execute("page.transition", &json!({"all": true, "kind": "none"})).unwrap();
assert!(s.execute("page.transitions", &json!({})).unwrap().as_array().unwrap().is_empty());
}
}