diff --git a/Cargo.lock b/Cargo.lock index 52f6578..62838d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -35,6 +35,7 @@ dependencies = [ "fontdue", "gifski", "imgref", + "jpeg-encoder", "log", "reqwest", "resvg", @@ -216,9 +217,9 @@ checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytemuck" -version = "1.13.1" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -904,9 +905,9 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "imgref" -version = "1.9.4" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2cf49df1085dcfb171460e4592597b84abe50d900fb83efb6e41b20fefd6c2c" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" @@ -953,6 +954,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "jpeg-encoder" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fefe5a4fb12fa836172dc53cc36c37af693f6197ae702f931faad8774caf926" + [[package]] name = "js-sys" version = "0.3.61" @@ -1011,9 +1018,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "loop9" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a703804431e5927454bcaf2b2a162595e95db931130c2728c18d050090f69940" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" dependencies = [ "imgref", ] @@ -1103,9 +1110,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -1331,9 +1338,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.7.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -1341,14 +1348,12 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] @@ -1462,9 +1467,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.36" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" dependencies = [ "bytemuck", ] diff --git a/Cargo.toml b/Cargo.toml index 4c09d2b..ee38657 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ fontdb = "0.22.0" fontdue = "0.7" gifski = "1" imgref = "1" +jpeg-encoder = "0.6.0" log = "0.4" reqwest = { version = "0.12.8", default-features = false, features = ["blocking", "rustls-tls-native-roots", "gzip"] } resvg = { version = "0.44.0", features = ["text"] } # TODO remove default features diff --git a/src/lib.rs b/src/lib.rs index 0a7ebc4..cebf506 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,10 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use clap::ArgEnum; +use gifski::progress::{ProgressReporter, ProgressBar, NoProgress}; use log::info; use std::fmt::{Debug, Display}; -use std::io::{BufRead, Write}; +use std::io::BufRead; +use std::fs::File; use std::{iter, thread, time::Instant}; mod asciicast; mod events; @@ -36,6 +38,8 @@ pub struct Config { pub speed: f64, pub theme: Option, pub show_progress_bar: bool, + pub output_frames: bool, + pub output_filename: String, } impl Default for Config { @@ -55,6 +59,8 @@ impl Default for Config { speed: DEFAULT_SPEED, theme: Default::default(), show_progress_bar: true, + output_frames: false, + output_filename: "".into(), } } } @@ -117,7 +123,7 @@ impl Display for Theme { } } -pub fn run(input: I, output: O, config: Config) -> Result<()> { +pub fn run(input: I, config: Config) -> Result<()> { let (header, events) = asciicast::open(input)?; let terminal_size = ( @@ -188,30 +194,73 @@ pub fn run(input: I, output: O, config: Config) -> let (collector, writer) = gifski::new(settings)?; let start_time = Instant::now(); - thread::scope(|s| { - let writer_handle = s.spawn(move || { - if config.show_progress_bar { - let mut pr = gifski::progress::ProgressBar::new(count); - let result = writer.write(output, &mut pr); - pr.finish(); - println!(); - result - } else { - let mut pr = gifski::progress::NoProgress {}; - writer.write(output, &mut pr) - } - }); + if config.output_frames { + // check for existance of the output directory to provide a clear error message + if std::fs::exists(&config.output_filename)? { + return Err(anyhow!("Frame output directory {:?} already exists", config.output_filename)); + } + + // create the directory in advance + std::fs::create_dir(&config.output_filename)?; + + let mut pr: Box = if config.show_progress_bar { + Box::new(ProgressBar::new(count)) + } else { + Box::new(NoProgress {}) + }; - for (i, (time, lines, cursor)) in frames.enumerate() { + for (i, (_, lines, cursor)) in frames.enumerate() { let image = renderer.render(lines, cursor); - let time = if i == 0 { 0.0 } else { time }; - collector.add_frame_rgba(i, image, time + config.last_frame_duration)?; + let buf = image.pixels().flat_map(|x| [x.r, x.g, x.g, x.b]).collect::>(); + + let frame_file_path = std::path::PathBuf::new() + .join(&config.output_filename) + .join(format!("{}.jpeg", i)); + + // TODO argument for jpeg quality? + let encoder = jpeg_encoder::Encoder::new_file(&frame_file_path, 90) + .with_context(|| anyhow!("Error creating frame file at {:?}", frame_file_path))?; + + // TODO remove unwraps + encoder.encode(&buf, + width.try_into().unwrap(), + height.try_into().unwrap(), + jpeg_encoder::ColorType::Rgba + ) + .with_context(|| anyhow!("Error while encoding frame {}", i))?; + + if !pr.increase() { + break; + } } + } else { + let mut output = File::create(&config.output_filename)?; + + thread::scope(|s| { + let writer_handle = s.spawn(move || { + if config.show_progress_bar { + let mut pr = ProgressBar::new(count); + let result = writer.write(&mut output, &mut pr); + pr.finish(); + println!(); + result + } else { + let mut pr = NoProgress {}; + writer.write(output, &mut pr) + } + }); + + for (i, (time, lines, cursor)) in frames.enumerate() { + let image = renderer.render(lines, cursor); + let time = if i == 0 { 0.0 } else { time }; + collector.add_frame_rgba(i, image, time + config.last_frame_duration)?; + } - drop(collector); - writer_handle.join().unwrap()?; - Result::<()>::Ok(()) - })?; + drop(collector); + writer_handle.join().unwrap()?; + Result::<()>::Ok(()) + })?; + } info!( "rendering finished in {}s", diff --git a/src/main.rs b/src/main.rs index 1c6d503..8ec2e84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,9 +50,13 @@ struct Cli { /// asciicast path/filename or URL input_filename_or_url: String, - /// GIF path/filename + /// GIF path/filename (output directory if outputting frames) output_filename: String, + /// Output each frame as separate JPEG + #[clap(long)] + output_frames: bool, + /// Select frame rendering backend #[clap(long, arg_enum, default_value_t = agg::Renderer::default())] renderer: agg::Renderer, @@ -178,9 +182,10 @@ fn main() -> Result<()> { speed: cli.speed, theme: cli.theme.map(|theme| theme.0), show_progress_bar: true, + output_frames: cli.output_frames, + output_filename: cli.output_filename, }; let input = BufReader::new(reader(&cli.input_filename_or_url)?); - let mut output = File::create(&cli.output_filename)?; - agg::run(input, &mut output, config) + agg::run(input, config) }