Skip to content

Commit d55294b

Browse files
committed
Add option to output frames as PNGs
1 parent 489de4d commit d55294b

5 files changed

Lines changed: 114 additions & 45 deletions

File tree

src/lib.rs

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ mod theme;
66
mod vt;
77

88
use std::fmt::{Debug, Display};
9+
use std::fs::File;
910
use std::io::{BufRead, Write};
1011
use std::{iter, thread, time::Instant};
1112

12-
use anyhow::{anyhow, Result};
13+
use anyhow::{anyhow, Context, Result};
1314
use clap::ArgEnum;
15+
use gifski::progress::{NoProgress, ProgressBar, ProgressReporter};
1416
use log::info;
1517

1618
use crate::asciicast::Asciicast;
@@ -40,6 +42,8 @@ pub struct Config {
4042
pub speed: f64,
4143
pub theme: Option<Theme>,
4244
pub show_progress_bar: bool,
45+
pub output_frames: bool,
46+
pub output_filename: String,
4347
}
4448

4549
impl Default for Config {
@@ -59,6 +63,8 @@ impl Default for Config {
5963
speed: DEFAULT_SPEED,
6064
theme: Default::default(),
6165
show_progress_bar: true,
66+
output_frames: false,
67+
output_filename: "".to_string(),
6268
}
6369
}
6470
}
@@ -129,7 +135,7 @@ impl Display for Theme {
129135
}
130136
}
131137

