diff --git a/.github/actions/setup-dav1d/action.yml b/.github/actions/setup-dav1d/action.yml new file mode 100644 index 000000000..10a8f12d4 --- /dev/null +++ b/.github/actions/setup-dav1d/action.yml @@ -0,0 +1,87 @@ +name: Dav1d +description: Compile and install dav1d + +runs: + using: composite + steps: + # Linux Section + - name: Install nasm + if: runner.os == 'Linux' + uses: ilammy/setup-nasm@v1 + + - name: Install Python 3.9 + if: runner.os == 'Linux' + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install pip packages + if: runner.os == 'Linux' + shell: bash + run: | + pip install -U pip + pip install -U wheel setuptools + pip install -U meson ninja + + - name: Build dav1d + if: runner.os == 'Linux' + env: + DAV1D_DIR: dav1d_dir + LIB_PATH: lib/x86_64-linux-gnu + shell: bash + run: | + git clone --branch 1.3.0 --depth 1 https://code.videolan.org/videolan/dav1d.git + cd dav1d + meson build -Dprefix=$HOME/$DAV1D_DIR -Denable_tools=false -Denable_examples=false --buildtype release + ninja -C build + ninja -C build install + echo "PKG_CONFIG_PATH=$HOME/$DAV1D_DIR/$LIB_PATH/pkgconfig" >> $GITHUB_ENV + echo "LD_LIBRARY_PATH=$HOME/$DAV1D_DIR/$LIB_PATH" >> $GITHUB_ENV + + # Windows setup + - name: Install nasm + if: runner.os == 'Windows' + uses: ilammy/setup-nasm@v1 + + - name: Install Python 3.9 + if: runner.os == 'Windows' + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install pip packages + if: runner.os == 'Windows' + shell: powershell + run: | + pip install -U pip + pip install -U wheel setuptools + pip install -U meson ninja + + - name: Setting up environment + if: runner.os == 'Windows' + shell: bash + run: | + echo "PKG_CONFIG=c:\build\bin\pkg-config.exe" >> $GITHUB_ENV + echo "PKG_CONFIG_PATH=C:\build\lib\pkgconfig" >> $GITHUB_ENV + echo "C:\build\bin" >> $GITHUB_PATH + + - name: Build pkg-config + if: runner.os == 'Windows' + shell: powershell + run: | + git clone --branch meson-glib-subproject --depth 1 https://gitlab.freedesktop.org/tpm/pkg-config.git + cd pkg-config + meson build -Dprefix=C:\build --buildtype release + ninja -C build + ninja -C build install + + - name: Build dav1d + if: runner.os == 'Windows' + shell: powershell + run: | + git clone --branch 1.3.0 --depth 1 https://code.videolan.org/videolan/dav1d.git + cd dav1d + meson build -Dprefix=C:\build -Denable_tools=false -Denable_examples=false --buildtype release + ninja -C build + ninja -C build install + diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3c7111657..9f8ff27e2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,6 +34,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 + - name: Setup dav1d + uses: ./.github/actions/setup-dav1d + - name: Setup rust uses: ./.github/actions/setup-rust diff --git a/core/Cargo.toml b/core/Cargo.toml index 681d50a06..a3832861f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,7 +14,7 @@ email = { path = "../crates/email" } epub = { git = "https://github.com/stumpapp/epub-rs", rev = "38e091abe96875952556ab7dec195022d0230e14" } futures = { workspace = true } globset = "0.4.14" -image = "0.25.2" +image = {version = "0.25.2", features = ["avif-native"]} infer = "0.16.0" itertools = "0.12.1" prisma-client-rust = { workspace = true } @@ -53,6 +53,7 @@ criterion = { version = "0.5.1", features = ["html_reports", "async_tokio"] } [build-dependencies] chrono = "0.4.37" +system-deps = "7.0.1" [target.'cfg(target_os = "linux")'.dependencies] libc = "0.2.152" diff --git a/core/src/filesystem/common.rs b/core/src/filesystem/common.rs index 6cef35fc8..b1adf9227 100644 --- a/core/src/filesystem/common.rs +++ b/core/src/filesystem/common.rs @@ -9,7 +9,8 @@ use walkdir::WalkDir; use super::{media::is_accepted_cover_name, ContentType, FileError}; -pub const ACCEPTED_IMAGE_EXTENSIONS: [&str; 5] = ["jpg", "png", "jpeg", "webp", "gif"]; +pub const ACCEPTED_IMAGE_EXTENSIONS: [&str; 6] = + ["jpg", "png", "jpeg", "webp", "avif", "gif"]; pub fn read_entire_file>(path: P) -> Result, FileError> { let mut file = File::open(path)?; diff --git a/core/src/filesystem/content_type.rs b/core/src/filesystem/content_type.rs index 6f5b3ff34..b3d3239ed 100644 --- a/core/src/filesystem/content_type.rs +++ b/core/src/filesystem/content_type.rs @@ -23,6 +23,7 @@ pub enum ContentType { PNG, JPEG, WEBP, + AVIF, GIF, TXT, #[default] @@ -79,6 +80,7 @@ impl ContentType { "jpg" => ContentType::JPEG, "jpeg" => ContentType::JPEG, "webp" => ContentType::WEBP, + "avif" => ContentType::AVIF, "gif" => ContentType::GIF, "txt" => ContentType::TXT, _ => temporary_content_workarounds(extension), @@ -265,6 +267,7 @@ impl ContentType { ContentType::PNG => "png", ContentType::JPEG => "jpg", ContentType::WEBP => "webp", + ContentType::AVIF => "avif", ContentType::GIF => "gif", ContentType::TXT => "txt", ContentType::UNKNOWN => "", @@ -291,6 +294,7 @@ impl From<&str> for ContentType { "image/png" => ContentType::PNG, "image/jpeg" => ContentType::JPEG, "image/webp" => ContentType::WEBP, + "image/avif" => ContentType::AVIF, "image/gif" => ContentType::GIF, _ => ContentType::UNKNOWN, } @@ -312,6 +316,7 @@ impl std::fmt::Display for ContentType { ContentType::PNG => write!(f, "image/png"), ContentType::JPEG => write!(f, "image/jpeg"), ContentType::WEBP => write!(f, "image/webp"), + ContentType::AVIF => write!(f, "image/avif"), ContentType::GIF => write!(f, "image/gif"), ContentType::TXT => write!(f, "text/plain"), ContentType::UNKNOWN => write!(f, "unknown"), @@ -327,6 +332,7 @@ impl From for ContentType { // ImageFormat::JpegXl => ContentType::JPEG, ImageFormat::Png => ContentType::PNG, ImageFormat::Webp => ContentType::WEBP, + ImageFormat::Avif => ContentType::AVIF, } } } @@ -349,6 +355,7 @@ impl TryFrom for image::ImageFormat { ContentType::PNG => Ok(image::ImageFormat::Png), ContentType::JPEG => Ok(image::ImageFormat::Jpeg), ContentType::WEBP => Ok(image::ImageFormat::WebP), + ContentType::AVIF => Ok(image::ImageFormat::Avif), ContentType::GIF => Ok(image::ImageFormat::Gif), ContentType::XHTML => Err(unsupported_error("ContentType::XHTML")), ContentType::XML => Err(unsupported_error("ContentType::XML")), diff --git a/core/src/filesystem/image/generic.rs b/core/src/filesystem/image/generic.rs index 39099f8e0..0b1eb2081 100644 --- a/core/src/filesystem/image/generic.rs +++ b/core/src/filesystem/image/generic.rs @@ -7,7 +7,7 @@ use crate::filesystem::{image::process::resized_dimensions, FileError}; use super::process::{self, ImageProcessor, ImageProcessorOptions}; /// An image processor that works for the most common image types, primarily -/// JPEG and PNG formats. +/// JPEG and PNG and AVIF formats. pub struct GenericImageProcessor; impl ImageProcessor for GenericImageProcessor { @@ -25,8 +25,18 @@ impl ImageProcessor for GenericImageProcessor { } let format = match options.format { - process::ImageFormat::Jpeg => Ok(ImageFormat::Jpeg), + process::ImageFormat::Jpeg => { + if image.color().has_alpha() { + if image.color().has_color() { + image = image::DynamicImage::from(image.into_rgb8()); + } else { + image = image::DynamicImage::from(image.into_luma8()); + } + } + Ok(ImageFormat::Jpeg) + }, process::ImageFormat::Png => Ok(ImageFormat::Png), + process::ImageFormat::Avif => Ok(ImageFormat::Avif), // TODO: change error kind _ => Err(FileError::UnknownError(String::from( "Incorrect image processor for requested format.", @@ -54,10 +64,12 @@ mod tests { use super::*; use crate::filesystem::image::{ - tests::{get_test_jpg_path, get_test_png_path}, + tests::{get_test_avif_path, get_test_jpg_path, get_test_png_path}, ImageFormat, ImageProcessorOptions, }; + //JPG -> other Tests + //JPG -> JPG #[test] fn test_generate_jpg_to_jpg() { let jpg_path = get_test_jpg_path(); @@ -119,37 +131,37 @@ mod tests { assert_eq!(dimensions.1, 100); } + //JPG -> PNG #[test] - fn test_generate_png_to_jpg() { - let png_path = get_test_png_path(); + fn test_generate_jpg_to_png() { + let jpg_path = get_test_jpg_path(); let options = ImageProcessorOptions { - format: ImageFormat::Jpeg, + format: ImageFormat::Png, ..Default::default() }; - let buffer = GenericImageProcessor::generate_from_path(&png_path, options) + let buffer = GenericImageProcessor::generate_from_path(&jpg_path, options) .expect("Failed to generate image buffer"); assert!(!buffer.is_empty()); - // should be a valid JPEG + // should be a valid PNG assert!( - image::load_from_memory_with_format(&buffer, image::ImageFormat::Jpeg) - .is_ok() + image::load_from_memory_with_format(&buffer, image::ImageFormat::Png).is_ok() ); } #[test] - fn test_generate_png_to_jpg_with_rescale() { - let png_path = get_test_png_path(); + fn test_generate_jpg_to_png_with_rescale() { + let jpg_path = get_test_jpg_path(); let options = ImageProcessorOptions { - format: ImageFormat::Jpeg, + format: ImageFormat::Png, resize_options: Some(ImageResizeOptions::scaled(0.5, 0.5)), ..Default::default() }; let current_dimensions = - image::image_dimensions(&png_path).expect("Failed to get dimensions"); + image::image_dimensions(&jpg_path).expect("Failed to get dimensions"); - let buffer = GenericImageProcessor::generate_from_path(&png_path, options) + let buffer = GenericImageProcessor::generate_from_path(&jpg_path, options) .expect("Failed to generate image buffer"); let new_dimensions = image::load_from_memory(&buffer) @@ -161,15 +173,15 @@ mod tests { } #[test] - fn test_generate_png_to_jpg_with_resize() { - let png_path = get_test_png_path(); + fn test_generate_jpg_to_png_with_resize() { + let jpg_path = get_test_jpg_path(); let options = ImageProcessorOptions { - format: ImageFormat::Jpeg, + format: ImageFormat::Png, resize_options: Some(ImageResizeOptions::sized(100f32, 100f32)), ..Default::default() }; - let buffer = GenericImageProcessor::generate_from_path(&png_path, options) + let buffer = GenericImageProcessor::generate_from_path(&jpg_path, options) .expect("Failed to generate image buffer"); let dimensions = image::load_from_memory(&buffer) @@ -180,28 +192,48 @@ mod tests { assert_eq!(dimensions.1, 100); } + //JPG -> webp #[test] - fn test_generate_jpg_to_png() { + fn test_generate_jpg_to_webp_fail() { let jpg_path = get_test_jpg_path(); let options = ImageProcessorOptions { - format: ImageFormat::Png, + format: ImageFormat::Webp, + ..Default::default() + }; + + let result = GenericImageProcessor::generate_from_path(&jpg_path, options); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "An unknown error occurred: Incorrect image processor for requested format." + ); + } + + //JPG -> AVIF + #[test] + fn test_generate_jpg_to_avif() { + let jpg_path = get_test_jpg_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Avif, ..Default::default() }; let buffer = GenericImageProcessor::generate_from_path(&jpg_path, options) .expect("Failed to generate image buffer"); + assert!(!buffer.is_empty()); - // should be a valid PNG + // should be a valid Avif assert!( - image::load_from_memory_with_format(&buffer, image::ImageFormat::Png).is_ok() + image::load_from_memory_with_format(&buffer, image::ImageFormat::Avif) + .is_ok() ); } #[test] - fn test_generate_jpg_to_png_with_rescale() { + fn test_generate_jpg_to_avif_with_rescale() { let jpg_path = get_test_jpg_path(); let options = ImageProcessorOptions { - format: ImageFormat::Png, + format: ImageFormat::Avif, resize_options: Some(ImageResizeOptions::scaled(0.5, 0.5)), ..Default::default() }; @@ -212,19 +244,20 @@ mod tests { let buffer = GenericImageProcessor::generate_from_path(&jpg_path, options) .expect("Failed to generate image buffer"); - let new_dimensions = image::load_from_memory(&buffer) - .expect("Failed to load image from buffer") - .dimensions(); + let new_dimensions = + image::load_from_memory_with_format(&buffer, image::ImageFormat::Avif) + .expect("Failed to load image from buffer") + .dimensions(); assert_eq!(new_dimensions.0, (current_dimensions.0 as f32 * 0.5) as u32); assert_eq!(new_dimensions.1, (current_dimensions.1 as f32 * 0.5) as u32); } #[test] - fn test_generate_jpg_to_png_with_resize() { + fn test_generate_jpg_to_avif_with_resize() { let jpg_path = get_test_jpg_path(); let options = ImageProcessorOptions { - format: ImageFormat::Png, + format: ImageFormat::Avif, resize_options: Some(ImageResizeOptions::sized(100f32, 100f32)), ..Default::default() }; @@ -232,14 +265,17 @@ mod tests { let buffer = GenericImageProcessor::generate_from_path(&jpg_path, options) .expect("Failed to generate image buffer"); - let dimensions = image::load_from_memory(&buffer) - .expect("Failed to load image from buffer") - .dimensions(); + let dimensions = + image::load_from_memory_with_format(&buffer, image::ImageFormat::Avif) + .expect("Failed to load image from buffer") + .dimensions(); assert_eq!(dimensions.0, 100); assert_eq!(dimensions.1, 100); } + // PNG -> other + // PNG -> PNG #[test] fn test_generate_png_to_png() { let png_path = get_test_png_path(); @@ -300,19 +336,319 @@ mod tests { assert_eq!(dimensions.1, 100); } + //PNG -> JPG #[test] - fn test_generate_jpg_to_webp_fail() { - let jpg_path = get_test_jpg_path(); + fn test_generate_png_to_jpg() { + let png_path = get_test_png_path(); let options = ImageProcessorOptions { - format: ImageFormat::Webp, + format: ImageFormat::Jpeg, ..Default::default() }; - let result = GenericImageProcessor::generate_from_path(&jpg_path, options); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().to_string(), - "An unknown error occurred: Incorrect image processor for requested format." + let buffer = GenericImageProcessor::generate_from_path(&png_path, options) + .expect("Failed to generate image buffer"); + assert!(!buffer.is_empty()); + // should be a valid JPEG + assert!( + image::load_from_memory_with_format(&buffer, image::ImageFormat::Jpeg) + .is_ok() ); } + + #[test] + fn test_generate_png_to_jpg_with_rescale() { + let png_path = get_test_png_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Jpeg, + resize_options: Some(ImageResizeOptions::scaled(0.5, 0.5)), + ..Default::default() + }; + + let current_dimensions = + image::image_dimensions(&png_path).expect("Failed to get dimensions"); + + let buffer = GenericImageProcessor::generate_from_path(&png_path, options) + .expect("Failed to generate image buffer"); + + let new_dimensions = image::load_from_memory(&buffer) + .expect("Failed to load image from buffer") + .dimensions(); + + assert_eq!(new_dimensions.0, (current_dimensions.0 as f32 * 0.5) as u32); + assert_eq!(new_dimensions.1, (current_dimensions.1 as f32 * 0.5) as u32); + } + + #[test] + fn test_generate_png_to_jpg_with_resize() { + let png_path = get_test_png_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Jpeg, + resize_options: Some(ImageResizeOptions::sized(100f32, 100f32)), + ..Default::default() + }; + + let buffer = GenericImageProcessor::generate_from_path(&png_path, options) + .expect("Failed to generate image buffer"); + + let dimensions = image::load_from_memory(&buffer) + .expect("Failed to load image from buffer") + .dimensions(); + + assert_eq!(dimensions.0, 100); + assert_eq!(dimensions.1, 100); + } + + //PNG -> AVIF + #[test] + fn test_generate_png_to_avif() { + let png_path = get_test_png_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Avif, + ..Default::default() + }; + + let buffer = GenericImageProcessor::generate_from_path(&png_path, options) + .expect("Failed to generate image buffer"); + assert!(!buffer.is_empty()); + // should be a valid JPEG + assert!( + image::load_from_memory_with_format(&buffer, image::ImageFormat::Avif) + .is_ok() + ); + } + + #[test] + fn test_generate_png_to_avif_with_rescale() { + let png_path = get_test_png_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Avif, + resize_options: Some(ImageResizeOptions::scaled(0.5, 0.5)), + ..Default::default() + }; + + let current_dimensions = + image::image_dimensions(&png_path).expect("Failed to get dimensions"); + + let buffer = GenericImageProcessor::generate_from_path(&png_path, options) + .expect("Failed to generate image buffer"); + + let new_dimensions = + image::load_from_memory_with_format(&buffer, image::ImageFormat::Avif) + .expect("Failed to load image from buffer") + .dimensions(); + + assert_eq!(new_dimensions.0, (current_dimensions.0 as f32 * 0.5) as u32); + assert_eq!(new_dimensions.1, (current_dimensions.1 as f32 * 0.5) as u32); + } + + #[test] + fn test_generate_png_to_avif_with_resize() { + let png_path = get_test_png_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Avif, + resize_options: Some(ImageResizeOptions::sized(100f32, 100f32)), + ..Default::default() + }; + + let buffer = GenericImageProcessor::generate_from_path(&png_path, options) + .expect("Failed to generate image buffer"); + + let dimensions = + image::load_from_memory_with_format(&buffer, image::ImageFormat::Avif) + .expect("Failed to load image from buffer") + .dimensions(); + + assert_eq!(dimensions.0, 100); + assert_eq!(dimensions.1, 100); + } + + //AVIF -> other + //AVIF -> AVIF + #[test] + fn test_generate_avif_to_avif() { + let avif_path = get_test_avif_path(); + + let options = ImageProcessorOptions { + format: ImageFormat::Avif, + ..Default::default() + }; + + let buffer = GenericImageProcessor::generate_from_path(&avif_path, options) + .expect("Failed to generate image buffer"); + assert!(!buffer.is_empty()); + + // should *still* be a valid AVIF + assert!( + image::load_from_memory_with_format(&buffer, image::ImageFormat::Avif) + .is_ok() + ); + } + + #[test] + fn test_generate_avif_to_avif_with_rescale() { + let avif_path = get_test_avif_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Avif, + resize_options: Some(ImageResizeOptions::scaled(0.5, 0.5)), + ..Default::default() + }; + + let current_dimensions = + image::image_dimensions(&avif_path).expect("Failed to get dimensions"); + + let buffer = GenericImageProcessor::generate_from_path(&avif_path, options) + .expect("Failed to generate image buffer"); + + let new_dimensions = + image::load_from_memory_with_format(&buffer, image::ImageFormat::Avif) + .expect("Failed to load image from buffer") + .dimensions(); + + assert_eq!(new_dimensions.0, (current_dimensions.0 as f32 * 0.5) as u32); + assert_eq!(new_dimensions.1, (current_dimensions.1 as f32 * 0.5) as u32); + } + + #[test] + fn test_generate_avif_to_avif_with_resize() { + let avif_path = get_test_avif_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Avif, + resize_options: Some(ImageResizeOptions::sized(100f32, 100f32)), + ..Default::default() + }; + + let buffer = GenericImageProcessor::generate_from_path(&avif_path, options) + .expect("Failed to generate image buffer"); + + let dimensions = + image::load_from_memory_with_format(&buffer, image::ImageFormat::Avif) + .expect("Failed to load image from buffer") + .dimensions(); + + assert_eq!(dimensions.0, 100); + assert_eq!(dimensions.1, 100); + } + + //AVIF -> PNG + #[test] + fn test_generate_avif_to_png() { + let avif_path = get_test_avif_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Png, + ..Default::default() + }; + + let buffer = GenericImageProcessor::generate_from_path(&avif_path, options) + .expect("Failed to generate image buffer"); + assert!(!buffer.is_empty()); + // should be a valid PNG + assert!( + image::load_from_memory_with_format(&buffer, image::ImageFormat::Png).is_ok() + ); + } + + #[test] + fn test_generate_avif_to_png_with_rescale() { + let avif_path = get_test_avif_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Png, + resize_options: Some(ImageResizeOptions::scaled(0.5, 0.5)), + ..Default::default() + }; + + let current_dimensions = + image::image_dimensions(&avif_path).expect("Failed to get dimensions"); + + let buffer = GenericImageProcessor::generate_from_path(&avif_path, options) + .expect("Failed to generate image buffer"); + + let new_dimensions = image::load_from_memory(&buffer) + .expect("Failed to load image from buffer") + .dimensions(); + + assert_eq!(new_dimensions.0, (current_dimensions.0 as f32 * 0.5) as u32); + assert_eq!(new_dimensions.1, (current_dimensions.1 as f32 * 0.5) as u32); + } + + #[test] + fn test_generate_avif_to_png_with_resize() { + let avif_path = get_test_avif_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Png, + resize_options: Some(ImageResizeOptions::sized(100f32, 100f32)), + ..Default::default() + }; + + let buffer = GenericImageProcessor::generate_from_path(&avif_path, options) + .expect("Failed to generate image buffer"); + + let dimensions = image::load_from_memory(&buffer) + .expect("Failed to load image from buffer") + .dimensions(); + + assert_eq!(dimensions.0, 100); + assert_eq!(dimensions.1, 100); + } + + //AVIF -> JPG + #[test] + fn test_generate_avif_to_jpg() { + let avif_path = get_test_avif_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Jpeg, + ..Default::default() + }; + + let buffer = GenericImageProcessor::generate_from_path(&avif_path, options) + .expect("Failed to generate image buffer"); + assert!(!buffer.is_empty()); + // should be a valid PNG + assert!( + image::load_from_memory_with_format(&buffer, image::ImageFormat::Jpeg) + .is_ok() + ); + } + + #[test] + fn test_generate_avif_to_jpg_with_rescale() { + let avif_path = get_test_avif_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Jpeg, + resize_options: Some(ImageResizeOptions::scaled(0.5, 0.5)), + ..Default::default() + }; + + let current_dimensions = + image::image_dimensions(&avif_path).expect("Failed to get dimensions"); + + let buffer = GenericImageProcessor::generate_from_path(&avif_path, options) + .expect("Failed to generate image buffer"); + + let new_dimensions = image::load_from_memory(&buffer) + .expect("Failed to load image from buffer") + .dimensions(); + + assert_eq!(new_dimensions.0, (current_dimensions.0 as f32 * 0.5) as u32); + assert_eq!(new_dimensions.1, (current_dimensions.1 as f32 * 0.5) as u32); + } + + #[test] + fn test_generate_avif_to_jpg_with_resize() { + let avif_path = get_test_avif_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Jpeg, + resize_options: Some(ImageResizeOptions::sized(100f32, 100f32)), + ..Default::default() + }; + + let buffer = GenericImageProcessor::generate_from_path(&avif_path, options) + .expect("Failed to generate image buffer"); + + let dimensions = image::load_from_memory(&buffer) + .expect("Failed to load image from buffer") + .dimensions(); + + assert_eq!(dimensions.0, 100); + assert_eq!(dimensions.1, 100); + } } diff --git a/core/src/filesystem/image/mod.rs b/core/src/filesystem/image/mod.rs index a0193a728..746abedd4 100644 --- a/core/src/filesystem/image/mod.rs +++ b/core/src/filesystem/image/mod.rs @@ -44,14 +44,15 @@ mod tests { .to_string() } + pub fn get_test_avif_path() -> String { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("integration-tests/data/example.avif") + .to_string_lossy() + .to_string() + } + // TODO(339): Avif + Jxl support // pub fn get_test_jxl_path() -> String { // PathBuf::from(env!("CARGO_MANIFEST_DIR")) // .join("integration-tests/data/example.jxl") - // } - - // pub fn get_test_avif_path() -> String { - // PathBuf::from(env!("CARGO_MANIFEST_DIR")) - // .join("integration-tests/data/example.avif") - // } } diff --git a/core/src/filesystem/image/process.rs b/core/src/filesystem/image/process.rs index bfde28391..d273f7ae0 100644 --- a/core/src/filesystem/image/process.rs +++ b/core/src/filesystem/image/process.rs @@ -58,6 +58,7 @@ pub enum ImageFormat { Jpeg, // JpegXl, Png, + Avif, } impl ImageFormat { @@ -68,7 +69,7 @@ impl ImageFormat { ImageFormat::Jpeg => "jpeg", // TODO(339): Support JpegXl and Avif // ImageFormat::JpegXl => "jxl", - // ImageFormat::Avif => "avif", + ImageFormat::Avif => "avif", ImageFormat::Png => "png", } } @@ -78,6 +79,7 @@ impl From for image::ImageFormat { fn from(val: ImageFormat) -> Self { match val { ImageFormat::Webp => image::ImageFormat::WebP, + ImageFormat::Avif => image::ImageFormat::Avif, ImageFormat::Jpeg => image::ImageFormat::Jpeg, // See https://github.com/image-rs/image/issues/1765. Image removed the // unsupported enum variant, which makes this awkward to support... @@ -207,6 +209,7 @@ mod tests { #[test] fn test_image_format_extension() { assert_eq!(ImageFormat::Webp.extension(), "webp"); + assert_eq!(ImageFormat::Avif.extension(), "avif"); assert_eq!(ImageFormat::Jpeg.extension(), "jpeg"); // assert_eq!(ImageFormat::JpegXl.extension(), "jxl"); assert_eq!(ImageFormat::Png.extension(), "png"); @@ -218,6 +221,10 @@ mod tests { image::ImageFormat::from(ImageFormat::Webp), image::ImageFormat::WebP ); + assert_eq!( + image::ImageFormat::from(ImageFormat::Avif), + image::ImageFormat::Avif + ); assert_eq!( image::ImageFormat::from(ImageFormat::Jpeg), image::ImageFormat::Jpeg diff --git a/core/src/filesystem/image/webp.rs b/core/src/filesystem/image/webp.rs index df424a9f4..6e82a7dc9 100644 --- a/core/src/filesystem/image/webp.rs +++ b/core/src/filesystem/image/webp.rs @@ -60,7 +60,9 @@ impl WebpProcessor { mod tests { use super::*; use crate::filesystem::image::{ - tests::{get_test_jpg_path, get_test_png_path, get_test_webp_path}, + tests::{ + get_test_avif_path, get_test_jpg_path, get_test_png_path, get_test_webp_path, + }, ImageFormat, ImageProcessorOptions, }; use std::fs; @@ -221,6 +223,73 @@ mod tests { assert_eq!(dimensions.1, 100); } + #[test] + fn test_generate_webp_from_avif() { + let avif_path = get_test_avif_path(); + let options = ImageProcessorOptions { + resize_options: None, + format: ImageFormat::Webp, + quality: None, + page: None, + }; + + let result = WebpProcessor::generate_from_path(&avif_path, options); + assert!(result.is_ok()); + + let webp_bytes = result.unwrap(); + // should be a valid webp image + assert!(image::load_from_memory_with_format( + &webp_bytes, + image::ImageFormat::WebP + ) + .is_ok()) + } + + #[test] + fn test_generate_webp_from_avif_with_rescale() { + let avif_path = get_test_avif_path(); + let options = ImageProcessorOptions { + resize_options: Some(ImageResizeOptions::scaled(0.5, 0.5)), + format: ImageFormat::Webp, + quality: None, + page: None, + }; + + let current_dimensions = + image::image_dimensions(&avif_path).expect("Failed to get dimensions"); + + let buffer = WebpProcessor::generate_from_path(&avif_path, options) + .expect("Failed to generate image buffer"); + + let dimensions = image::load_from_memory(&buffer) + .expect("Failed to load image from buffer") + .dimensions(); + + assert_eq!(dimensions.0, (current_dimensions.0 as f32 * 0.5) as u32); + assert_eq!(dimensions.1, (current_dimensions.1 as f32 * 0.5) as u32); + } + + #[test] + fn test_generate_webp_from_avif_with_resize() { + let avif_path = get_test_avif_path(); + let options = ImageProcessorOptions { + resize_options: Some(ImageResizeOptions::sized(100f32, 100f32)), + format: ImageFormat::Webp, + quality: None, + page: None, + }; + + let buffer = WebpProcessor::generate_from_path(&avif_path, options) + .expect("Failed to generate image buffer"); + + let dimensions = image::load_from_memory(&buffer) + .expect("Failed to load image from buffer") + .dimensions(); + + assert_eq!(dimensions.0, 100); + assert_eq!(dimensions.1, 100); + } + #[test] fn test_generate_webp_from_webp() { let webp_path = get_test_webp_path(); diff --git a/docker/Dockerfile b/docker/Dockerfile index 11f97d303..e6bafd65e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -16,6 +16,7 @@ RUN yarn config set network-timeout 300000 && \ mv ./apps/web/dist/ ./build && \ if [ ! -d "./build" ] || [ ! "$(ls -A ./build)" ]; then exit 1; fi + # ------------------------------------------------------------------------------ # Cargo Build Stage # ------------------------------------------------------------------------------ @@ -28,16 +29,33 @@ ENV GIT_REV=${GIT_REV} ARG TAGS ENV TAGS=${TAGS} -WORKDIR /app - RUN apt-get update && apt-get install -y \ build-essential \ cmake \ git \ libssl-dev \ pkg-config \ + meson \ + ninja-build \ + nasm \ libsqlite3-dev; +#Building dav1d for AVIF Support +RUN git clone https://github.com/stumpapp/dav1d.git + +WORKDIR /dav1d + +RUN mkdir build + +WORKDIR /dav1d/build + +RUN meson setup ../; \ + ninja; \ + ninja install + +#Cargo build for stump +WORKDIR /app + COPY . . RUN ./scripts/release/utils.sh -w; \ @@ -95,4 +113,4 @@ ENV STUMP_CONFIG_DIR=/config \ WORKDIR /app -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docs/pages/contributing.mdx b/docs/pages/contributing.mdx index fd760d83a..6859af1ab 100644 --- a/docs/pages/contributing.mdx +++ b/docs/pages/contributing.mdx @@ -23,7 +23,7 @@ If you're completely new to rust and/or web development, I put together a small [Rosetta](https://support.apple.com/en-us/>HT211861). -You need to install [yarn](https://yarnpkg.com), [rust](https://www.rust-lang.org/tools/install) and [node](https://nodejs.org/en/download/). Additionally, if you want to run any of the dev scripts on the rust side of things, you'll need to install [cargo-watch](https://crates.io/crates/cargo-watch). Afterwards, run the following: +You need to install [yarn](https://yarnpkg.com), [rust](https://www.rust-lang.org/tools/install), [dav1d](https://github.com/stumpapp/dav1d), and [node](https://nodejs.org/en/download/). Additionally, if you want to run any of the dev scripts on the rust side of things, you'll need to install [cargo-watch](https://crates.io/crates/cargo-watch). Afterwards, run the following: ```bash yarn run setup diff --git a/scripts/system-setup.sh b/scripts/system-setup.sh index 757c117f0..727359ac9 100755 --- a/scripts/system-setup.sh +++ b/scripts/system-setup.sh @@ -6,6 +6,7 @@ source "${SCRIPTS_DIR}/lib" _DEV_SETUP=${DEV_SETUP:=1} _CHECK_CARGO=${CHECK_CARGO:=1} _CHECK_NODE=${CHECK_NODE:=1} +_CHECK_DAV1D=${CHECK_DAV1D:=1} _FORCE_INSTALL_YARN=${INSTALL_YARN:=0} dev_setup() { @@ -30,6 +31,8 @@ if [ ${_CHECK_CARGO} == 1 ]; then fi fi + + if [ ${_CHECK_NODE} == 1 ]; then which node &> /dev/null if [ $? -eq 1 ]; then @@ -111,10 +114,11 @@ if [[ "$OSTYPE" == "linux-gnu"* ]]; then openssl \ appmenu-gtk-module \ gtk3 \ + dav1d \ libappindicator-gtk3 librsvg libvips elif which dnf &> /dev/null; then sudo dnf check-update - sudo dnf install openssl-devel webkit2gtk4.0-devel curl wget libappindicator-gtk3 librsvg2-devel + sudo dnf install openssl-devel webkit2gtk4.0-devel curl wget libappindicator-gtk3 librsvg2-devel dav1d sudo dnf group install "C Development Tools and Libraries" else log_error $UNSUPPORTED_DISTRO @@ -134,3 +138,19 @@ elif [[ "$OSTYPE" == "darwin"* ]]; then else log_error "Your OS '$OSTYPE' is not supported by the pre-setup script. $CALL_TO_ACTION_LOL" fi + +if [ ${_CHECK_DAV1D} == 1 ]; then + which dav1d &> /dev/null + if [ $? -ne 0 ]; then + echo "Dav1d requirement is not met. Visit https://code.videolan.org/videolan/dav1d" + else + curver="$(dav1d --version)" + reqver="1.3.0" + if [ "$(printf '%s\n' "$reqver" "$curver" | sort -V | head -n1)" = "$reqver" ]; then + echo "Dav1d requirement met!" + else + echo "Dav1d requirement is not met (version must be greater than 1.3.0). Visit https://code.videolan.org/videolan/dav1d" + fi + fi +fi +