Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| 2026-10-24 | Preferences ▸ Measurement units: inches or centimetres. The Format pane's Size and Position fields, Shape Format ▸ Size on the ribbon, the Slide Size dialog and the ruler show and take the chosen unit; the deck still stores points (#1326) |
| 2026-10-23 | Chart text colour: `chart.options {textColor}` and Font Color on a selected chart colour its title, axes, legend and data labels; the colour is written to the chart part (`c:txPr` on the axes, legend, labels and chart, the title's run defaults) so an imported chart keeps its `txPr` colours through an edit and a save; text without a colour of its own takes PowerPoint's default, the theme's text colour at 65 % plus 35 % (595959 on a light theme, light grey on a dark one) |
| 2026-10-24 | A picture's own fill shows behind its transparent pixels, and an embedded object (an old equation, `p:oleObj`) takes the fill and outline of its fallback picture, as PowerPoint for Mac draws them (synthetic probe: a half-transparent PNG on orange and on yellow with an outline); on a private 166-deck corpus 51 slides of equations on coloured boxes move closer to PowerPoint's PDF, none further |
| 2026-10-18 | `file.export {format: "frames"}` writes the slide show as a PNG frame sequence for a video encoder: build steps play through their timelines with holds, slide transitions play between slides (the show's own frame composition, now shared through `deckcraft_anim::state_for`), frame and pixel caps, and an answer mapping frames back to slides |
| 2026-10-23 | Format Background ▸ Apply to All also sets the background of the slide masters the slides use (and clears their layouts' overrides), so new slides keep it (#891) |
| 2026-10-22 | Table text takes the master's other-text style (`otherStyle`), shapes and text boxes the presentation's default text style, as PowerPoint for Mac draws them (synthetic probe: 14 pt cells under a 14 pt `otherStyle` and a 26 pt `defaultTextStyle`); a private deck's table had been drawn 20 pt instead of 18 |
| 2026-10-18 | Find (Ctrl+F): "Find what:" has the keyboard as soon as the dialog opens, and keeps it after Enter finds a match, so the search can be typed and changed without clicking the field (#951) |
Expand Down
4 changes: 3 additions & 1 deletion apps/deckcraft-cli/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ everything; .pptx, .pdf and .md (the outline) are written as convert writes them
current slide as .png or .jpg, or the presentation as .pdf, .pptx or an animated .gif (one frame per
slide). --export-params passes file.export options as one JSON object, such as
'{\"width\": \"large\", \"secondsPerSlide\": 3, \"loop\": false, \"slides\": [0, 1, 2]}' for a GIF
(`deckcraft-cli describe file.export` lists them).
(`deckcraft-cli describe file.export` lists them). A video's frames (the show with its builds and
transitions, one PNG per frame in a folder) are exported with a command:
--cmd 'file.export={\"format\":\"frames\",\"path\":\"frames/\",\"fps\":30}'.

Each --cmd is one command: COMMAND is a command id, then `=`, then the command's parameters as one
JSON object. Quote it for the shell. A command that needs no parameters is just its id.
Expand Down
2 changes: 1 addition & 1 deletion crates/anim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub use interact::{Click, HitShape, Target};
pub use mesh::{MeshVertex, layer_mesh};
pub use morph::{MorphFrame, MorphPath, MorphText, morph_crop, morph_frame, morph_pairs, morph_xfrm};
pub use path::{MotionPath, default_path};
pub use show::{LinkJump, ShowAction, ShowState, safe_url, show_order};
pub use show::{LinkJump, ShapeFrame, ShowAction, ShowState, safe_url, show_order, state_for};
pub use timeline::{EffectTiming, Sequence, StepStamp, Timeline, TriggerPlay};
pub use transition::{DEFAULT_ASPECT, Layer, Source, transition_frame, transition_layers, uses_slide_parts};

Expand Down
23 changes: 21 additions & 2 deletions crates/anim/src/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,29 @@
use std::collections::{BTreeMap, HashMap};

use deckcraft_model::text::{Action, Hyperlink};
use deckcraft_model::{Presentation, SlideId};
use deckcraft_model::{Presentation, ShapeId, SlideId};

use crate::Timeline;
use crate::interact::SlideRun;
use crate::{AnimState, StepStamp, Timeline, TriggerPlay};

/// What one shape looks like at one moment of the show: its own state and, for by-paragraph text
/// builds, the state of each animated paragraph relative to the shape's (paragraph index, state).
#[derive(Clone, Debug, PartialEq)]
pub struct ShapeFrame {
pub state: AnimState,
pub paras: Vec<(usize, AnimState)>,
}

/// Shape `id` of timeline `tl` when `step` main steps are done and step `step` has been playing
/// for `t` seconds, with the triggered sequences at `plays` applied in the order of `stamps`
/// ([`Timeline::state_ordered`]); its paragraphs follow [`Timeline::state`]. The slide show and
/// the frame-sequence export draw every frame from this.
pub fn state_for(tl: &Timeline, id: ShapeId, step: usize, t: f64, plays: &[TriggerPlay], stamps: &[StepStamp]) -> ShapeFrame {
ShapeFrame {
state: tl.state_ordered(id, None, step, t, plays, stamps),
paras: tl.paragraph_targets(id).into_iter().map(|p| (p, tl.state(id, Some(p), step, t))).collect(),
}
}

/// What the UI should do after a navigation call.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
Expand Down
2 changes: 1 addition & 1 deletion crates/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ deckcraft-pdf = { workspace = true }
deckcraft-format = { workspace = true }
deckcraft-pptx = { workspace = true }
deckcraft-media = { workspace = true }
deckcraft-anim = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
i_overlay = { workspace = true }
Expand All @@ -28,7 +29,6 @@ image = { workspace = true }
chrono = { workspace = true }

[dev-dependencies]
deckcraft-anim = { workspace = true }
zip = { workspace = true }

# The browser clock for comment timestamps (wasm has no std::time::SystemTime).
Expand Down
26 changes: 25 additions & 1 deletion crates/engine/src/cmd/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!(noundo "file.saveAs", "Save As…", ["File"], Some("Cmd+Shift+S"), "{path, format?}", has_doc, save_as),
cmd!(noundo "file.saveTemplate", "Save as Template…", ["File"], None, "{path}", has_doc, save_as),
cmd!(query "file.saveBytes", "Save to Data", [], None, "{format?: deckcraft|pptx} → {data: base64}", has_doc, save_bytes_cmd),
cmd!(noundo "file.export", "Export…", ["File"], None, "{path, format?: png|jpeg|pptx|deckcraft|outline|rtf|pdf|gif (else by path), slide?: index, all?: bool, scale?: px per pt, layout?: slides|notes|handouts (pdf), perPage?: 1|2|3|4|6|9, paper?: letter|a4|legal|a3 (pdf), orientation?: portrait|landscape (pdf), dpi?, slides?: [index]|range (\"1-3,5\"), includeHidden?, textLayer?, frame?, transparentBackground?: bool (png: leave the slide background out), width?: px 16-3840|small|medium|large|extraLarge (gif), secondsPerSlide?: 0.1-600 (gif; 5), loop?: bool (gif; true: forever)} → {path, bytes} or {files} (png, jpeg); gif adds frames, width, height, secondsPerSlide", has_doc, export),
cmd!(noundo "file.export", "Export…", ["File"], None, "{path: file to write (frames: a folder that receives frame-000001.png…; its earlier frame-NNNNNN.png files are replaced once the new ones are all written, and kept when writing the new ones fails; if removing the earlier ones then fails, the error says so and the new frames stay), format?: png|jpeg|pptx|deckcraft|outline|rtf|pdf|gif|frames (else by path; frames needs it), slide?: index, all?: bool, scale?: px per pt, layout?: slides|notes|handouts (pdf), perPage?: 1|2|3|4|6|9, paper?: letter|a4|legal|a3 (pdf), orientation?: portrait|landscape (pdf), dpi?, slides?: [index]|range (\"1-3,5\"), includeHidden?, textLayer?, frame?, transparentBackground?: bool (png: leave the slide background out), width?: px 16-3840|small|medium|large|extraLarge (gif: 960; frames: a number, 1920, and both frame sides are rounded to even numbers for video encoders), secondsPerSlide?: 0.1-600 (gif; 5) or 0-60 (frames; 3: hold after a slide's last build step, the whole time of a slide without animations), loop?: bool (gif; true: forever), fps?: 1-60 (frames; 30), secondsPerStep?: 0-30 (frames; 1: hold before the first click step and after each click step settles; automatic steps run on), transitions?: bool (frames; true: play each slide's transition from the previous slide, frame by frame; false cuts), useTimings?: bool (frames; default: the deck's Use Timings: a slide's Advance Slide After time is its wait before each click step and before it advances, as in the show)} → {path, bytes} or {files} (png, jpeg); gif adds frames, width, height, secondsPerSlide; frames (the slide show as a PNG frame sequence for a video encoder: every build step plays at fps, then holds; at most 18000 frames) answers {folder, frames, fps, width, height, seconds, slides: [{index, from, to}]: the first and last frame number (from 1) of each slide, its transition is included}", has_doc, export),
cmd!(
query "file.info",
"Info",
Expand Down Expand Up @@ -655,6 +655,30 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
"secondsPerSlide": f64::from(out.delay_cs) / 100.0,
}))
}
"frames" => {
// `path` is a folder; `slides` may be a range, as for PDF ("1-3,5").
let mut fp = p.clone();
if let Some(text) = p.get("slides").and_then(Value::as_str) {
fp["slides"] = json!(parse_slide_range(text, st.doc.slides.len())?);
} else if p.get("slides").is_none_or(Value::is_null)
&& let Some(i) = opt_usize_param(p, "slide", "file.export")?
{
// `slide` narrows the export to that one slide, as it does for the images; one that
// is not an index is refused, not read as the whole deck.
fp["slides"] = json!([i]);
Comment thread
zberry marked this conversation as resolved.
Resolved
}
let opts = crate::frames_export::FramesOptions::from_params(&fp)?;
let out = crate::frames_export::export(&st.doc, &opts, &path)?;
Ok(json!({
"folder": out.folder,
"frames": out.frames,
"fps": out.fps,
"width": out.width,
"height": out.height,
"seconds": out.seconds(),
"slides": out.slides.iter().map(|r| json!({"index": r.index, "from": r.from, "to": r.to})).collect::<Vec<_>>(),
}))
}
"pptx" | "potx" | "ppsx" | "deckcraft" | "outline" | "txt" | "rtf" => {
let bytes = save_bytes(&st.doc, &format)?;
write_file(&path, &bytes)?;
Expand Down
Loading
Loading