WebP support in resize_image (#1360)
* Removing unused webpl * Adding clarification comment * Updating documentation * Adding webp
This commit is contained in:
parent
d4db249997
commit
8eac5a5994
36
Cargo.lock
generated
36
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue