Skip to content

Commit

Permalink
Merge pull request #237 from LGFae/animate-stdin
Browse files Browse the repository at this point in the history
support animations when piping from stdin
  • Loading branch information
LGFae authored Mar 22, 2024
2 parents 777c55e + bdf9cb0 commit 8ebb5a3
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 125 deletions.
166 changes: 63 additions & 103 deletions src/imgproc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ use image::{
AnimationDecoder, DynamicImage, Frames, ImageFormat, RgbImage,
};
use std::{
fs::File,
io::{stdin, BufRead, BufReader, Cursor, Read, Seek, Stdin},
io::{stdin, Cursor, Read},
num::NonZeroU32,
path::Path,
time::Duration,
Expand All @@ -20,72 +19,54 @@ use crate::cli::ResizeStrategy;

use super::cli;

enum ImgBufInner {
Stdin(BufReader<Stdin>),
File(image::io::Reader<BufReader<File>>),
}

impl ImgBufInner {
/// Guess the format of the ImgBufInner
#[inline]
fn format(&self) -> Option<ImageFormat> {
match &self {
ImgBufInner::Stdin(_) => None, // not seekable
ImgBufInner::File(reader) => reader.format(),
}
}
}

#[derive(Clone)]
pub struct ImgBuf {
inner: ImgBufInner,
bytes: Box<[u8]>,
format: ImageFormat,
is_animated: bool,
}

impl ImgBuf {
/// Create a new ImgBuf from a given path. Use - for Stdin
pub fn new(path: &Path) -> Result<Self, String> {
Ok(if let Some("-") = path.to_str() {
let reader = BufReader::new(stdin());
Self {
inner: ImgBufInner::Stdin(reader),
is_animated: false,
}
let bytes = if let Some("-") = path.to_str() {
let mut bytes = Vec::new();
stdin()
.read_to_end(&mut bytes)
.map_err(|e| format!("failed to read standard input: {e}"))?;
bytes.into_boxed_slice()
} else {
let reader = image::io::Reader::open(path)
.map_err(|e| format!("failed to open image: {e}"))?
.with_guessed_format()
.map_err(|e| format!("failed to detect the image's format: {e}"))?;

let is_animated = {
match reader.format() {
Some(ImageFormat::Gif) => true,
Some(ImageFormat::WebP) => {
// Note: unwrapping is safe because we already opened the file once before this
WebPDecoder::new(BufReader::new(File::open(path).unwrap()))
.map_err(|e| format!("failed to decode Webp Image: {e}"))?
.has_animation()
}
Some(ImageFormat::Png) => {
PngDecoder::new(BufReader::new(File::open(path).unwrap()))
.map_err(|e| format!("failed to decode Png Image: {e}"))?
.is_apng()
.map_err(|e| format!("failed to detect if Png is animated: {e}"))?
}

_ => false,
}
};

Self {
inner: ImgBufInner::File(reader),
is_animated,
std::fs::read(path)
.map_err(|e| format!("failed to read file: {e}"))?
.into_boxed_slice()
};

let reader = image::io::Reader::new(Cursor::new(&bytes))
.with_guessed_format()
.map_err(|e| format!("failed to detect the image's format: {e}"))?;

let format = reader.format();
let is_animated = match format {
Some(ImageFormat::Gif) => true,
Some(ImageFormat::WebP) => {
// Note: unwrapping is safe because we already opened the file once before this
WebPDecoder::new(Cursor::new(&bytes))
.map_err(|e| format!("failed to decode Webp Image: {e}"))?
.has_animation()
}
})
}
Some(ImageFormat::Png) => PngDecoder::new(Cursor::new(&bytes))
.map_err(|e| format!("failed to decode Png Image: {e}"))?
.is_apng()
.map_err(|e| format!("failed to detect if Png is animated: {e}"))?,
None => return Err("Unknown image format".to_string()),
_ => false,
};

/// Guess the format of the ImgBuf
fn format(&self) -> Option<ImageFormat> {
self.inner.format()
Ok(Self {
format: format.unwrap(), // this is ok because we return err earlier if it is None
bytes,
is_animated,
})
}

#[inline]
Expand All @@ -94,54 +75,33 @@ impl ImgBuf {
}

/// Decode the ImgBuf into am RgbImage
pub fn decode(self) -> Result<RgbImage, String> {
Ok(match self.inner {
ImgBufInner::Stdin(mut reader) => {
let mut buffer = Vec::new();
reader
.read_to_end(&mut buffer)
.map_err(|e| format!("failed to read stdin: {e}"))?;

image::load_from_memory(&buffer)
}
ImgBufInner::File(reader) => reader.decode(),
}
.map_err(|e| format!("failed to decode image: {e}"))?
.into_rgb8())
pub fn decode(&self) -> Result<RgbImage, String> {
let mut reader = image::io::Reader::new(Cursor::new(&self.bytes));
reader.set_format(self.format);
Ok(reader
.decode()
.map_err(|e| format!("failed to decode image: {e}"))?
.into_rgb8())
}

/// Convert this ImgBuf into Frames
pub fn into_frames<'a>(self) -> Result<Frames<'a>, String> {
fn create_decoder<'a>(
img_format: Option<ImageFormat>,
reader: impl BufRead + Seek + 'a,
) -> Result<Frames<'a>, String> {
match img_format {
Some(ImageFormat::Gif) => Ok(GifDecoder::new(reader)
.map_err(|e| format!("failed to decode gif during animation: {e}"))?
.into_frames()),
Some(ImageFormat::WebP) => Ok(WebPDecoder::new(reader)
.map_err(|e| format!("failed to decode webp during animation: {e}"))?
.into_frames()),
Some(ImageFormat::Png) => Ok(PngDecoder::new(reader)
.map_err(|e| format!("failed to decode png during animation: {e}"))?
.apng()
.unwrap() // we detected this earlier
.into_frames()),
_ => Err(format!("requested format has no decoder: {img_format:#?}")),
}
}

let img_format = self.format();
match self.inner {
ImgBufInner::Stdin(mut reader) => {
let mut bytes = Vec::new();
reader
.read_to_end(&mut bytes)
.map_err(|e| format!("failed to read stdin: {e}"))?;
create_decoder(img_format, Cursor::new(bytes))
}
ImgBufInner::File(reader) => create_decoder(img_format, reader.into_inner()),
pub fn as_frames(&self) -> Result<Frames, String> {
match self.format {
ImageFormat::Gif => Ok(GifDecoder::new(Cursor::new(&self.bytes))
.map_err(|e| format!("failed to decode gif during animation: {e}"))?
.into_frames()),
ImageFormat::WebP => Ok(WebPDecoder::new(Cursor::new(&self.bytes))
.map_err(|e| format!("failed to decode webp during animation: {e}"))?
.into_frames()),
ImageFormat::Png => Ok(PngDecoder::new(Cursor::new(&self.bytes))
.map_err(|e| format!("failed to decode png during animation: {e}"))?
.apng()
.unwrap() // we detected this earlier
.into_frames()),
_ => Err(format!(
"requested format has no decoder: {:#?}",
self.format
)),
}
}
}
Expand Down
38 changes: 18 additions & 20 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,13 @@ fn make_request(args: &Swww) -> Result<Option<Request>, String> {
let (format, dims, outputs) = get_format_dims_and_outputs(&requested_outputs)?;
let imgbuf = ImgBuf::new(&img.path)?;
if imgbuf.is_animated() {
let animations = std::thread::scope::<_, Result<_, String>>(|s1| {
let animations =
s1.spawn(|| make_animation_request(img, &dims, format, &outputs));
let animations = {
let first_frame = imgbuf
.into_frames()?
.next()
.ok_or("missing first frame".to_owned())?
.decode()
.map_err(|e| format!("unable to decode first frame: {e}"))?;

let img_request =
make_img_request(img, frame_to_rgb(first_frame), format, &dims, &outputs)?;
let animations = animations.join().unwrap_or_else(|e| Err(format!("{e:?}")));
let img_request = make_img_request(img, first_frame, format, &dims, &outputs)?;
let animations = make_animation_request(img, &imgbuf, &dims, format, &outputs);

let socket = connect_to_socket(5, 100)?;
Request::Img(img_request).send(&socket)?;
Expand All @@ -153,7 +148,7 @@ fn make_request(args: &Swww) -> Result<Option<Request>, String> {
return Err(format!("daemon error when sending image: {e}"));
}
animations
})
}
.map_err(|e| format!("failed to create animated request: {e}"))?;

Ok(Some(Request::Animation(animations)))
Expand Down Expand Up @@ -276,31 +271,34 @@ fn get_format_dims_and_outputs(

fn make_animation_request(
img: &cli::Img,
imgbuf: &ImgBuf,
dims: &[(u32, u32)],
pixel_format: ArchivedPixelFormat,
outputs: &[Vec<String>],
) -> Result<AnimationRequest, String> {
let filter = make_filter(&img.filter);
let mut animations = Vec::with_capacity(dims.len());
for (dim, outputs) in dims.iter().zip(outputs) {
//TODO: make cache work for all resize strategies
if img.resize == ResizeStrategy::Crop {
match cache::load_animation_frames(&img.path, *dim, pixel_format.de()) {
Ok(Some(animation)) => {
animations.push((animation, outputs.to_owned().into_boxed_slice()));
continue;
// do not load cache if we are reading from stdin
if let Some("-") = img.path.to_str() {
//TODO: make cache work for all resize strategies
if img.resize == ResizeStrategy::Crop {
match cache::load_animation_frames(&img.path, *dim, pixel_format.de()) {
Ok(Some(animation)) => {
animations.push((animation, outputs.to_owned().into_boxed_slice()));
continue;
}
Ok(None) => (),
Err(e) => eprintln!("Error loading cache for {:?}: {e}", img.path),
}
Ok(None) => (),
Err(e) => eprintln!("Error loading cache for {:?}: {e}", img.path),
}
}

let imgbuf = ImgBuf::new(&img.path)?;
let animation = ipc::Animation {
path: img.path.to_string_lossy().to_string(),
dimensions: *dim,
animation: compress_frames(
imgbuf.into_frames()?,
imgbuf.as_frames()?,
*dim,
pixel_format,
filter,
Expand Down
7 changes: 5 additions & 2 deletions utils/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,11 @@ impl Request {
if let Self::Animation(animations) = self {
s.spawn(|| {
for (animation, _) in animations.iter() {
if let Err(e) = cache::store_animation_frames(animation) {
eprintln!("Error storing cache for {}: {e}", animation.path);
// only store the cache if we aren't reading from stdin
if animation.path != "-" {
if let Err(e) = cache::store_animation_frames(animation) {
eprintln!("Error storing cache for {}: {e}", animation.path);
}
}
}
});
Expand Down

0 comments on commit 8ebb5a3

Please sign in to comment.