mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
M4: read page transitions and display times (/Trans, /Dur) into DocInfo (#2074)
Each page's presentation settings of ISO 32000-2 s12.4.4 are read with the spec's defaults for what is missing or out of range: the transition style, duration, dimension, motion, direction, Fly scale and backdrop, and the /Dur display time. doc_info lists them; pdfcraft-cli info counts them. This is the data full screen needs to play transitions and auto-advance; playing and editing them stay planned. Refs #379, #2070. Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
f00a84121b
commit
8772db0a6e
@@ -168,6 +168,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
- **2026-10-13 (M3, Single Page edge paging, #1924):** In Single Page view a page taller than the window pans with the wheel, and one more notch (or a fresh trackpad swipe) at the bottom/top edge shows the next page at its top / the previous page at its bottom. A swipe that panned to the edge, momentum included, does not turn. Estimates unchanged.
|
||||
- **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-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.
|
||||
|
||||
@@ -312,6 +312,7 @@ fn info(args: &[String]) -> Result<(), CliError> {
|
||||
"xfa_layout": xfa_layout,
|
||||
"bookmarks": info.outline.len(), "annotations": info.annotations.len(), "fields": info.fields.len(),
|
||||
"links": info.links.len(), "layers": info.layers.len(), "attachments": info.attachments.len(),
|
||||
"transitions": info.transitions.len(),
|
||||
"page_labels": info.pages.iter().take(8).map(|p| p.label.clone()).collect::<Vec<_>>(),
|
||||
"warnings": info.warnings,
|
||||
});
|
||||
|
||||
@@ -2258,6 +2258,13 @@ fn info(d: &Document) -> Value {
|
||||
},
|
||||
})).collect::<Vec<_>>(),
|
||||
"layers": i.layers.iter().map(|l| json!({ "name": l.name, "visible": l.visible })).collect::<Vec<_>>(),
|
||||
// Full-screen presentation settings a page carries (ISO 32000-2 §12.4.4): only pages with
|
||||
// a `/Trans` or `/Dur` appear; `style` is null for a display time alone.
|
||||
"transitions": i.transitions.iter().map(|t| json!({
|
||||
"page": page1(t.page), "style": t.style.map(pdfcraft_render::TransitionStyle::name), "duration": t.duration,
|
||||
"horizontal": t.horizontal, "inward": t.inward, "direction": t.direction, "scale": t.scale, "opaque": t.opaque,
|
||||
"display": t.display,
|
||||
})).collect::<Vec<_>>(),
|
||||
"attachments": i.attachments.iter().map(|a| json!({ "name": a.name, "description": a.description, "size": a.size })).collect::<Vec<_>>(),
|
||||
// `substitute` names the face standing in for a program the document does not embed, and
|
||||
// `substitution` says why. Both are null for an embedded font: nothing stands in. A null
|
||||
|
||||
@@ -133,7 +133,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
.cmd("file.open")
|
||||
.with(schema(json!({ "path": { "type": "string" }, "password": { "type": "string", "description": "User or owner password for encrypted files." } }), &["path"])),
|
||||
t("doc_list", "List open documents", "List the open documents with their ids, page counts and unsaved state.").ro().with(schema(json!({}), &[])),
|
||||
t("doc_info", "Inspect a document", "Metadata, page sizes and labels, bookmarks, annotations, form fields, links, layers, attachments, fonts, security and repair notes.")
|
||||
t("doc_info", "Inspect a document", "Metadata, page sizes and labels, bookmarks, annotations, form fields, links, layers, page transitions and display times, attachments, fonts, security and repair notes.")
|
||||
.ro()
|
||||
.cmd("file.properties")
|
||||
.with(schema(json!({ "doc": doc() }), &["doc"])),
|
||||
|
||||
@@ -115,6 +115,45 @@ fn auto(dir: &Path) -> Automation {
|
||||
Automation::new().with_root(dir).unwrap().with_clock(|| 1_700_000_000)
|
||||
}
|
||||
|
||||
/// Full-screen presentation settings (`/Trans`, `/Dur`) reach `doc_info`, 1-based pages, only
|
||||
/// for the pages that carry them.
|
||||
#[test]
|
||||
fn doc_info_lists_page_transitions_and_display_times() {
|
||||
let dir = workdir("transitions");
|
||||
let objects: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 /MediaBox [0 0 200 300] >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Page /Parent 2 0 R /Trans << /S /Fade /D 0.5 >> /Dur 4 >>".to_vec(),
|
||||
];
|
||||
let mut bytes = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, object) in objects.iter().enumerate() {
|
||||
offsets.push(bytes.len());
|
||||
bytes.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
|
||||
bytes.extend_from_slice(object);
|
||||
bytes.extend_from_slice(b"\nendobj\n");
|
||||
}
|
||||
let xref = bytes.len();
|
||||
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes());
|
||||
for o in offsets {
|
||||
bytes.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
bytes.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes());
|
||||
std::fs::write(dir.join("slides.pdf"), bytes).unwrap();
|
||||
let mut a = auto(&dir);
|
||||
let doc = ok(&mut a, "doc_open", json!({ "path": "slides.pdf" }))["doc"].as_u64().unwrap();
|
||||
let info = ok(&mut a, "doc_info", json!({ "doc": doc }));
|
||||
let t = info["transitions"].as_array().unwrap();
|
||||
assert_eq!(t.len(), 1, "{}", info["transitions"]);
|
||||
assert_eq!(
|
||||
(t[0]["page"].as_u64(), t[0]["style"].as_str(), t[0]["duration"].as_f64(), t[0]["display"].as_f64()),
|
||||
(Some(2), Some("Fade"), Some(0.5), Some(4.0)),
|
||||
"{}",
|
||||
t[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autotag_preview_proposes_text_without_changing_the_pdf() {
|
||||
let dir = workdir("autotag-preview");
|
||||
|
||||
@@ -76,6 +76,8 @@ pub struct DocInfo {
|
||||
pub layer_groups: Vec<Vec<(u32, u16)>>,
|
||||
pub fonts: Vec<FontInfo>,
|
||||
pub attachments: Vec<Attachment>,
|
||||
/// Pages with a presentation transition (`/Trans`) or display time (`/Dur`), in page order.
|
||||
pub transitions: Vec<PageTransition>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -90,6 +92,89 @@ pub enum Xfa {
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
/// A page's presentation transition and display time (ISO 32000-2 §12.4.4): how the page comes
|
||||
/// into view in a full-screen presentation, and how long it stays before advancing. Only pages
|
||||
/// carrying `/Trans` or `/Dur` are listed. Values outside the spec's ranges fall back to the
|
||||
/// defaults rather than being refused.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PageTransition {
|
||||
pub page: usize,
|
||||
/// `/Trans /S`; `None` when the page has only `/Dur`.
|
||||
pub style: Option<TransitionStyle>,
|
||||
/// `/D`: how long the transition takes, in seconds (default 1).
|
||||
pub duration: f32,
|
||||
/// `/Dm /H` (true) or `/V` (Split, Blinds).
|
||||
pub horizontal: bool,
|
||||
/// `/M /I` (true) or `/O` (Split, Box, Fly).
|
||||
pub inward: bool,
|
||||
/// `/Di`: 0, 90, 180, 270 or 315 degrees (Wipe, Glitter, Fly, Cover, Uncover, Push); `None`
|
||||
/// for `/None` (Fly only).
|
||||
pub direction: Option<u16>,
|
||||
/// `/SS`: the Fly scale (default 1).
|
||||
pub scale: f32,
|
||||
/// `/B`: Fly with an opaque backdrop.
|
||||
pub opaque: bool,
|
||||
/// `/Dur`: seconds the page shows before the presentation advances; `None` waits for the
|
||||
/// presenter.
|
||||
pub display: Option<f32>,
|
||||
}
|
||||
|
||||
/// The transition styles of ISO 32000-2 table 164 (`/Trans /S`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TransitionStyle {
|
||||
Split,
|
||||
Blinds,
|
||||
Box,
|
||||
Wipe,
|
||||
Dissolve,
|
||||
Glitter,
|
||||
/// `/R`: the next page replaces the old one with no effect (the default).
|
||||
Replace,
|
||||
Fly,
|
||||
Push,
|
||||
Cover,
|
||||
Uncover,
|
||||
Fade,
|
||||
}
|
||||
|
||||
impl TransitionStyle {
|
||||
/// The style a `/S` name means; an unknown name is `Replace`, the spec's default.
|
||||
pub fn from_name(name: &str) -> Self {
|
||||
match name {
|
||||
"Split" => Self::Split,
|
||||
"Blinds" => Self::Blinds,
|
||||
"Box" => Self::Box,
|
||||
"Wipe" => Self::Wipe,
|
||||
"Dissolve" => Self::Dissolve,
|
||||
"Glitter" => Self::Glitter,
|
||||
"Fly" => Self::Fly,
|
||||
"Push" => Self::Push,
|
||||
"Cover" => Self::Cover,
|
||||
"Uncover" => Self::Uncover,
|
||||
"Fade" => Self::Fade,
|
||||
_ => Self::Replace,
|
||||
}
|
||||
}
|
||||
|
||||
/// The name as written in a PDF (`/S`).
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Split => "Split",
|
||||
Self::Blinds => "Blinds",
|
||||
Self::Box => "Box",
|
||||
Self::Wipe => "Wipe",
|
||||
Self::Dissolve => "Dissolve",
|
||||
Self::Glitter => "Glitter",
|
||||
Self::Replace => "R",
|
||||
Self::Fly => "Fly",
|
||||
Self::Push => "Push",
|
||||
Self::Cover => "Cover",
|
||||
Self::Uncover => "Uncover",
|
||||
Self::Fade => "Fade",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PageInfo {
|
||||
/// Displayed size in points, after `/Rotate` and `/UserUnit`.
|
||||
@@ -548,6 +633,7 @@ impl<'a> Inspector<'a> {
|
||||
(info.outline, info.outline_more) = self.outline_window(first);
|
||||
}
|
||||
self.annotations(info);
|
||||
self.transitions(info);
|
||||
let mut field_seen = HashSet::new();
|
||||
if let Some(form) = catalog.get(b"AcroForm").ok().and_then(|o| self.dict(o))
|
||||
&& let Ok(fields) = form.get(b"Fields").and_then(|o| self.resolve(o).as_array())
|
||||
@@ -596,6 +682,53 @@ impl<'a> Inspector<'a> {
|
||||
|
||||
// ── object helpers ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// `/Trans` and `/Dur` of every page (ISO 32000-2 §12.4.4), in page order. Lenient: a value
|
||||
/// outside the spec's choices takes that entry's default, and a page with neither entry is
|
||||
/// not listed. Bounded by the page count.
|
||||
fn transitions(&self, info: &mut DocInfo) {
|
||||
for (page, &pid) in self.page_ids.iter().enumerate() {
|
||||
let Ok(d) = self.doc.get_dictionary(pid) else { continue };
|
||||
let display = d.get(b"Dur").ok().and_then(|o| self.resolve(o).as_float().ok()).filter(|s| s.is_finite() && *s >= 0.0);
|
||||
let trans = d.get(b"Trans").ok().and_then(|o| self.dict(o));
|
||||
if trans.is_none() && display.is_none() {
|
||||
continue;
|
||||
}
|
||||
let mut t = PageTransition {
|
||||
page,
|
||||
style: None,
|
||||
duration: 1.0,
|
||||
horizontal: true,
|
||||
inward: true,
|
||||
direction: Some(0),
|
||||
scale: 1.0,
|
||||
opaque: false,
|
||||
display,
|
||||
};
|
||||
if let Some(tr) = trans {
|
||||
t.style = Some(self.name(tr, b"S").map_or(TransitionStyle::Replace, |n| TransitionStyle::from_name(&n)));
|
||||
let num = |key: &[u8]| tr.get(key).ok().and_then(|o| self.resolve(o).as_float().ok()).filter(|v| v.is_finite());
|
||||
t.duration = num(b"D").filter(|d| *d >= 0.0).unwrap_or(1.0);
|
||||
t.horizontal = self.name(tr, b"Dm").as_deref() != Some("V");
|
||||
t.inward = self.name(tr, b"M").as_deref() != Some("O");
|
||||
t.direction = match tr.get(b"Di").map(|o| self.resolve(o)) {
|
||||
Ok(Object::Name(n)) if n.as_slice() == b"None" => None,
|
||||
Ok(o) => match o.as_i64().ok() {
|
||||
Some(0) => Some(0),
|
||||
Some(90) => Some(90),
|
||||
Some(180) => Some(180),
|
||||
Some(270) => Some(270),
|
||||
Some(315) => Some(315),
|
||||
_ => Some(0),
|
||||
},
|
||||
Err(_) => Some(0),
|
||||
};
|
||||
t.scale = num(b"SS").filter(|s| *s > 0.0).unwrap_or(1.0);
|
||||
t.opaque = matches!(tr.get(b"B").map(|o| self.resolve(o)), Ok(Object::Boolean(true)));
|
||||
}
|
||||
info.transitions.push(t);
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve(&self, o: &'a Object) -> &'a Object {
|
||||
self.doc.dereference(o).map(|(_, o)| o).unwrap_or(o)
|
||||
}
|
||||
@@ -2049,6 +2182,46 @@ trailer << /Root 1 0 R >>
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF";
|
||||
|
||||
/// `/Trans` and `/Dur` (ISO 32000-2 §12.4.4): read with the spec's defaults for what a page
|
||||
/// leaves out, lenient about what is out of range, and only for pages that have either.
|
||||
const TRANSITIONS: &[u8] = b"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R 5 0 R 6 0 R 7 0 R] /Count 5 /MediaBox [0 0 200 200] >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /Trans << /Type /Trans /S /Wipe /D 2.5 /Di 270 >> /Dur 5 >> endobj
|
||||
4 0 obj << /Type /Page /Parent 2 0 R /Dur 3 >> endobj
|
||||
5 0 obj << /Type /Page /Parent 2 0 R /Trans 8 0 R >> endobj
|
||||
6 0 obj << /Type /Page /Parent 2 0 R /Trans << /S /Bogus /D (slow) /Di 45 /Dm /Q >> /Dur -1 >> endobj
|
||||
7 0 obj << /Type /Page /Parent 2 0 R >> endobj
|
||||
8 0 obj << /S /Fly /M /O /SS 0.5 /B true /Di /None /Dm /V >> endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF";
|
||||
|
||||
#[test]
|
||||
fn page_transitions_and_display_times_are_read() {
|
||||
use super::TransitionStyle as S;
|
||||
let info = inspect(Arc::new(TRANSITIONS.to_vec()), None).expect("opens");
|
||||
let t = &info.transitions;
|
||||
assert_eq!(t.iter().map(|t| t.page).collect::<Vec<_>>(), [0, 1, 2, 3], "pages with /Trans or /Dur, in order; page 5 has neither");
|
||||
assert_eq!((t[0].style, t[0].duration, t[0].direction, t[0].display), (Some(S::Wipe), 2.5, Some(270), Some(5.0)));
|
||||
assert!((t[0].horizontal, t[0].inward, t[0].scale, t[0].opaque) == (true, true, 1.0, false), "defaults: {:?}", t[0]);
|
||||
assert_eq!((t[1].style, t[1].duration, t[1].display), (None, 1.0, Some(3.0)), "/Dur without /Trans");
|
||||
assert_eq!(
|
||||
(t[2].style, t[2].inward, t[2].scale, t[2].opaque, t[2].direction, t[2].horizontal),
|
||||
(Some(S::Fly), false, 0.5, true, None, false),
|
||||
"an indirect /Trans: {:?}",
|
||||
t[2]
|
||||
);
|
||||
// Out of range: an unknown style is the spec's default R, a non-numeric /D the default
|
||||
// second, a direction that is not one of the five falls back to 0, a bad /Dm to
|
||||
// horizontal, and a negative /Dur is no display time.
|
||||
assert_eq!(
|
||||
(t[3].style, t[3].duration, t[3].direction, t[3].horizontal, t[3].display),
|
||||
(Some(S::Replace), 1.0, Some(0), true, None),
|
||||
"{:?}",
|
||||
t[3]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radio_button_layer_groups_are_read() {
|
||||
let info = inspect(Arc::new(LAYERS.to_vec()), None).expect("opens");
|
||||
|
||||
@@ -17,7 +17,7 @@ pub mod text;
|
||||
|
||||
pub use inspect::{
|
||||
Annotation, Attachment, AttachmentSource, DestView, DocInfo, Field, FieldKind, FontInfo, Layer, LayerOp, Link, LinkTarget, OutlineItem, PageInfo,
|
||||
RemoteDestination, Xfa, attachment_data, check_pages, inspect, pretty_date,
|
||||
PageTransition, RemoteDestination, TransitionStyle, Xfa, attachment_data, check_pages, inspect, pretty_date,
|
||||
};
|
||||
pub use pixels::Pixels;
|
||||
pub use raster::{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PDF specification and standards parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (§7.6.4: written encrypted files open in Acrobat, #774) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (§12.4.4: page transitions and display times read) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
|
||||
How much of ISO 32000-2 (PDF 2.0) and the PDF subset standards PdfCraft implements, set against
|
||||
what Acrobat Pro implements. This is PdfCraft's equivalent of a "geometry" or "codec" checklist:
|
||||
@@ -35,7 +35,7 @@ are borrowed; PDF 2.0 additions and most subset standards are missing. **150–2
|
||||
| §10 | Rendering: transfer functions, halftones, overprint, smoothness | full | borrowed; no overprint preview | `print.overprint-preview` planned |
|
||||
| §11 | Transparency: blend modes, soft masks, groups, knockout | full | borrowed (knockout shipped) | `view.render-transparency` partial |
|
||||
| §12.1–12.3 | Viewer preferences, page layout, page labels, outlines, destinations | full | **own**: initial view, page labels, bookmarks edit and from structure, named destinations rewired on copy | named-destination editing panel planned |
|
||||
| §12.4 | Page transitions, thumbnails, articles, presentations | full | none (thumbnails discarded or kept) | `organize.page-transitions`, `view.articles-panel` planned |
|
||||
| §12.4 | Page transitions, thumbnails, articles, presentations | full | `/Trans` and `/Dur` read and listed by `doc_info`, not played; thumbnails discarded or kept | `organize.page-transitions` partial, `view.articles-panel` planned |
|
||||
| §12.5 | Annotations | 28 types | see table below | |
|
||||
| §12.6 | Actions | 19 types | see table below | |
|
||||
| §12.7 | Interactive forms (AcroForm): fields, appearances, calculation order, JavaScript actions | full | **own**: all field types, regenerated appearances, Acrobat's AF functions and event order, sandboxed JavaScript (boa) | `form.js-host` partial (object-model subset); submit planned |
|
||||
@@ -130,5 +130,6 @@ are borrowed; PDF 2.0 additions and most subset standards are missing. **150–2
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | minor | §12.4.4: `/Trans` and `/Dur` read into `DocInfo::transitions` and reported by `doc_info` |
|
||||
| 2026-10-11 | minor | §7.6.4: encrypted files we write open in Acrobat (#774, fixed by #836) |
|
||||
| 2026-10-10 | major | Created from the code, `vendor/README.md` and `parity/acrobat-features.toml`, against ISO 32000-2 and Acrobat Pro 26.002.21931's documented capabilities |
|
||||
|
||||
@@ -3193,11 +3193,11 @@ title = "Page transitions"
|
||||
area = "D"
|
||||
tier = "P2"
|
||||
milestone = "M4"
|
||||
status = "planned"
|
||||
status = "partial"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
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"
|
||||
|
||||
[[feature]]
|
||||
id = "organize.page-tab-order"
|
||||
|
||||
Reference in New Issue
Block a user