Skip to content

Commit

Permalink
Y4M support
Browse files Browse the repository at this point in the history
  • Loading branch information
kornelski committed Apr 14, 2024
1 parent fe61271 commit ba59191
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 26 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ required-features = ["binary"]
clap = { version = "4.5.4", features = ["cargo"], optional = true }
imgref = "1.10.1"
gif = { version = "0.13.1", default-features = false, features = ["std", "raii_no_panic"] }
gif-dispose = "5.0.0-beta.2"
gif-dispose = "5.0.0"
imagequant = "4.3.0"
lodepng = { version = "3.10.1", optional = true }
pbr = { version = "1.1.1", optional = true }
Expand All @@ -40,6 +40,8 @@ loop9 = "0.1.5"
num-traits = { version = "0.2.17", features = ["i128", "std"] }
crossbeam-utils = "0.8.19"
ordered-channel = { version = "1.0.0", features = ["crossbeam-channel"] }
y4m = { version = "0.8.0", optional = true }
yuv = { version = "0.1.5", optional = true }

[dependencies.ffmpeg]
package = "ffmpeg-next"
Expand All @@ -54,9 +56,9 @@ lodepng = "3.10.1"
[features]
# `cargo build` will skip the binaries with missing `required-features`
# so all CLI dependencies have to be enabled by default.
default = ["gifsicle", "binary"]
default = ["gifsicle", "video", "binary"]
# You can disable this feture when using gifski as a library.
binary = ["dep:clap", "png", "pbr", "dep:wild", "dep:natord", "dep:dunce"]
binary = ["dep:clap", "dep:yuv", "dep:y4m", "png", "pbr", "dep:wild", "dep:natord", "dep:dunce"]
capi = [] # internal for cargo-c only
png = ["dep:lodepng"]
# Links dynamically to ffmpeg. Needs ffmpeg devel package installed on the system.
Expand Down
82 changes: 61 additions & 21 deletions src/bin/gifski.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#![allow(clippy::redundant_closure_for_method_calls)]
#![allow(clippy::wildcard_imports)]

use clap::error::ErrorKind::MissingRequiredArgument;
use clap::builder::NonEmptyStringValueParser;
use std::io::stdin;
use std::io::BufRead;
Expand All @@ -23,6 +24,7 @@ use clap::value_parser;
mod ffmpeg_source;
mod png;
mod gif_source;
mod y4m_source;
mod source;
use crate::source::Source;

Expand All @@ -43,7 +45,7 @@ use std::time::Duration;
#[cfg(feature = "video")]
const VIDEO_FRAMES_ARG_HELP: &str = "one video file supported by FFmpeg, or multiple PNG image files";
#[cfg(not(feature = "video"))]
const VIDEO_FRAMES_ARG_HELP: &str = "PNG image files for the animation frames";
const VIDEO_FRAMES_ARG_HELP: &str = "PNG image files for the animation frames, or a .y4m file";

fn main() {
if let Err(e) = bin_main() {
Expand Down Expand Up @@ -171,7 +173,13 @@ fn bin_main() -> BinResult<()> {
.num_args(1)
.value_parser(parse_color)
.value_name("RGBHEX"))
.get_matches_from(wild::args_os());
.try_get_matches_from(wild::args_os())
.unwrap_or_else(|e| {
if e.kind() == MissingRequiredArgument && !stdin().is_terminal() {
eprintln!("If you're trying to pipe a file, use \"-\" as the input file name");
}
e.exit()
});

let mut frames: Vec<&str> = matches.get_many::<String>("FILES").ok_or("?")?.map(|s| s.as_str()).collect();
if !matches.get_flag("nosort") && frames.len() > 1 {
Expand Down Expand Up @@ -280,7 +288,7 @@ fn bin_main() -> BinResult<()> {
_ 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)?,
other_type => get_video_decoder(other_type, src, rate, settings)?,
}
} else {
if speed != 1.0 {
Expand All @@ -294,9 +302,8 @@ fn bin_main() -> BinResult<()> {
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());
},
FileType::GIF => return unexpected("GIF"),
FileType::Y4M => return unexpected("Y4M"),
_ => Box::new(png::Lodecoder::new(frames, rate)),
}
};
Expand Down Expand Up @@ -362,6 +369,11 @@ fn check_errors(err1: Result<(), gifski::Error>, err2: BinResult<()>) -> BinResu
}
}

