Merge PR #444 from MarekSpodBiedry: CLI: render-transition refuses --t values that would share one file name (#423)

# Conflicts:
#	apps/deckcraft-cli/src/main.rs
This commit is contained in:
Brandon Thomas
2026-10-14 14:33:40 -04:00
+44 -10
View File
@@ -351,6 +351,7 @@ fn render_transition(args: &[String]) -> Result<(), String> {
if ts.is_empty() {
return Err("render-transition needs --t".into());
}
let paths = frame_paths(&out, &ts)?;
let mut s = Session::new();
open(&mut s, Some(&input), false)?;
let st = s.doc().map_err(|e| e.to_string())?;
@@ -368,27 +369,38 @@ fn render_transition(args: &[String]) -> Result<(), String> {
(prev.map(|p| render::render_slide(doc, p, &opts)), render::render_slide(doc, i, &opts), parts)
});
let aspect = sw / doc.slide_size.height.max(1.0);
for t in &ts {
for (t, path) in ts.iter().zip(&paths) {
let img = match &cached {
Some((old, new, parts)) => {
render::composite_transition(old.as_ref(), new, parts.as_ref(), &render::transition_frame(&tr.kind, &tr.option, *t, aspect))
}
None => render::render_transition(doc, i, *t, &opts),
};
let path = if ts.len() == 1 {
out.clone()
} else {
let p = std::path::Path::new(&out);
let stem = p.file_stem().and_then(|x| x.to_str()).unwrap_or("frame");
let name = format!("{stem}-t{t:.3}.png");
p.with_file_name(name).to_string_lossy().into_owned()
};
std::fs::write(&path, img.to_png_at(render::dpi_for_scale(opts.scale))).map_err(|e| format!("{path}: {e}"))?;
std::fs::write(path, img.to_png_at(render::dpi_for_scale(opts.scale))).map_err(|e| format!("{path}: {e}"))?;
outln!("{path} ({}×{}) {} {} t={t}", img.width, img.height, tr.kind, tr.option);
}
Ok(())
}
/// Output path of each frame. A single frame goes to OUT; several get `-t<value>` (three decimals)
/// before the extension. Values that round to the same name would overwrite each other, so that is an error.
fn frame_paths(out: &str, ts: &[f64]) -> Result<Vec<String>, String> {
if ts.len() == 1 {
return Ok(vec![out.to_string()]);
}
let p = std::path::Path::new(out);
let stem = p.file_stem().and_then(|x| x.to_str()).unwrap_or("frame");
let mut paths: Vec<String> = Vec::with_capacity(ts.len());
for t in ts {
let path = p.with_file_name(format!("{stem}-t{t:.3}.png")).to_string_lossy().into_owned();
if paths.contains(&path) {
return Err(format!("--t values round to the same three decimals: more than one frame would be written to {path}"));
}
paths.push(path);
}
Ok(paths)
}
fn convert(args: &[String]) -> Result<(), String> {
if args.first().is_some_and(|a| is_help(a)) {
outln!("{USAGE}");
@@ -591,3 +603,25 @@ mod slide_index_tests {
assert!(missing_slide(1, 1).contains("index 0"));
}
}
#[cfg(test)]
mod frame_path_tests {
use super::frame_paths;
#[test]
fn single_frame_uses_out_as_is() {
assert_eq!(frame_paths("a/b.png", &[0.5]), Ok(vec!["a/b.png".to_string()]));
}
#[test]
fn frames_get_distinct_names() {
let paths = frame_paths("f.png", &[0.0, 0.25, 1.0]).unwrap_or_default();
assert_eq!(paths, ["f-t0.000.png", "f-t0.250.png", "f-t1.000.png"]);
}
#[test]
fn values_rounding_to_one_name_are_rejected() {
assert!(frame_paths("f.png", &[0.1001, 0.1002]).is_err());
assert!(frame_paths("f.png", &[0.5, 0.5]).is_err());
}
}