From e9fec41356602dbc68dabd69e9d83224f1a0bc5b Mon Sep 17 00:00:00 2001 From: Kornel Date: Sun, 14 Apr 2024 01:35:33 +0100 Subject: [PATCH] Support - as stdin path --- src/bin/ffmpeg_source.rs | 14 ++-- src/bin/gif_source.rs | 17 +++-- src/bin/gifski.rs | 156 +++++++++++++++++++++++++++------------ src/bin/source.rs | 2 +- 4 files changed, 128 insertions(+), 61 deletions(-) diff --git a/src/bin/ffmpeg_source.rs b/src/bin/ffmpeg_source.rs index 8bf3c97..80a6990 100644 --- a/src/bin/ffmpeg_source.rs +++ b/src/bin/ffmpeg_source.rs @@ -1,10 +1,10 @@ -use crate::source::*; +use crate::source::{Fps, Source}; +use crate::SrcPath; use crate::BinResult; use gifski::Collector; use gifski::Settings; use imgref::*; use rgb::*; -use std::path::Path; pub struct FfmpegDecoder { input_context: ffmpeg::format::context::Input, @@ -23,10 +23,14 @@ impl Source for FfmpegDecoder { } impl FfmpegDecoder { - pub fn new(path: &Path, rate: Fps, settings: Settings) -> BinResult { + pub fn new(src: SrcPath, rate: Fps, settings: Settings) -> BinResult { ffmpeg::init().map_err(|e| format!("Unable to initialize ffmpeg: {}", e))?; - let input_context = ffmpeg::format::input(&path) - .map_err(|e| format!("Unable to open video file {}: {}", path.display(), e))?; + let input_context = match src { + SrcPath::Path(path) => ffmpeg::format::input(&path) + .map_err(|e| format!("Unable to open video file {}: {}", path.display(), e))?, + SrcPath::Stdin(_) => return Err("Video files must be specified as a path on disk. Input via stdin is not supported".into()), + }; + // take fps override into account let filter_fps = rate.fps / rate.speed; let stream = input_context.streams().best(ffmpeg::media::Type::Video).ok_or("The file has no video tracks")?; diff --git a/src/bin/gif_source.rs b/src/bin/gif_source.rs index 71f597d..940b125 100644 --- a/src/bin/gif_source.rs +++ b/src/bin/gif_source.rs @@ -1,26 +1,29 @@ //! This is for reading GIFs as an input for re-encoding as another GIF -use std::fs::File; +use std::io::Read; +use crate::source::{Fps, Source}; +use crate::{BinResult, SrcPath}; use gif::Decoder; use gifski::Collector; -use std::path::Path; -use crate::{source::{Fps, Source}, BinResult}; pub struct GifDecoder { speed: f32, - decoder: Decoder, + decoder: Decoder>, screen: gif_dispose::Screen, } impl GifDecoder { - pub fn new(path: &Path, fps: Fps) -> BinResult { - let file = std::fs::File::open(path)?; + pub fn new(src: SrcPath, fps: Fps) -> BinResult { + let input = match src { + SrcPath::Path(path) => Box::new(std::fs::File::open(path)?) as Box, + SrcPath::Stdin(buf) => Box::new(buf), + }; let mut gif_opts = gif::DecodeOptions::new(); // Important: gif_opts.set_color_output(gif::ColorOutput::Indexed); - let decoder = gif_opts.read_info(file)?; + let decoder = gif_opts.read_info(input)?; let screen = gif_dispose::Screen::new_decoder(&decoder); Ok(Self { diff --git a/src/bin/gifski.rs b/src/bin/gifski.rs index 5f32725..f82c0d8 100644 --- a/src/bin/gifski.rs +++ b/src/bin/gifski.rs @@ -9,7 +9,11 @@ #![allow(clippy::wildcard_imports)] use clap::builder::NonEmptyStringValueParser; +use std::io::stdin; +use std::io::BufRead; +use std::io::BufReader; use std::io::Read; +use std::io::StdinLock; use std::io::Stdout; use gifski::{Settings, Repeat}; use clap::value_parser; @@ -226,46 +230,7 @@ fn bin_main() -> BinResult<()> { check_if_paths_exist(&frames)?; - let mut decoder = if let [path] = &frames[..] { - match file_type(path).unwrap_or(FileType::Other) { - FileType::PNG | FileType::JPEG => return Err("Only a single image file was given as an input. This is not enough to make an animation.".into()), - FileType::GIF => { - if !quiet && (width.is_none() && settings.quality > 50) { - eprintln!("warning: reading an existing GIF as an input. This can only worsen the quality. Use PNG frames instead."); - } - Box::new(gif_source::GifDecoder::new(path, rate)?) - }, - _ if path.is_dir() => { - return Err(format!("{} is a directory, not a PNG file", path.display()).into()); - }, - FileType::Other => get_video_decoder(path, rate, settings)?, - } - } else { - if speed != 1.0 { - eprintln!("warning: --fast-forward option is for videos. It doesn't make sense for images. Use --fps only."); - } - match file_type(&frames[0]).unwrap_or(FileType::Other) { - FileType::JPEG => { - return Err("JPEG format is unsuitable for conversion to GIF.\n\n\ - JPEG's compression artifacts and color space are very problematic for palette-based\n\ - compression. Please don't use JPEG for making GIF animations. Please re-export\n\ - your animation using the PNG format.".into()) - }, - FileType::GIF => { - return Err("Too many arguments. Unexpectedly got a GIF as an input frame. Only PNG format is supported for individual frames.".into()); - }, - _ => Box::new(png::Lodecoder::new(frames, rate)), - } - }; - - let mut pb; - let mut nopb = NoProgress {}; - let progress: &mut dyn ProgressReporter = if quiet { - &mut nopb - } else { - pb = ProgressBar::new(decoder.total_frames()); - &mut pb - }; + std::thread::scope(move |scope| { let (mut collector, mut writer) = gifski::new(settings)?; if let Some(fixed_colors) = fixed_colors { @@ -290,7 +255,49 @@ fn bin_main() -> BinResult<()> { writer.set_lossy_quality(lossy_quality); } - let decode_thread = thread::Builder::new().name("decode".into()).spawn(move || { + let (decoder_ready_send, decoder_ready_recv) = crossbeam_channel::bounded(1); + + let decode_thread = thread::Builder::new().name("decode".into()).spawn_scoped(scope, move || { + let mut decoder = if let [path] = &frames[..] { + let mut src = if path.as_os_str() == "-" { + SrcPath::Stdin(BufReader::new(stdin().lock())) + } else { + SrcPath::Path(path.to_path_buf()) + }; + match file_type(&mut src).unwrap_or(FileType::Other) { + FileType::PNG | FileType::JPEG => return Err("Only a single image file was given as an input. This is not enough to make an animation.".into()), + FileType::GIF => { + if !quiet && (width.is_none() && settings.quality > 50) { + eprintln!("warning: reading an existing GIF as an input. This can only worsen the quality. Use PNG frames instead."); + } + Box::new(gif_source::GifDecoder::new(src, rate)?) + }, + _ if path.is_dir() => { + return Err(format!("{} is a directory, not a PNG file", path.display()).into()); + }, + FileType::Other => get_video_decoder(src, rate, settings)?, + } + } else { + if speed != 1.0 { + eprintln!("warning: --fast-forward option is for videos. It doesn't make sense for images. Use --fps only."); + } + let file_type = file_type(&mut SrcPath::Path(frames[0].clone())).unwrap_or(FileType::Other); + match file_type { + FileType::JPEG => { + return Err("JPEG format is unsuitable for conversion to GIF.\n\n\ + JPEG's compression artifacts and color space are very problematic for palette-based\n\ + compression. Please don't use JPEG for making GIF animations. Please re-export\n\ + your animation using the PNG format.".into()) + }, + FileType::GIF => { + return Err("Too many arguments. Unexpectedly got a GIF as an input frame. Only PNG format is supported for individual frames.".into()); + }, + _ => Box::new(png::Lodecoder::new(frames, rate)), + } + }; + + decoder_ready_send.send(decoder.total_frames())?; + decoder.collect(&mut collector) })?; @@ -307,11 +314,47 @@ fn bin_main() -> BinResult<()> { &mut stdio_tmp }, }; - writer.write(io::BufWriter::new(out), progress)?; - decode_thread.join().map_err(|_| "thread died?")??; + + let total_frames = match decoder_ready_recv.recv() { + Ok(t) => t, + Err(_) => { + // if the decoder failed to start, + // writer won't have any interesting error to report + return decode_thread.join().map_err(panic_err)?; + } + }; + + let mut pb; + let mut nopb = NoProgress {}; + let progress: &mut dyn ProgressReporter = if quiet { + &mut nopb + } else { + pb = ProgressBar::new(total_frames); + &mut pb + }; + + let write_result = writer.write(io::BufWriter::new(out), progress); + let thread_result = decode_thread.join().map_err(panic_err)?; + check_errors(write_result, thread_result)?; progress.done(&format!("gifski created {output_path}")); Ok(()) + }) +} + +fn check_errors(err1: Result<(), gifski::Error>, err2: BinResult<()>) -> BinResult<()> { + use gifski::Error::*; + match err1 { + Ok(()) => err2, + Err(ThreadSend | Aborted | NoFrames) if err2.is_err() => err2, + Err(err1) => Err(err1.into()), + } +} + +#[cold] +fn panic_err(err: Box) -> String { + err.downcast::().map(|s| *s) + .unwrap_or_else(|e| e.downcast_ref::<&str>().copied().unwrap_or("panic").to_owned()) } fn parse_color(c: &str) -> Result { @@ -348,10 +391,18 @@ enum FileType { PNG, GIF, JPEG, Other, } -fn file_type(path: &Path) -> BinResult { - let mut file = std::fs::File::open(path)?; +fn file_type(src: &mut SrcPath) -> BinResult { let mut buf = [0; 4]; - file.read_exact(&mut buf)?; + match src { + SrcPath::Path(path) => { + let mut file = std::fs::File::open(path)?; + file.read_exact(&mut buf)?; + }, + SrcPath::Stdin(stdin) => { + buf.copy_from_slice(&stdin.fill_buf()?[..4]); + // don't consume + }, + } if &buf == b"\x89PNG" { return Ok(FileType::PNG); @@ -367,6 +418,10 @@ fn file_type(path: &Path) -> BinResult { fn check_if_paths_exist(paths: &[PathBuf]) -> BinResult<()> { for path in paths { + // stdin is ok + if path.as_os_str() == "-" && paths.len() == 1 { + break; + } if !path.exists() { let mut msg = format!("Unable to find the input file: \"{}\"", path.display()); if path.to_str().map_or(false, |p| p.contains(['*','?','['])) { @@ -389,6 +444,11 @@ enum DestPath<'a> { Stdout, } +enum SrcPath { + Path(PathBuf), + Stdin(BufReader>), +} + impl<'a> DestPath<'a> { pub fn new(path: &'a Path) -> Self { if path.as_os_str() == "-" { @@ -412,13 +472,13 @@ impl fmt::Display for DestPath<'_> { } #[cfg(feature = "video")] -fn get_video_decoder(path: &Path, fps: source::Fps, settings: Settings) -> BinResult> { +fn get_video_decoder(path: SrcPath, fps: source::Fps, settings: Settings) -> BinResult> { Ok(Box::new(ffmpeg_source::FfmpegDecoder::new(path, fps, settings)?)) } #[cfg(not(feature = "video"))] #[cold] -fn get_video_decoder(_: &Path, _: source::Fps, _: Settings) -> BinResult> { +fn get_video_decoder(_: SrcPath<'_>, _: source::Fps, _: Settings) -> BinResult> { Err(r"Video support is permanently disabled in this executable. To enable video decoding you need to recompile gifski from source with: diff --git a/src/bin/source.rs b/src/bin/source.rs index 6548871..5071059 100644 --- a/src/bin/source.rs +++ b/src/bin/source.rs @@ -1,7 +1,7 @@ use crate::BinResult; use gifski::Collector; -pub trait Source: Send { +pub trait Source { fn total_frames(&self) -> Option; fn collect(&mut self, dest: &mut Collector) -> BinResult<()>; }