From 8eac5a5994c53e4d00bbad6ee9ccc7c706529f6b Mon Sep 17 00:00:00 2001 From: Philip Kristoffersen Date: Thu, 18 Feb 2021 22:30:10 +0100 Subject: [PATCH] WebP support in resize_image (#1360) * Removing unused webpl * Adding clarification comment * Updating documentation * Adding webp --- Cargo.lock | 36 +++++++++++++- components/imageproc/Cargo.toml | 1 + components/imageproc/src/lib.rs | 47 ++++++++++++++----- components/templates/src/global_fns/mod.rs | 10 ++-- .../content/image-processing/index.md | 3 +- 5 files changed, 77 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e05b5ae..e1dd981e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,9 @@ name = "cc" version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +dependencies = [ + "jobserver", +] [[package]] name = "cedarwood" @@ -1017,9 +1020,9 @@ dependencies = [ [[package]] name = "image" -version = "0.23.12" +version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce04077ead78e39ae8610ad26216aed811996b043d47beed5090db674f9e9b5" +checksum = "293f07a1875fa7e9c5897b51aa68b2d8ed8271b87e1a44cb64b9c3d98aabbc0d" dependencies = [ "bytemuck", "byteorder", @@ -1045,6 +1048,7 @@ dependencies = [ "regex", "tera", "utils", + "webp", ] [[package]] @@ -1112,6 +1116,15 @@ dependencies = [ "regex", ] +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.1.22" @@ -1209,6 +1222,15 @@ dependencies = [ "utils", ] +[[package]] +name = "libwebp-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e70c064738b35a28fd6f991d27c0d9680353641d167ae3702a8228dd8272ef6" +dependencies = [ + "cc", +] + [[package]] name = "lindera" version = "0.3.5" @@ -3114,6 +3136,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webp" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea1f2bd35e46165ef40a7fd74f33f64f2912ad92593fbfc5ec75eb2604cfd7b5" +dependencies = [ + "image", + "libwebp-sys", +] + [[package]] name = "webpki" version = "0.21.4" diff --git a/components/imageproc/Cargo.toml b/components/imageproc/Cargo.toml index b4547978..a24e8a34 100644 --- a/components/imageproc/Cargo.toml +++ b/components/imageproc/Cargo.toml @@ -10,6 +10,7 @@ regex = "1.0" tera = "1" image = "0.23" rayon = "1" +webp="0.1.1" errors = { path = "../errors" } utils = { path = "../utils" } diff --git a/components/imageproc/src/lib.rs b/components/imageproc/src/lib.rs index 0b0a8b48..e0daaf64 100644 --- a/components/imageproc/src/lib.rs +++ b/components/imageproc/src/lib.rs @@ -1,11 +1,11 @@ -use std::collections::hash_map::DefaultHasher; +use std::{collections::hash_map::DefaultHasher, io::Write}; use std::collections::hash_map::Entry as HEntry; use std::collections::HashMap; use std::fs::{self, File}; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; -use image::imageops::FilterType; +use image::{EncodableLayout, imageops::FilterType}; use image::{GenericImageView, ImageOutputFormat}; use lazy_static::lazy_static; use rayon::prelude::*; @@ -18,7 +18,7 @@ static RESIZED_SUBDIR: &str = "processed_images"; lazy_static! { pub static ref RESIZED_FILENAME: Regex = - Regex::new(r#"([0-9a-f]{16})([0-9a-f]{2})[.](jpg|png)"#).unwrap(); + Regex::new(r#"([0-9a-f]{16})([0-9a-f]{2})[.](jpg|png|webp)"#).unwrap(); } /// Describes the precise kind of a resize operation @@ -132,6 +132,7 @@ impl Hash for ResizeOp { } } } +const DEFAULT_Q_JPG: u8 = 75; /// Thumbnail image format #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -140,22 +141,26 @@ pub enum Format { Jpeg(u8), /// PNG Png, + /// WebP, The `u8` argument is WebP quality (in percent), None meaning lossless. + WebP(Option), } impl Format { - pub fn from_args(source: &str, format: &str, quality: u8) -> Result { + pub fn from_args(source: &str, format: &str, quality: Option) -> Result { use Format::*; - - assert!(quality > 0 && quality <= 100, "Jpeg quality must be within the range [1; 100]"); - + if let Some(quality) = quality { + assert!(quality > 0 && quality <= 100, "Quality must be within the range [1; 100]"); + } + let jpg_quality = quality.unwrap_or(DEFAULT_Q_JPG); match format { "auto" => match Self::is_lossy(source) { - Some(true) => Ok(Jpeg(quality)), + Some(true) => Ok(Jpeg(jpg_quality)), Some(false) => Ok(Png), None => Err(format!("Unsupported image file: {}", source).into()), }, - "jpeg" | "jpg" => Ok(Jpeg(quality)), - "png" => Ok(Png), + "jpeg" | "jpg" => Ok(Jpeg(jpg_quality)), + "png" => Ok(Png), + "webp" => Ok(WebP(quality)), _ => Err(format!("Invalid image format: {}", format).into()), } } @@ -170,6 +175,8 @@ impl Format { "png" => Some(false), "gif" => Some(false), "bmp" => Some(false), + // It is assumed that webp is lossless, but it can be both + "webp" => Some(false), _ => None, }) .unwrap_or(None) @@ -182,6 +189,7 @@ impl Format { match *self { Png => "png", Jpeg(_) => "jpg", + WebP(_) => "webp" } } } @@ -193,7 +201,9 @@ impl Hash for Format { let q = match *self { Png => 0, - Jpeg(q) => q, + Jpeg(q) => q, + WebP(None) => 0, + WebP(Some(q)) => q }; hasher.write_u8(q); @@ -232,7 +242,7 @@ impl ImageOp { width: Option, height: Option, format: &str, - quality: u8, + quality: Option, ) -> Result { let op = ResizeOp::from_args(op, width, height)?; let format = Format::from_args(&source, format, quality)?; @@ -303,6 +313,19 @@ impl ImageOp { Format::Jpeg(q) => { img.write_to(&mut f, ImageOutputFormat::Jpeg(q))?; } + Format::WebP(q) => { + let encoder = webp::Encoder::from_image(&img); + let memory = match q { + Some(q) => { + encoder.encode(q as f32 / 100.) + } + None => { + encoder.encode_lossless() + } + }; + let mut bytes = memory.as_bytes(); + f.write_all(&mut bytes)?; + } } Ok(()) diff --git a/components/templates/src/global_fns/mod.rs b/components/templates/src/global_fns/mod.rs index fa7bb283..7f4d48d4 100644 --- a/components/templates/src/global_fns/mod.rs +++ b/components/templates/src/global_fns/mod.rs @@ -221,7 +221,6 @@ impl ResizeImage { static DEFAULT_OP: &str = "fill"; static DEFAULT_FMT: &str = "auto"; -const DEFAULT_Q: u8 = 75; impl TeraFn for ResizeImage { fn call(&self, args: &HashMap) -> Result { @@ -248,10 +247,11 @@ impl TeraFn for ResizeImage { .unwrap_or_else(|| DEFAULT_FMT.to_string()); let quality = - optional_arg!(u8, args.get("quality"), "`resize_image`: `quality` must be a number") - .unwrap_or(DEFAULT_Q); - if quality == 0 || quality > 100 { - return Err("`resize_image`: `quality` must be in range 1-100".to_string().into()); + optional_arg!(u8, args.get("quality"), "`resize_image`: `quality` must be a number"); + if let Some(quality) = quality { + if quality == 0 || quality > 100 { + return Err("`resize_image`: `quality` must be in range 1-100".to_string().into()); + } } let mut imageproc = self.imageproc.lock().unwrap(); diff --git a/docs/content/documentation/content/image-processing/index.md b/docs/content/documentation/content/image-processing/index.md index bd860c37..30a4a310 100644 --- a/docs/content/documentation/content/image-processing/index.md +++ b/docs/content/documentation/content/image-processing/index.md @@ -28,10 +28,11 @@ resize_image(path, width, height, op, format, quality) - `"auto"` - `"jpg"` - `"png"` + - `"webp"` The default is `"auto"`, this means that the format is chosen based on input image format. JPEG is chosen for JPEGs and other lossy formats, and PNG is chosen for PNGs and other lossless formats. -- `quality` (_optional_): JPEG quality of the resized image, in percent. Only used when encoding JPEGs; default value is `75`. +- `quality` (_optional_): JPEG or WebP quality of the resized image, in percent. Only used when encoding JPEGs or WebPs; for JPEG default value is `75`, for WebP default is lossless. ### Image processing and return value