feat: Detect animation in image files for fetch_preview (#574)
* Implement animated metadata TODOs for database and thumbnailing. Signed-off-by: Assisting <erik@eriklabine.com> * Run linter for code changes Signed-off-by: Assisting <erik@eriklabine.com> --------- Signed-off-by: Assisting <erik@eriklabine.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use image::{DynamicImage, ImageBuffer, ImageReader};
|
||||
use image::{AnimationDecoder, DynamicImage, ImageBuffer, ImageReader};
|
||||
use jxl_oxide::integration::JxlDecoder;
|
||||
use revolt_config::report_internal_error;
|
||||
use std::io::{BufRead, Read, Seek};
|
||||
@@ -35,6 +35,31 @@ impl MediaRepository for MediaImpl {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_animated(&self, f: &NamedTempFile, mime: &str) -> Option<bool> {
|
||||
match mime {
|
||||
// Current behaviour is to assume GIFs are animated, this checks for at least 2 frames
|
||||
"image/gif" => {
|
||||
let file = std::fs::File::open(f.path()).ok()?;
|
||||
let reader = std::io::BufReader::new(file);
|
||||
let decoder = image::codecs::gif::GifDecoder::new(reader).ok()?;
|
||||
Some(decoder.into_frames().take(2).count() > 1)
|
||||
}
|
||||
"image/png" => {
|
||||
let file = std::fs::File::open(f.path()).ok()?;
|
||||
let reader = std::io::BufReader::new(file);
|
||||
let decoder = image::codecs::png::PngDecoder::new(reader).ok()?;
|
||||
decoder.is_apng().ok()
|
||||
}
|
||||
"image/webp" => {
|
||||
let file = std::fs::File::open(f.path()).ok()?;
|
||||
let reader = std::io::BufReader::new(file);
|
||||
let decoder = image::codecs::webp::WebPDecoder::new(reader).ok()?;
|
||||
Some(decoder.has_animation())
|
||||
}
|
||||
_ => Some(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn image_size_vec(&self, v: &[u8], mime: &str) -> Option<(usize, usize)> {
|
||||
match mime {
|
||||
"image/svg+xml" => {
|
||||
@@ -171,9 +196,9 @@ impl MediaRepository for MediaImpl {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::{MediaImpl, MediaRepository};
|
||||
use std::io::{Cursor, Write};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[tokio::test]
|
||||
async fn asset_test_jpeg() {
|
||||
@@ -186,6 +211,15 @@ mod tests {
|
||||
media.create_thumbnail(image, "attachments");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn asset_test_jpeg_is_not_animated() {
|
||||
let media = MediaImpl::from_config().await;
|
||||
let mut f = NamedTempFile::new().unwrap();
|
||||
f.write_all(include_bytes!("../../tests/assets/test.jpeg"))
|
||||
.unwrap();
|
||||
assert_eq!(media.is_animated(&f, "image/jpeg"), Some(false));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn asset_test_jpeg_extra_bytes() {
|
||||
let media = MediaImpl::from_config().await;
|
||||
@@ -212,6 +246,15 @@ mod tests {
|
||||
media.create_thumbnail(image, "emojis");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn asset_test_png_is_not_animated() {
|
||||
let media = MediaImpl::from_config().await;
|
||||
let mut f = NamedTempFile::new().unwrap();
|
||||
f.write_all(include_bytes!("../../tests/assets/test.png"))
|
||||
.unwrap();
|
||||
assert_eq!(media.is_animated(&f, "image/png"), Some(false));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn asset_test_png_extra_bytes() {
|
||||
let media = MediaImpl::from_config().await;
|
||||
@@ -259,6 +302,15 @@ mod tests {
|
||||
media.create_thumbnail(image, "attachments");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn asset_test_animated_png_is_animated() {
|
||||
let media = MediaImpl::from_config().await;
|
||||
let mut f = NamedTempFile::new().unwrap();
|
||||
f.write_all(include_bytes!("../../tests/assets/anim-icos.apng"))
|
||||
.unwrap();
|
||||
assert_eq!(media.is_animated(&f, "image/png"), Some(true));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn asset_test_jxl() {
|
||||
let media = MediaImpl::from_config().await;
|
||||
@@ -292,6 +344,15 @@ mod tests {
|
||||
media.create_thumbnail(image, "attachments");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn asset_test_webp_is_not_animated() {
|
||||
let media = MediaImpl::from_config().await;
|
||||
let mut f = NamedTempFile::new().unwrap();
|
||||
f.write_all(include_bytes!("../../tests/assets/dice.webp"))
|
||||
.unwrap();
|
||||
assert_eq!(media.is_animated(&f, "image/webp"), Some(false));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn asset_test_animated_webp() {
|
||||
let media = MediaImpl::from_config().await;
|
||||
@@ -303,6 +364,15 @@ mod tests {
|
||||
media.create_thumbnail(image, "attachments");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn asset_test_animated_webp_is_animated() {
|
||||
let media = MediaImpl::from_config().await;
|
||||
let mut f = NamedTempFile::new().unwrap();
|
||||
f.write_all(include_bytes!("../../tests/assets/anim-icos.webp"))
|
||||
.unwrap();
|
||||
assert_eq!(media.is_animated(&f, "image/webp"), Some(true));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn asset_test_animated_gif() {
|
||||
let media = MediaImpl::from_config().await;
|
||||
@@ -313,4 +383,13 @@ mod tests {
|
||||
let image = media.decode_image(&mut reader, "image/gif").unwrap();
|
||||
media.create_thumbnail(image, "attachments");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn asset_test_animated_gif_is_animated() {
|
||||
let media = MediaImpl::from_config().await;
|
||||
let mut f = NamedTempFile::new().unwrap();
|
||||
f.write_all(include_bytes!("../../tests/assets/anim-icos.gif"))
|
||||
.unwrap();
|
||||
assert_eq!(media.is_animated(&f, "image/gif"), Some(true));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user