#[cold]
fn unexpected(ftype: &'static str) -> BinResult<()> {
Err(format!("Too many arguments. Unexpectedly got a {ftype} as an input frame. Only PNG format is supported for individual frames.").into())
}

#[cold]
fn panic_err(err: Box<dyn std::any::Any + Send>) -> String {
err.downcast::<String>().map(|s| *s)
Expand Down Expand Up @@ -398,19 +410,26 @@ fn color_parser() {
}

#[allow(clippy::upper_case_acronyms)]
#[derive(PartialEq)]
enum FileType {
PNG, GIF, JPEG, Other,
PNG, GIF, JPEG, Y4M, Other,
}

fn file_type(src: &mut SrcPath) -> BinResult<FileType> {
let mut buf = [0; 4];
match src {
SrcPath::Path(path) => {
let mut file = std::fs::File::open(path)?;
file.read_exact(&mut buf)?;
SrcPath::Path(path) => match path.extension() {
Some(e) if e.eq_ignore_ascii_case("y4m") => return Ok(FileType::Y4M),
Some(e) if e.eq_ignore_ascii_case("png") => return Ok(FileType::PNG),
_ => {
let mut file = std::fs::File::open(path)?;
file.read_exact(&mut buf)?;
}
},
SrcPath::Stdin(stdin) => {
buf.copy_from_slice(&stdin.fill_buf()?[..4]);
let buf_in = stdin.fill_buf()?;
let max_len = buf_in.len().min(4);
buf[..max_len].copy_from_slice(&buf_in[..max_len]);
// don't consume
},
}
Expand All @@ -421,6 +440,9 @@ fn file_type(src: &mut SrcPath) -> BinResult<FileType> {
if &buf == b"GIF8" {
return Ok(FileType::GIF);
}
if &buf == b"YUV4" {
return Ok(FileType::Y4M);
}
if buf[..2] == [0xFF, 0xD8] {
return Ok(FileType::JPEG);
}
Expand Down Expand Up @@ -483,23 +505,41 @@ impl fmt::Display for DestPath<'_> {
}

#[cfg(feature = "video")]
fn get_video_decoder(path: SrcPath, fps: source::Fps, settings: Settings) -> BinResult<Box<dyn Source>> {
Ok(Box::new(ffmpeg_source::FfmpegDecoder::new(path, fps, settings)?))
fn get_video_decoder(ftype: FileType, src: SrcPath, fps: source::Fps, settings: Settings) -> BinResult<Box<dyn Source>> {
Ok(if ftype == FileType::Y4M {
Box::new(y4m_source::Y4MDecoder::new(src, fps)?)
} else {
Box::new(ffmpeg_source::FfmpegDecoder::new(src, fps, settings)?)
})
}

#[cfg(not(feature = "video"))]
#[cold]
fn get_video_decoder(_: SrcPath<'_>, _: source::Fps, _: Settings) -> BinResult<Box<dyn Source>> {
Err(r"Video support is permanently disabled in this executable.
fn get_video_decoder(ftype: FileType, src: SrcPath, fps: source::Fps, _: Settings) -> BinResult<Box<dyn Source>> {
if ftype == FileType::Y4M {
Ok(Box::new(y4m_source::Y4MDecoder::new(src, fps)?))
} else {
let path = match &src {
SrcPath::Path(path) => path,
SrcPath::Stdin(_) => Path::new("video.mp4"),
};
let rel_path = path.file_name().map(Path::new).unwrap_or(path);
Err(format!(r#"Video support is permanently disabled in this distribution of gifski.
To enable video decoding you need to recompile gifski from source with:
cargo build --release --features=video
or
cargo install gifski --features=video
The only 'video' format supported at this time is YUV4MPEG2, which can be piped from ffmpeg:
Alternatively, use ffmpeg command to export PNG frames, and then specify
ffmpeg -i "{src}" -f yuv4mpegpipe | gifski -o "{gif}" -
To enable full video decoding you need to recompile gifski from source.
https://github.com/imageoptim/gifski
Alternatively, use ffmpeg or other tool to export PNG frames, and then specify
the PNG files as input for this executable. Instructions on https://gif.ski
".into())
"#,
src = path.display(),
gif = rel_path.with_extension("gif").display()
).into())
}
}

struct ProgressBar {
Expand Down
Loading

0 comments on commit ba59191

Please sign in to comment.