WebP support in resize_image (#1360)

* Removing unused webpl

* Adding clarification comment

* Updating documentation

* Adding webp
This commit is contained in:
Philip Kristoffersen 2021-02-18 22:30:10 +01:00 committed by GitHub
parent d4db249997
commit 8eac5a5994
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 77 additions and 20 deletions

36
Cargo.lock generated
View file

@ -212,6 +212,9 @@ name = "cc"
version = "1.0.66" version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
dependencies = [
"jobserver",
]
[[package]] [[package]]
name = "cedarwood" name = "cedarwood"
@ -1017,9 +1020,9 @@ dependencies = [
[[package]] [[package]]
name = "image" name = "image"
version = "0.23.12" version = "0.23.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce04077ead78e39ae8610ad26216aed811996b043d47beed5090db674f9e9b5" checksum = "293f07a1875fa7e9c5897b51aa68b2d8ed8271b87e1a44cb64b9c3d98aabbc0d"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder", "byteorder",
@ -1045,6 +1048,7 @@ dependencies = [
"regex", "regex",
"tera", "tera",
"utils", "utils",
"webp",
] ]
[[package]] [[package]]
@ -1112,6 +1116,15 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "jobserver"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "jpeg-decoder" name = "jpeg-decoder"
version = "0.1.22" version = "0.1.22"
@ -1209,6 +1222,15 @@ dependencies = [
"utils", "utils",
] ]
[[package]]
name = "libwebp-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e70c064738b35a28fd6f991d27c0d9680353641d167ae3702a8228dd8272ef6"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "lindera" name = "lindera"
version = "0.3.5" version = "0.3.5"
@ -3114,6 +3136,16 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "webpki" name = "webpki"
version = "0.21.4" version = "0.21.4"

View file

@ -10,6 +10,7 @@ regex = "1.0"
tera = "1" tera = "1"
image = "0.23" image = "0.23"
rayon = "1" rayon = "1"
webp="0.1.1"
errors = { path = "../errors" } errors = { path = "../errors" }
utils = { path = "../utils" } utils = { path = "../utils" }

View file

@ -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::hash_map::Entry as HEntry;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::{self, File}; use std::fs::{self, File};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use image::imageops::FilterType; use image::{EncodableLayout, imageops::FilterType};
use image::{GenericImageView, ImageOutputFormat}; use image::{GenericImageView, ImageOutputFormat};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rayon::prelude::*; use rayon::prelude::*;
@ -18,7 +18,7 @@ static RESIZED_SUBDIR: &str = "processed_images";
lazy_static! { lazy_static! {
pub static ref RESIZED_FILENAME: Regex = 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 /// 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 /// Thumbnail image format
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -140,22 +141,26 @@ pub enum Format {
Jpeg(u8), Jpeg(u8),
/// PNG /// PNG
Png, Png,
/// WebP, The `u8` argument is WebP quality (in percent), None meaning lossless.
WebP(Option<u8>),
} }
impl Format { impl Format {
pub fn from_args(source: &str, format: &str, quality: u8) -> Result<Format> { pub fn from_args(source: &str, format: &str, quality: Option<u8>) -> Result<Format> {
use Format::*; use Format::*;
if let Some(quality) = quality {
assert!(quality > 0 && quality <= 100, "Jpeg quality must be within the range [1; 100]"); assert!(quality > 0 && quality <= 100, "Quality must be within the range [1; 100]");
}
let jpg_quality = quality.unwrap_or(DEFAULT_Q_JPG);
match format { match format {
"auto" => match Self::is_lossy(source) { "auto" => match Self::is_lossy(source) {
Some(true) => Ok(Jpeg(quality)), Some(true) => Ok(Jpeg(jpg_quality)),
Some(false) => Ok(Png), Some(false) => Ok(Png),
None => Err(format!("Unsupported image file: {}", source).into()), None => Err(format!("Unsupported image file: {}", source).into()),
}, },
"jpeg" | "jpg" => Ok(Jpeg(quality)), "jpeg" | "jpg" => Ok(Jpeg(jpg_quality)),
"png" => Ok(Png), "png" => Ok(Png),
"webp" => Ok(WebP(quality)),
_ => Err(format!("Invalid image format: {}", format).into()), _ => Err(format!("Invalid image format: {}", format).into()),
} }
} }
@ -170,6 +175,8 @@ impl Format {
"png" => Some(false), "png" => Some(false),
"gif" => Some(false), "gif" => Some(false),
"bmp" => Some(false), "bmp" => Some(false),
// It is assumed that webp is lossless, but it can be both
"webp" => Some(false),
_ => None, _ => None,
}) })
.unwrap_or(None) .unwrap_or(None)
@ -182,6 +189,7 @@ impl Format {
match *self { match *self {
Png => "png", Png => "png",
Jpeg(_) => "jpg", Jpeg(_) => "jpg",
WebP(_) => "webp"
} }
} }
} }
@ -194,6 +202,8 @@ impl Hash for Format {
let q = match *self { let q = match *self {
Png => 0, Png => 0,
Jpeg(q) => q, Jpeg(q) => q,
WebP(None) => 0,
WebP(Some(q)) => q
}; };
hasher.write_u8(q); hasher.write_u8(q);
@ -232,7 +242,7 @@ impl ImageOp {
width: Option<u32>, width: Option<u32>,
height: Option<u32>, height: Option<u32>,
format: &str, format: &str,
quality: u8, quality: Option<u8>,
) -> Result<ImageOp> { ) -> Result<ImageOp> {
let op = ResizeOp::from_args(op, width, height)?; let op = ResizeOp::from_args(op, width, height)?;
let format = Format::from_args(&source, format, quality)?; let format = Format::from_args(&source, format, quality)?;
@ -303,6 +313,19 @@ impl ImageOp {
Format::Jpeg(q) => { Format::Jpeg(q) => {
img.write_to(&mut f, ImageOutputFormat::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(()) Ok(())

View file

@ -221,7 +221,6 @@ impl ResizeImage {
static DEFAULT_OP: &str = "fill"; static DEFAULT_OP: &str = "fill";
static DEFAULT_FMT: &str = "auto"; static DEFAULT_FMT: &str = "auto";
const DEFAULT_Q: u8 = 75;
impl TeraFn for ResizeImage { impl TeraFn for ResizeImage {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> { fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
@ -248,11 +247,12 @@ impl TeraFn for ResizeImage {
.unwrap_or_else(|| DEFAULT_FMT.to_string()); .unwrap_or_else(|| DEFAULT_FMT.to_string());
let quality = let quality =
optional_arg!(u8, args.get("quality"), "`resize_image`: `quality` must be a number") optional_arg!(u8, args.get("quality"), "`resize_image`: `quality` must be a number");
.unwrap_or(DEFAULT_Q); if let Some(quality) = quality {
if quality == 0 || quality > 100 { if quality == 0 || quality > 100 {
return Err("`resize_image`: `quality` must be in range 1-100".to_string().into()); return Err("`resize_image`: `quality` must be in range 1-100".to_string().into());
} }
}
let mut imageproc = self.imageproc.lock().unwrap(); let mut imageproc = self.imageproc.lock().unwrap();
if !imageproc.source_exists(&path) { if !imageproc.source_exists(&path) {

View file

@ -28,10 +28,11 @@ resize_image(path, width, height, op, format, quality)
- `"auto"` - `"auto"`
- `"jpg"` - `"jpg"`
- `"png"` - `"png"`
- `"webp"`
The default is `"auto"`, this means that the format is chosen based on input image format. 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. 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 ### Image processing and return value