From c0f76fc73ae7240c9af71e834b9a37c816c763c4 Mon Sep 17 00:00:00 2001 From: mtlcl410 Date: Thu, 15 Oct 2026 14:42:28 +0800 Subject: [PATCH] M4: set page transitions and display times (/Trans, /Dur) through the engine and the tools (#2083) * M4: set page transitions and display times (/Trans, /Dur) through the engine and the tools organize::set_transition writes or removes a page's /Trans dictionary (only the entries the style reads and that differ from the defaults) and /Dur, with every value checked before any page changes; Edit::SetTransition is one undo step ("Set page transitions", translated in every catalogue); page_transition_set is the tool. The counterpart of reading them (#2074); playing them and the Organize dialog stay planned. Refs #379. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz * Refresh catalog hashes after merging main --------- Co-authored-by: Claude Fable 5.1 Co-authored-by: Brandon Thomas --- ATTRIBUTION.toml | 42 ++++---- ROADMAP.md | 1 + crates/automation/src/lib.rs | 53 ++++++++++ crates/automation/src/tools.rs | 20 ++++ crates/automation/tests/automation.rs | 60 ++++++++++++ crates/engine/src/lib.rs | 12 ++- crates/organize/src/lib.rs | 2 + crates/organize/src/tests.rs | 50 ++++++++++ crates/organize/src/transition.rs | 133 ++++++++++++++++++++++++++ crates/ui-egui/src/i18n/ar.tsv | 1 + crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/kk.tsv | 1 + crates/ui-egui/src/i18n/lt.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sl.tsv | 1 + crates/ui-egui/src/i18n/te.tsv | 1 + crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + parity/acrobat-features.toml | 3 + 31 files changed, 375 insertions(+), 22 deletions(-) create mode 100644 crates/organize/src/transition.rs diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index ddeeb86b65..cbf7f899bf 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8f2dc597b6f0b05c430260cccfa98e59905ae6badd920ea6982e6a48736e7602" +sha256 = "70da78d17186e3d91e952f9d1feb0f71ae03355eea5963f30d38496ee8802f80" # ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences. # ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`). @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "747fb15896208d759e3c4d4ef243d39ea55f39e7d096a4f0c17f7b8276dd01f7" +sha256 = "b4c3d8c6b181c2bec36d5b4cb28312b261cf17dcebc9e41f0b1bed2f3744b86d" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "4430f20b3dd5e86471fb61d63529695f1b81d7af14cba028aa9e4a7709f2eb51" +sha256 = "cbb627936023af9331e2a4441661f5bae43e2453cd325c920f09d86de18020e0" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "4aef3cd513d168abd15d1760d6e754278e7539e4c02f36770f9eed44dd6e88fd" +sha256 = "9d7ded91862278510b7b0fd7c6c1ae953936a720535e33d89003c3aa50422ad7" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "707973b0c7f0e546c84c0ba07b4d828071d24a8318f1287ccbdf065b985ac326" +sha256 = "4648d4afc78f8b37ea6f855471c0713c5c4e948c1b909da3aa406a81cb51135e" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "f4a3c6f7c33b31f489c3e7e3f23084ccbd0c31567e7d665d9c583ff4f9b56af7" +sha256 = "cb8f794e06eedbb9cff34ac7b6b41cf2eba2a36d24daba7307854babae85a89f" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "50fc9a57a9d301f46d38a812764b4e6e9791fff154297cdaa5240519d281c48b" +sha256 = "6568887271ac5f0bc00c127b3270e9b4d5eedbd791e0268dcbea6b63c22672bd" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "cbdd825e664c37a1dc2cf559a63a1ca6fb093a8c1036ccdbbf0738d684e61c8b" +sha256 = "57b8ba03355ca1a866f234a0814f76be895a9499acdba8473bc5301e9cb55cf1" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "c119f82118264b45e46de6c31f4db3c062c86f35a661b8d2f12170ee11042ddf" +sha256 = "67a57e49fc7c1ff984337b099da27e81f583197f0508aeac2fef819bbcf82edb" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "24132323fb0ab81a96a18c1ed7a7dee719469b3733f0425346fce8f9a8ea8081" +sha256 = "3188bd767d048d27ad70535d950202044611958f90a39bc738f5551b0494282d" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8146db6443af5cb41655aabc415d6bd738259c886dc47eddb4fd88b82f166347" +sha256 = "0ee764524ec3fbacd1eb3f5a59f4f23c3041a658851dca0dc01a4c544df058fb" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "6e18893f0cad5d23b72bfcd5e8bbd697143811a4288a71efc87292e7bf1ef39b" +sha256 = "0a2fc4ecdbbbe6e709730c3aa76b2dc7d3e9c9ea6de42a9b3ce9b6765d3c54eb" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8f4ddb7fec77362d652f4a08f65610ebbf83f1fce1c061dbc347b22bd92c9b23" +sha256 = "2e85a97fb2cfdb6bb20f1f8f79384a2984cae46cab7e56f6cec76a21995ab1e9" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "f1e021147716496129d887db04f576380f3ed75348dece15cf6446e5e57ef0e9" +sha256 = "2a55c0c380d45fa41d3f6461e42e351e0f2e3e7142e60546bf141fb4685866ba" [[asset]] path = "crates/ui-egui/src/i18n/kk.tsv" @@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "842fe4d5047359db29f014b23eee4b7c1eb2cb76b0bf13077dcace2c763a17f4" +sha256 = "c6caafd21bfb93fe1c53a1a2c1cbbbb0d74325b00aaab307e00eacdd6f2e02dc" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "ad4bb29a8c740bc5d9148f93d13e8b548509b107d9612a1a45e2a80decf1b0a8" +sha256 = "3374b7ef71dc8ec388493d45a7db280c881626af9e03694d032f586cae645397" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "0fb190f6ee02103b7f06a94ee2ed7764c56ab8b905567d68485b8023b66e3e59" +sha256 = "d851673052fa3c794de1dfc83802caf09a1ce8a1156098c47ea151e53a28bf87" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "15aa44d01688879b0321f820b0087cbcb9e3924a957cf1457ee9256e42626957" +sha256 = "de9609658581173eed2f3a565b59b920d4363f54f7a46148883f00600a2f88e5" [[asset]] path = "crates/ui-egui/src/i18n/tr.tsv" @@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "09cf93e500a6a0c461942ceb985731dbcee3ca00bb3160c1af60448a402b1862" +sha256 = "6b1e58e80045623c9e92a70f1bc84861b0e7935e0ed8c12fdb56b75af4612283" [[asset]] path = "crates/ui-egui/src/i18n/lt.tsv" @@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "10222eaf5a9dd16f5677554f586ed715f10f2c56ccfc8ffcf68f3f90d2e8c61a" +sha256 = "484b28b1dbe0d9851a08c4a73178f85285a29019e09baa32533dfbccfec39b28" [[asset]] @@ -3363,7 +3363,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "dddc56fe8c320dd807c56a5d6450eeb5cdaecc81d9e517b6c1adfe986d688a41" +sha256 = "d85b6bf3b62a82912aeb87ac97ca54fc6adb9dd79941117ee56cf198ede4d752" [[asset]] path = "vendor/vello_common/assets/probe.png" diff --git a/ROADMAP.md b/ROADMAP.md index cc5ff96faa..71a0ddf8c4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -169,6 +169,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-13 (M3, #1980):** Ctrl+Alt+Q in the digital ID email field no longer requests application exit. Quit requires exact Ctrl+Q/Cmd+Q modifiers and uses the existing unsaved-document confirmation. Synthetic keyboard regressions cover email input and clean/dirty quit requests; estimates unchanged. - **2026-10-14 (M4, page transitions read, §12.4.4):** `/Trans` and `/Dur` are read into `DocInfo::transitions` and listed by `doc_info`, the data a full-screen presentation needs to play transitions and auto-advance; playing and editing them stay planned. +- **2026-10-14 (M4, page transitions set, §12.4.4):** `page_transition_set` (one undoable edit) writes or removes `/Trans` and `/Dur` on chosen pages with the values checked first, the counterpart of reading them into `DocInfo::transitions`; playing them and the dialog stay planned. - **2026-10-13 (M3, native-opening candidate):** Chosen tools survive preparation, conversion and password retries; startup intake waits for pending preparation. Regression tests cover completion, cancellation and document identity. Estimates unchanged. - **2026-10-13 (community, M3, #531 touchpad momentum):** On Wayland a two-finger swipe keeps gliding after the fingers lift and slows to a stop, measured from the gesture's last 0.1 s and decaying with a 0.35 s time constant through the #759 1:1 path. Any wheel input, click, key, zoom or edge ends it (fingers resting on the touchpad don't yet: the vendored winit binds no Wayland hold gestures); single-page paging, macOS (which sends its own momentum), the web, and Animation effects Off never glide. Unit tests for the physics and two headless view tests. Estimates unchanged. - **2026-10-13 (M3, native-open admission):** Later prepared replies stay queued while a modal or native picker owns the foreground. Picker completion, cancellation and abandonment wake retained opens. Regressions cover recipient ownership, exactly-once admission, source/history preservation and one-shot repaint requests. Estimates unchanged. diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index c4ea39036e..f003cc12dc 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -353,6 +353,7 @@ impl Automation { self.apply(&a, Edit::DuplicatePages { pages })? } "page_set_box" => self.page_set_box(&a)?, + "page_transition_set" => self.page_transition_set(&a)?, "doc_create" => self.doc_create(&a)?, "xfa_to_static" => self.apply(&a, Edit::XfaToStatic)?, "doc_flatten" => { @@ -1297,6 +1298,58 @@ impl Automation { Ok(summary(self.session.get(id).ok_or_else(|| failed("the document vanished"))?)) } + fn page_transition_set(&mut self, a: &Args) -> Result { + use pdfcraft_engine::{Transition, TransitionSpec}; + let n = self.doc(a)?.info.pages.len(); + let pages = match a.opt_ints("pages")? { + Some(_) => self.pages(a, "pages")?, + None => (0..n).collect(), + }; + let transition = match a.opt_str("style")? { + None | Some("none") => None, + Some(style) => { + let mut t = Transition::new(style); + if let Some(d) = a.opt_num("duration")? { + t.duration = d as f32; + } + if let Some(dm) = a.opt_str("dimension")? { + t.horizontal = match dm { + "horizontal" => true, + "vertical" => false, + other => return Err(ToolError::InvalidArgs(format!("dimension must be horizontal or vertical, not {other:?}"))), + }; + } + if let Some(m) = a.opt_str("motion")? { + t.inward = match m { + "inward" => true, + "outward" => false, + other => return Err(ToolError::InvalidArgs(format!("motion must be inward or outward, not {other:?}"))), + }; + } + match a.get("direction") { + None => {} + Some(Value::String(s)) if s == "none" => t.direction = None, + Some(v) => { + t.direction = Some( + v.as_u64() + .and_then(|d| u16::try_from(d).ok()) + .ok_or_else(|| ToolError::InvalidArgs("direction must be 0, 90, 180, 270, 315 or \"none\"".into()))?, + ); + } + } + if let Some(s) = a.opt_num("scale")? { + t.scale = s as f32; + } + if let Some(b) = a.opt_bool("opaque")? { + t.opaque = b; + } + Some(t) + } + }; + let display = a.opt_num("display")?.map(|s| s as f32); + self.apply(a, Edit::SetTransition { pages, spec: TransitionSpec { transition, display } }) + } + fn page_set_box(&mut self, a: &Args) -> Result { use pdfcraft_engine::{BoxSpec, PageBox}; let doc = self.doc(a)?; diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 1f3e965f08..07f955a2ee 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -824,6 +824,26 @@ pub fn tools() -> Vec { t("page_duplicate", "Duplicate pages", "Insert copies of pages after the last of them (fonts and images are shared, not copied). Undoable.") .cmd("page.duplicate") .with(schema(json!({ "doc": doc(), "pages": pages("to duplicate") }), &["doc", "pages"])), + t( + "page_transition_set", + "Set page transitions", + "Set or remove the transition and display time pages play in a full-screen presentation (ISO 32000-2 §12.4.4; Acrobat's Organize ▸ Page Transitions). style: Split, Blinds, Box, Wipe, Dissolve, Glitter, R (replace, no effect), Fly, Push, Cover, Uncover or Fade, or none to remove the transition; duration in seconds (default 1); dimension horizontal/vertical (Split, Blinds); motion inward/outward (Split, Box, Fly); direction 0, 90, 180, 270, 315 or none (Wipe, Glitter, Fly, Cover, Uncover, Push); scale and opaque for Fly. display: seconds the page stays before advancing; left out, the page waits for the presenter. doc_info lists the result under transitions.", + ) + .with(schema( + json!({ + "doc": doc(), + "pages": pages("to change (default: all)"), + "style": { "type": "string", "enum": ["none", "Split", "Blinds", "Box", "Wipe", "Dissolve", "Glitter", "R", "Fly", "Push", "Cover", "Uncover", "Fade"] }, + "duration": { "type": "number", "minimum": 0 }, + "dimension": { "type": "string", "enum": ["horizontal", "vertical"] }, + "motion": { "type": "string", "enum": ["inward", "outward"] }, + "direction": { "description": "0, 90, 180, 270, 315 or \"none\"" }, + "scale": { "type": "number", "exclusiveMinimum": 0 }, + "opaque": { "type": "boolean" }, + "display": { "type": "number", "minimum": 0 }, + }), + &["doc"], + )), t( "page_set_box", "Set page boxes / crop", diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 9eae3dfc0f..fa0e10034e 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -154,6 +154,66 @@ fn doc_info_lists_page_transitions_and_display_times() { ); } +/// Page transitions through the tools: set on chosen pages, written to the file as `/Trans` and +/// `/Dur`, removed again, bad values refused, and one undo step. (Read through `pdfcraft_cos`, +/// so this does not lean on `doc_info` listing them.) +#[test] +fn page_transitions_are_set_and_removed_through_the_tools() { + use pdfcraft_cos::Object; + /// `key` of 0-based `page` in the saved file `name`, resolved. + fn entry(dir: &Path, name: &str, page: usize, key: &[u8]) -> Option { + let d = pdfcraft_cos::Document::open(std::sync::Arc::new(std::fs::read(dir.join(name)).ok()?)).ok()?; + let root = d.root()?; + let pages = d.get(root).as_dict()?.reference(b"Pages")?; + let kids = d.get(pages).as_dict()?.get(b"Kids")?.clone(); + let kid = d.resolve(&kids).as_array()?.get(page)?.clone(); + let o = d.resolve(&kid).as_dict()?.get(key)?.clone(); + Some((*d.resolve(&o)).clone()) + } + let dir = workdir("transitions-set"); + std::fs::write(dir.join("a.pdf"), fixture(3)).unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + ok(&mut a, "page_transition_set", json!({ "doc": doc, "pages": [2, 3], "style": "Wipe", "duration": 2, "direction": 270, "display": 5 })); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "out/t1.pdf" })); + let t = entry(&dir, "out/t1.pdf", 1, b"Trans").expect("/Trans on page 2"); + let t = t.as_dict().expect("a dictionary"); + assert_eq!( + (t.name(b"S"), t.get(b"D").and_then(Object::as_f64), t.get(b"Di").and_then(Object::as_f64)), + (Some(&b"Wipe"[..]), Some(2.0), Some(270.0)), + "{t:?}" + ); + assert_eq!(entry(&dir, "out/t1.pdf", 1, b"Dur").and_then(|o| o.as_f64()), Some(5.0)); + assert!(entry(&dir, "out/t1.pdf", 2, b"Trans").is_some(), "page 3 too"); + assert!(entry(&dir, "out/t1.pdf", 0, b"Trans").is_none() && entry(&dir, "out/t1.pdf", 0, b"Dur").is_none(), "page 1 was not asked for"); + ok( + &mut a, + "page_transition_set", + json!({ "doc": doc, "pages": [3], "style": "Fly", "motion": "outward", "direction": "none", "scale": 0.5, "opaque": true }), + ); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "out/t2.pdf" })); + let t = entry(&dir, "out/t2.pdf", 2, b"Trans").expect("/Trans on page 3"); + let t = t.as_dict().expect("a dictionary"); + assert_eq!( + (t.name(b"S"), t.name(b"M"), t.name(b"Di"), t.get(b"SS").and_then(Object::as_f64), t.get(b"B")), + (Some(&b"Fly"[..]), Some(&b"O"[..]), Some(&b"None"[..]), Some(0.5), Some(&Object::Bool(true))), + "{t:?}" + ); + assert!(entry(&dir, "out/t2.pdf", 2, b"Dur").is_none(), "display time left out removes /Dur"); + assert!(a.call("page_transition_set", &json!({ "doc": doc, "style": "Swirl" })).is_err(), "an unknown style is refused"); + assert!(a.call("page_transition_set", &json!({ "doc": doc, "style": "Wipe", "direction": 45 })).is_err()); + assert!(a.call("page_transition_set", &json!({ "doc": doc, "pages": [9], "style": "Fade" })).is_err(), "no such page"); + ok(&mut a, "page_transition_set", json!({ "doc": doc, "style": "none" })); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "out/t3.pdf" })); + assert!((0..3).all(|p| entry(&dir, "out/t3.pdf", p, b"Trans").is_none() && entry(&dir, "out/t3.pdf", p, b"Dur").is_none()), "all removed"); + ok(&mut a, "edit_undo", json!({ "doc": doc })); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "out/t4.pdf" })); + assert!( + entry(&dir, "out/t4.pdf", 1, b"Trans").is_some() && entry(&dir, "out/t4.pdf", 2, b"Trans").is_some(), + "one undo step brings both pages back" + ); +} + #[test] fn autotag_preview_proposes_text_without_changing_the_pdf() { let dir = workdir("autotag-preview"); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index e3ec7c457d..6af859416f 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -44,7 +44,7 @@ pub use signature_image::{ImageSignaturePreview, SignatureImage}; pub use signature_scan::{PhotoRect, ScanError, SignaturePhoto, Turn}; pub use text_preview::TextEditPreview; -pub use pdfcraft_organize::{BoxSpec, FieldConflicts, PageBox, SplitBy, split_ranges}; +pub use pdfcraft_organize::{BoxSpec, FieldConflicts, PageBox, SplitBy, Transition, TransitionSpec, split_ranges}; /// One file produced by a split: (1-based first page, last page, PDF bytes). pub use pdfcraft_organize::LabelStyle; @@ -1155,6 +1155,12 @@ pub enum Edit { which: PageBox, spec: BoxSpec, }, + /// Set or remove the page transition and display time of pages (ISO 32000-2 §12.4.4), + /// Organize ▸ Page Transitions. + SetTransition { + pages: Vec, + spec: TransitionSpec, + }, SetInfo { key: String, value: String, @@ -1662,6 +1668,7 @@ pub const EDIT_LABELS: &[&str] = &[ "Crop page", "Crop pages", "Set page boxes", + "Set page transitions", "Change Title", "Change Author", "Change Subject", @@ -1797,6 +1804,7 @@ impl Edit { Edit::ReplacePages { pages, .. } => plural("Replace page", "Replace pages", pages.len()), Edit::SetPageBox { pages, which: PageBox::Crop, .. } => plural("Crop page", "Crop pages", pages.len()), Edit::SetPageBox { .. } => Label::of("Set page boxes"), + Edit::SetTransition { .. } => Label::of("Set page transitions"), // The four `/Info` keys PdfCraft itself offers get a wording of their own, so each can // be said in another language; any other key is the document's and goes in as it stands. Edit::SetInfo { key, .. } => match key.as_str() { @@ -1966,6 +1974,7 @@ fn check_permission(edit: &Edit, p: &pdfcraft_cos::Permissions) -> Result<(), Ed | Edit::DuplicatePages { .. } | Edit::ReplacePages { .. } | Edit::SetPageBox { .. } + | Edit::SetTransition { .. } | Edit::InsertPagesFrom { .. } // "Assemble the document: insert, rotate or delete pages and create bookmarks" (Table 22). | Edit::AddBookmark { .. } @@ -2180,6 +2189,7 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> pdfcraft_organize::replace_pages(doc, pages, &src, src_pages)?; } Edit::SetPageBox { pages, which, spec } => pdfcraft_organize::set_page_box(doc, pages, *which, *spec)?, + Edit::SetTransition { pages, spec } => pdfcraft_organize::set_transition(doc, pages, spec)?, Edit::InsertPagesFrom { name, bytes, pages, at, field_conflicts } => { let src = open_source(name, bytes)?; let pages = match pages { diff --git a/crates/organize/src/lib.rs b/crates/organize/src/lib.rs index b29aba379f..d73815e73e 100644 --- a/crates/organize/src/lib.rs +++ b/crates/organize/src/lib.rs @@ -20,6 +20,7 @@ mod labels; mod outline; mod pdfx; mod prune; +mod transition; pub mod view; mod xmp; @@ -35,6 +36,7 @@ pub use outline::{ Bookmark, BookmarkPage, OutlineEntry, OutlineError, add_bookmark, add_bookmark_tree, bookmark_page, bookmarks, delete_bookmark, move_bookmark, rename_bookmark, set_bookmark_open, set_bookmark_page, top_level_bookmarks, }; +pub use transition::{TRANSITION_DIRECTIONS, TRANSITION_STYLES, Transition, TransitionSpec, set_transition}; pub use view::{InitialView, displays_doc_title, initial_view, set_initial_view}; #[derive(Debug, thiserror::Error, PartialEq)] diff --git a/crates/organize/src/tests.rs b/crates/organize/src/tests.rs index 8ac1f66beb..69360a42e7 100644 --- a/crates/organize/src/tests.rs +++ b/crates/organize/src/tests.rs @@ -267,6 +267,56 @@ fn document_info_edits_round_trip_with_unicode() { assert_eq!(info(&doc, "Producer"), None); } +/// Page transitions (§12.4.4): written as `/Trans` and `/Dur` with only the entries the style +/// reads and that differ from the defaults, removed again, checked before any page changes, and +/// kept through a save. +#[test] +fn page_transitions_are_set_removed_and_checked() { + let mut doc = open(fixture()); + let wipe = Transition { duration: 2.5, direction: Some(270), ..Transition::new("Wipe") }; + set_transition(&mut doc, &[0, 2], &TransitionSpec { transition: Some(wipe), display: Some(5.0) }).unwrap(); + let d = page_dict(&doc, 0); + let t = d.get(b"Trans").and_then(|o| o.as_dict()).expect("/Trans").clone(); + assert_eq!(t.name(b"Type"), Some(&b"Trans"[..])); + assert_eq!(t.name(b"S"), Some(&b"Wipe"[..])); + assert_eq!(t.get(b"D"), Some(&Object::Real(2.5))); + assert_eq!(t.get(b"Di"), Some(&Object::Int(270))); + assert!( + t.get(b"Dm").is_none() && t.get(b"M").is_none() && t.get(b"SS").is_none() && t.get(b"B").is_none(), + "entries Wipe does not read stay out: {t:?}" + ); + assert_eq!(d.get(b"Dur"), Some(&Object::Real(5.0))); + assert!(page_dict(&doc, 1).get(b"Trans").is_none(), "page 2 was not asked for"); + // Fly with everything non-default; the defaults themselves are not written. + let fly = Transition { style: "Fly".into(), duration: 1.0, horizontal: false, inward: false, direction: None, scale: 0.5, opaque: true }; + set_transition(&mut doc, &[1], &TransitionSpec { transition: Some(fly), display: None }).unwrap(); + let t = page_dict(&doc, 1).get(b"Trans").and_then(|o| o.as_dict()).expect("/Trans").clone(); + assert_eq!((t.name(b"S"), t.name(b"M"), t.name(b"Di")), (Some(&b"Fly"[..]), Some(&b"O"[..]), Some(&b"None"[..]))); + assert_eq!((t.get(b"SS"), t.get(b"B")), (Some(&Object::Real(0.5)), Some(&Object::Bool(true)))); + assert!(t.get(b"D").is_none() && t.get(b"Dm").is_none(), "{t:?}"); + // Removed again. + set_transition(&mut doc, &[0], &TransitionSpec { transition: None, display: None }).unwrap(); + let d = page_dict(&doc, 0); + assert!(d.get(b"Trans").is_none() && d.get(b"Dur").is_none()); + // Checked before anything changes. + for (what, spec) in [ + ("style", TransitionSpec { transition: Some(Transition::new("Swirl")), display: None }), + ("direction", TransitionSpec { transition: Some(Transition { direction: Some(45), ..Transition::new("Wipe") }), display: None }), + ("duration", TransitionSpec { transition: Some(Transition { duration: -1.0, ..Transition::new("Fade") }), display: None }), + ("scale", TransitionSpec { transition: Some(Transition { scale: 0.0, ..Transition::new("Fly") }), display: None }), + ("display", TransitionSpec { transition: None, display: Some(f32::NAN) }), + ] { + assert!(set_transition(&mut doc, &[2], &spec).is_err(), "{what} must be refused"); + } + assert!(set_transition(&mut doc, &[7], &TransitionSpec { transition: None, display: None }).is_err(), "no such page"); + let t = page_dict(&doc, 2).get(b"Trans").and_then(|o| o.as_dict()).expect("page 3 unchanged").clone(); + assert_eq!(t.name(b"S"), Some(&b"Wipe"[..])); + // Kept through a save. + let doc = save_and_reopen(&doc); + // (A whole number of seconds may come back as an integer: the writer's choice.) + assert_eq!(page_dict(&doc, 2).get(b"Dur").and_then(Object::as_f64), Some(5.0)); +} + /// A one-page document whose catalog has `xmp` as its (Flate-compressed) XMP packet, and the /// same title and author in `/Info`, as LibreOffice writes them. fn doc_with_xmp(xmp: &[u8]) -> Document { diff --git a/crates/organize/src/transition.rs b/crates/organize/src/transition.rs new file mode 100644 index 0000000000..1960ab0695 --- /dev/null +++ b/crates/organize/src/transition.rs @@ -0,0 +1,133 @@ +//! Page transitions and display times (ISO 32000-2 §12.4.4): the `/Trans` dictionary and `/Dur` +//! entry a full-screen presentation plays, as Acrobat's Organize ▸ Page Transitions sets them. +//! Reading them back is the renderer's `DocInfo::transitions`. + +use pdfcraft_cos::{Dict, Document, Object}; + +use crate::{OrganizeError, check, walk}; + +/// The transition styles of ISO 32000-2 table 164, as written in `/S` (`R` is "replace": no +/// effect). +pub const TRANSITION_STYLES: [&str; 12] = ["Split", "Blinds", "Box", "Wipe", "Dissolve", "Glitter", "R", "Fly", "Push", "Cover", "Uncover", "Fade"]; + +/// The directions `/Di` allows, in degrees. +pub const TRANSITION_DIRECTIONS: [u16; 5] = [0, 90, 180, 270, 315]; + +/// A page transition to write: the `/Trans` dictionary's entries, with the spec's defaults. +#[derive(Clone, Debug, PartialEq)] +pub struct Transition { + /// One of [`TRANSITION_STYLES`]. + pub style: String, + /// `/D`: seconds the transition takes (default 1). + pub duration: f32, + /// `/Dm`: horizontal (`/H`, the default) or vertical (`/V`); Split and Blinds. + pub horizontal: bool, + /// `/M`: inward (`/I`, the default) or outward (`/O`); Split, Box and Fly. + pub inward: bool, + /// `/Di`: one of [`TRANSITION_DIRECTIONS`] (default 0), or `None` for `/None` (Fly only); + /// Wipe, Glitter, Fly, Cover, Uncover and Push. + pub direction: Option, + /// `/SS`: the Fly scale (default 1). + pub scale: f32, + /// `/B`: Fly with an opaque backdrop. + pub opaque: bool, +} + +impl Transition { + /// A transition of `style` with every other entry at its default. + pub fn new(style: &str) -> Self { + Self { style: style.to_string(), duration: 1.0, horizontal: true, inward: true, direction: Some(0), scale: 1.0, opaque: false } + } +} + +/// What a page gets: its transition (`None` removes `/Trans`) and the seconds it stays before the +/// presentation advances (`None` removes `/Dur`: the page waits for the presenter). +#[derive(Clone, Debug, PartialEq)] +pub struct TransitionSpec { + pub transition: Option, + pub display: Option, +} + +/// Set (or remove) the transition and display time of `pages` (0-based). Every value is checked +/// before any page changes: an unknown style, a direction that is not one of the five, a negative +/// or non-finite duration, scale or display time are refused. +pub fn set_transition(doc: &mut Document, pages: &[usize], spec: &TransitionSpec) -> Result<(), OrganizeError> { + let bad = |m: String| OrganizeError::Invalid(m); + if let Some(t) = &spec.transition { + if !TRANSITION_STYLES.contains(&t.style.as_str()) { + return Err(bad(format!("unknown transition style {:?}; one of {}", t.style, TRANSITION_STYLES.join(", ")))); + } + if !(t.duration.is_finite() && t.duration >= 0.0) { + return Err(bad("the transition duration must be zero or more seconds".into())); + } + if let Some(d) = t.direction + && !TRANSITION_DIRECTIONS.contains(&d) + { + return Err(bad(format!("the direction must be one of {:?} degrees", TRANSITION_DIRECTIONS))); + } + if !(t.scale.is_finite() && t.scale > 0.0) { + return Err(bad("the Fly scale must be more than zero".into())); + } + } + if let Some(s) = spec.display + && !(s.is_finite() && s >= 0.0) + { + return Err(bad("the display time must be zero or more seconds".into())); + } + let all = walk(doc)?; + check(pages, all.len())?; + let trans = spec.transition.as_ref().map(trans_dict); + for &i in pages { + let Some((page, _)) = all.get(i) else { continue }; + let trans = trans.clone(); + doc.update_dict(*page, |d| { + match trans { + Some(t) => d.set(b"Trans".to_vec(), Object::Dict(t)), + None => { + d.remove(b"Trans"); + } + } + match spec.display { + Some(s) => d.set(b"Dur".to_vec(), Object::Real(f64::from(s))), + None => { + d.remove(b"Dur"); + } + } + })?; + } + Ok(()) +} + +/// The `/Trans` dictionary for `t`: only the entries that differ from the defaults, and only the +/// ones the style reads (table 164), so the file says no more than it means. +fn trans_dict(t: &Transition) -> Dict { + let style = t.style.as_str(); + let mut d = Dict::new(); + d.set(b"Type".to_vec(), Object::name("Trans")); + d.set(b"S".to_vec(), Object::name(style)); + if t.duration != 1.0 { + d.set(b"D".to_vec(), Object::Real(f64::from(t.duration))); + } + if matches!(style, "Split" | "Blinds") && !t.horizontal { + d.set(b"Dm".to_vec(), Object::name("V")); + } + if matches!(style, "Split" | "Box" | "Fly") && !t.inward { + d.set(b"M".to_vec(), Object::name("O")); + } + if matches!(style, "Wipe" | "Glitter" | "Fly" | "Cover" | "Uncover" | "Push") { + match t.direction { + None if style == "Fly" => d.set(b"Di".to_vec(), Object::name("None")), + Some(deg) if deg != 0 => d.set(b"Di".to_vec(), Object::Int(i64::from(deg))), + _ => {} + } + } + if style == "Fly" { + if t.scale != 1.0 { + d.set(b"SS".to_vec(), Object::Real(f64::from(t.scale))); + } + if t.opaque { + d.set(b"B".to_vec(), Object::Bool(true)); + } + } + d +} diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 0d597de4f2..ff03c8a2f7 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -370,6 +370,7 @@ Split تقسيم More المزيد Set page boxes ضبط مربعات الصفحة + Set page transitions تعيين انتقالات الصفحات Page labels تسميات الصفحات Page transitions انتقالات الصفحات Add comments إضافة تعليقات diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 4ea97a5aac..132a98e8e1 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -980,6 +980,7 @@ Replace Замени Split Раздели Set page boxes Задай полета на страницата + Set page transitions Задаване на преходи между страници Page labels Етикети на страниците Page transitions Преходи между страниците Sticky note Бележка diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 97876c3b95..1100da1924 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -451,6 +451,7 @@ key Space Mezerník Split Rozdělit More Další Set page boxes Nastavit rámečky stránek + Set page transitions Nastavit přechody stránek Page labels Označení stránek Page transitions Přechody stránek Add comments Přidat komentáře diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index e86d487fda..45431f9ff6 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -343,6 +343,7 @@ Split Teilen More Mehr Set page boxes Seitenrahmen festlegen + Set page transitions Seitenübergänge festlegen Page labels Seitenbeschriftungen Page transitions Seitenübergänge Add comments Kommentare hinzufügen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index f12a553344..54929c4c58 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1712,6 +1712,7 @@ Page grid Πλέγμα σελίδων Extract Εξαγωγή Set page boxes Ορισμός πλαισίων σελίδας + Set page transitions Ορισμός εναλλαγών σελίδων Page transitions Μεταβάσεις σελίδων Add comments Προσθήκη σχολίων Markup Σημειώσεις επισήμανσης diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 5fada5d9ad..52e7c9c3ed 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -366,6 +366,7 @@ Split Dividir More Más Set page boxes Definir cuadros de página + Set page transitions Definir transiciones de página Page labels Etiquetas de página Page transitions Transiciones de página Add comments Añadir comentarios diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 1ebdea307b..65bbc97b2a 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -343,6 +343,7 @@ Split Fractionner More Plus Set page boxes Définir les zones de page + Set page transitions Définir les transitions de page Page labels Étiquettes de page Page transitions Transitions de page Add comments Ajouter des commentaires diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 702d4e3dd0..7f7433e916 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -341,6 +341,7 @@ Split Felosztás More Továbbiak Set page boxes Oldalkeretek beállítása + Set page transitions Oldalátmenetek beállítása Page labels Oldalcímkék Page transitions Oldalváltások Add comments Megjegyzések hozzáadása diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 85065c90b7..45cbed1009 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -367,6 +367,7 @@ Split Dividi More Altro Set page boxes Imposta riquadri pagina + Set page transitions Imposta transizioni di pagina Page labels Etichette pagina Page transitions Transizioni pagina Add comments Aggiungi commenti diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 9db76cf4dc..50c0a85b02 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -370,6 +370,7 @@ Split 分割 More その他 Set page boxes ページボックスを設定 + Set page transitions ページ切り替え効果を設定 Page labels ページラベル Page transitions ページ切り替え効果 Add comments コメントを追加 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index 3c492d9355..fe8ec4de09 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -330,6 +330,7 @@ Split Бөлу More Қосымша Set page boxes Бет шектерін орнату + Set page transitions Бет ауысуларын орнату Page labels Бет белгілері Page transitions Бет ауысуы Add comments Түсініктеме қосу diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index cde7b99142..9a7f6c0167 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -1549,6 +1549,7 @@ Separate PDFs Atskiri PDF dokumentai Set measurement scale Nustatyti matavimo mastelį Set page boxes Nustatyti puslapio sritis + Set page transitions Nustatyti puslapių perėjimus Set page boxes… Nustatyti puslapio sritis… Set properties Nustatyti savybes Show cover page in two-page view Rodyti viršelio puslapį dviejų puslapių rodinyje diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index daebec9734..c45233909b 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -365,6 +365,7 @@ Split Podziel More Więcej Set page boxes Ustaw obszary strony + Set page transitions Ustaw przejścia stron Page labels Etykiety stron Page transitions Przejścia stron Add comments Dodaj komentarze diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 3b8d33d56a..ab945a8861 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -924,6 +924,7 @@ Replace Substituir Split Dividir Set page boxes Definir limites da página + Set page transitions Definir transições de página Page labels Rótulos de página Page transitions Transições de página Sticky note Nota adesiva diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 3864249586..4bda940f01 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -363,6 +363,7 @@ Split Разделить More Ещё Set page boxes Задать поля страниц + Set page transitions Задать переходы между страницами Page labels Метки страниц Page transitions Переходы страниц Add comments Добавить комментарии diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index aca66b0dbf..befa698159 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -1633,6 +1633,7 @@ Set layer visibility Nastavi vidnost plasti Set measurement scale Nastavi merilo meritev Set page boxes Nastavi robove strani + Set page transitions Nastavi prehode strani Set page boxes… Nastavi robove strani… Set properties Nastavi lastnosti Set status Nastavi status diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 25f2a5ecde..cc426640e9 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -347,6 +347,7 @@ comment menu Edit సవరించు Split విభజించు More మరిన్ని Set page boxes పేజీ బాక్స్‌లను సెట్ చేయి + Set page transitions పేజీ పరివర్తనలను సెట్ చేయండి Page labels పేజీ లేబుల్‌లు Page transitions పేజీ ట్రాన్సిషన్‌లు Add comments వ్యాఖ్యలను జోడించు diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 6ee205cafa..a0cfef44fc 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -328,6 +328,7 @@ Split Böl More Daha fazla Set page boxes Sayfa kutularını ayarla + Set page transitions Sayfa geçişlerini ayarla Page labels Sayfa etiketleri Page transitions Sayfa geçişleri Add comments Yorum ekle diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 1af91509da..2d3a550986 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -368,6 +368,7 @@ Split Розділити More Більше Set page boxes Задати межі сторінок + Set page transitions Задати переходи між сторінками Page labels Позначки сторінок Page transitions Переходи між сторінками Add comments Додати коментарі diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 8a70d693d2..4a82e732c9 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -987,6 +987,7 @@ Replace 替换 Split 拆分 Set page boxes 设置页面框 + Set page transitions 设置页面切换效果 Page labels 页面标签 Page transitions 页面切换 Sticky note 附注 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 40335829e9..cda6964d6d 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -932,6 +932,7 @@ Replace 取代 Split 分割 Set page boxes 設定頁面方框 + Set page transitions 設定換頁效果 Page labels 頁面標籤 Page transitions 頁面轉場 Sticky note 便利貼註解 diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 2b9fada35e..bd0756f328 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -3198,6 +3198,9 @@ commands = [] tools = ["doc_info"] evidence = ["crates/render/src/inspect.rs::page_transitions_and_display_times_are_read", "crates/automation/tests/automation.rs::doc_info_lists_page_transitions_and_display_times"] notes = "Read only: each page's /Trans (style, duration, dimension, motion, direction, Fly scale and backdrop) and /Dur display time are read with the spec's defaults for what is missing or out of range, and doc_info lists them (DocInfo::transitions). Not yet: playing them in full screen, auto-advancing on /Dur, or the Page Transitions dialog that sets them" +tools = [] +evidence = [] +notes = "Read and set, not played: each page's /Trans (style, duration, dimension, motion, direction, Fly scale and backdrop) and /Dur display time are read with the spec's defaults and listed by doc_info (DocInfo::transitions); page_transition_set (Edit::SetTransition, one undo step) writes or removes them on chosen pages with the values checked first. Not yet: playing them in full screen, auto-advancing on /Dur, and the Page Transitions dialog" [[feature]] id = "organize.page-tab-order"