132-
pub fn run<I: BufRead, O: Write + Send>(input: I, output: O, config: Config) -> Result<()> {
138+
pub fn run<I: BufRead>(input: I, config: Config) -> Result<()> {
133139
let Asciicast { header, events, .. } = asciicast::open(input)?;
134140

135141
if header.term_cols == 0 || header.term_rows == 0 {
@@ -188,50 +194,84 @@ pub fn run<I: BufRead, O: Write + Send>(input: I, output: O, config: Config) ->
188194

189195
let (width, height) = renderer.pixel_size();
190196

191-
info!("gif dimensions: {}x{}", width, height);
197+
info!("output dimensions: {}x{}", width, height);
192198

193-
let repeat = if config.no_loop {
194-
gifski::Repeat::Finite(0)
195-
} else {
196-
gifski::Repeat::Infinite
197-
};
199+
let start_time = Instant::now();
198200

199-
let settings = gifski::Settings {
200-
width: Some(width as u32),
201-
height: Some(height as u32),
202-
fast: true,
203-
repeat,
204-
..Default::default()
205-
};
201+
if config.output_frames {
202+
// check for existance of the output directory to provide a clear error message
203+
if std::fs::exists(&config.output_filename)? {
204+
return Err(anyhow!("Frame output path {:?} already exists", config.output_filename));
205+
}
206206

207-
let (collector, writer) = gifski::new(settings)?;
208-
let start_time = Instant::now();
207+
// create the directory in advance
208+
std::fs::create_dir(&config.output_filename)?;
209209

210-
thread::scope(|s| {
211-
let writer_handle = s.spawn(move || {
212-
if config.show_progress_bar {
213-
let mut pr = gifski::progress::ProgressBar::new(count);
214-
let result = writer.write(output, &mut pr);
215-
pr.finish();
216-
println!();
217-
result
218-
} else {
219-
let mut pr = gifski::progress::NoProgress {};
220-
writer.write(output, &mut pr)
221-
}
222-
});
210+
let mut pr: Box<dyn ProgressReporter> = if config.show_progress_bar {
211+
Box::new(ProgressBar::new(count))
212+
} else {
213+
Box::new(NoProgress {})
214+
};
223215

224216
for (i, frame) in frames.enumerate() {
225-
let (time, lines, cursor) = frame?;
226-
let image = renderer.render(lines, cursor);
227-
let time = if i == 0 { 0.0 } else { time };
228-
collector.add_frame_rgba(i, image, time + config.last_frame_duration)?;
217+
let (_, lines, cursor) = frame?;
218+
219+
let frame_file_path = std::path::PathBuf::new()
220+
.join(&config.output_filename)
221+
.join(format!("{}.png", i));
222+
223+
let svg = renderer.render_pixmap(lines, cursor);
224+
svg.save_png(frame_file_path)
225+
.with_context(|| anyhow!("Could not encode frame {i}"))?;
226+
227+
if !pr.increase() {
228+
return Err(anyhow!("Progress bar interrupted"));
229+
}
229230
}
231+
} else {
232+
let repeat = if config.no_loop {
233+
gifski::Repeat::Finite(0)
234+
} else {
235+
gifski::Repeat::Infinite
236+
};
237+
238+
let settings = gifski::Settings {
239+
width: Some(width as u32),
240+
height: Some(height as u32),
241+
fast: true,
242+
repeat,
243+
..Default::default()
244+
};
245+
246+
let (collector, writer) = gifski::new(settings)?;
247+
let output = File::create(&config.output_filename)?;
248+
249+
thread::scope(|s| {
250+
let writer_handle = s.spawn(move || {
251+
if config.show_progress_bar {
252+
let mut pr = gifski::progress::ProgressBar::new(count);
253+
let result = writer.write(output, &mut pr);
254+
pr.finish();
255+
println!();
256+
result
257+
} else {
258+
let mut pr = gifski::progress::NoProgress {};
259+
writer.write(output, &mut pr)
260+
}
261+
});
262+
263+
for (i, frame) in frames.enumerate() {
264+
let (time, lines, cursor) = frame?;
265+
let image = renderer.render(lines, cursor);
266+
let time = if i == 0 { 0.0 } else { time };
267+
collector.add_frame_rgba(i, image, time + config.last_frame_duration)?;
268+
}
230269

231-
drop(collector);
232-
writer_handle.join().unwrap()?;
233-
Result::<()>::Ok(())
234-
})?;
270+
drop(collector);
271+
writer_handle.join().unwrap()?;
272+
Result::<()>::Ok(())
273+
})?;
274+
}
235275

236276
info!(
237277
"rendering finished in {}s",

src/main.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,13 @@ struct Cli {
5050
/// asciicast path/filename or URL
5151
input_filename_or_url: String,
5252

53-
/// GIF path/filename
53+
/// GIF path/filename (output directory if outputting frames)
5454
output_filename: String,
5555

56+
/// Output each frame as PNG image
57+
#[clap(long)]
58+
output_frames: bool,
59+
5660
/// Select frame rendering backend
5761
#[clap(long, arg_enum, default_value_t = agg::Renderer::default())]
5862
renderer: agg::Renderer,
@@ -187,15 +191,19 @@ fn main() -> Result<()> {
187191
speed: cli.speed,
188192
theme: cli.theme.map(|theme| theme.0),
189193
show_progress_bar: !cli.quiet,
194+
output_frames: cli.output_frames,
195+
output_filename: cli.output_filename.clone(),
190196
};
191197

192198
let input = BufReader::new(reader(&cli.input_filename_or_url)?);
193-
let mut output = File::create(&cli.output_filename)?;
194-
195-
match agg::run(input, &mut output, config) {
199+
match agg::run(input, config) {
196200
Ok(ok) => Ok(ok),
197201
Err(err) => {
198-
std::fs::remove_file(cli.output_filename)?;
202+
// do not try to delete a directory
203+
if !cli.output_frames {
204+
std::fs::remove_file(&cli.output_filename)?;
205+
}
206+
199207
Err(err)
200208
}
201209
}

src/renderer.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ mod resvg;
33

44
use imgref::ImgVec;
55
use rgb::{RGB8, RGBA8};
6+
use tiny_skia::Pixmap;
67

78
use crate::theme::Theme;
89

910
pub trait Renderer {
11+
fn render_pixmap(&mut self, lines: Vec<avt::Line>, cursor: Option<(usize, usize)>) -> Pixmap;
1012
fn render(&mut self, lines: Vec<avt::Line>, cursor: Option<(usize, usize)>) -> ImgVec<RGBA8>;
1113
fn pixel_size(&self) -> (usize, usize);
1214
}

src/renderer/fontdue.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::theme::Theme;
33
use imgref::ImgVec;
44
use log::debug;
55
use rgb::RGBA8;
6+
use tiny_skia::{PremultipliedColorU8, Pixmap};
67
use std::collections::HashMap;
78

89
type CharVariant = (char, bool, bool);
@@ -167,6 +168,19 @@ fn mix_colors(fg: RGBA8, bg: RGBA8, ratio: u8) -> RGBA8 {
167168
}
168169

169170
impl Renderer for FontdueRenderer {
171+
fn render_pixmap(&mut self, lines: Vec<avt::Line>, cursor: Option<(usize, usize)>) -> tiny_skia::Pixmap {
172+
let buf = self.render(lines, cursor);
173+
let mut pixmap = Pixmap::new(buf.width() as u32, buf.height() as u32).unwrap();
174+
175+
// TODO could this be done more efficiently as it is currently copying all the data
176+
let pixels = pixmap.pixels_mut();
177+
for (i, color) in buf.pixels().enumerate() {
178+
pixels[i] = PremultipliedColorU8::from_rgba(color.r, color.g, color.b, color.a).unwrap();
179+
}
180+
181+
pixmap
182+
}
183+
170184
fn render(&mut self, lines: Vec<avt::Line>, cursor: Option<(usize, usize)>) -> ImgVec<RGBA8> {
171185
let mut buf: Vec<RGBA8> =
172186
vec![self.theme.background.alpha(255); self.pixel_width * self.pixel_height];

src/renderer/resvg.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ impl<'a> ResvgRenderer<'a> {
241241
}
242242

243243
impl<'a> Renderer for ResvgRenderer<'a> {
244-
fn render(&mut self, lines: Vec<avt::Line>, cursor: Option<(usize, usize)>) -> ImgVec<RGBA8> {
244+
fn render_pixmap(&mut self, lines: Vec<avt::Line>, cursor: Option<(usize, usize)>) -> tiny_skia::Pixmap {
245245
let mut svg = self.header.clone();
246246
self.push_lines(&mut svg, lines, cursor);
247247
svg.push_str(Self::footer());
@@ -251,7 +251,12 @@ impl<'a> Renderer for ResvgRenderer<'a> {
251251
tiny_skia::Pixmap::new(self.pixel_width as u32, self.pixel_height as u32).unwrap();
252252

253253
resvg::render(&tree, self.transform, &mut pixmap.as_mut());
254-
let buf = pixmap.take().as_rgba().to_vec();
254+
255+
pixmap
256+
}
257+
258+
fn render(&mut self, lines: Vec<avt::Line>, cursor: Option<(usize, usize)>) -> ImgVec<RGBA8> {
259+
let buf = self.render_pixmap(lines, cursor).take().as_rgba().to_vec();
255260

256261
ImgVec::new(buf, self.pixel_width, self.pixel_height)
257262
}

0 commit comments

Comments
 (0)