Lossy webp encoding
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
parent
961b30ae4e
commit
7577cf3acd
5 changed files with 124 additions and 3 deletions
27
Cargo.lock
generated
27
Cargo.lock
generated
|
@ -810,6 +810,12 @@ version = "0.31.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.7.0"
|
||||
|
@ -1281,6 +1287,16 @@ version = "0.2.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libwebp-sys"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54cd30df7c7165ce74a456e4ca9732c603e8dc5e60784558c1c6dc047f876733"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"glob",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
|
@ -2880,6 +2896,16 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webp"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f53152f51fb5af0c08484c33d16cca96175881d1f3dec068c23b31a158c2d99"
|
||||
dependencies = [
|
||||
"image",
|
||||
"libwebp-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.8"
|
||||
|
@ -3174,6 +3200,7 @@ dependencies = [
|
|||
"toml",
|
||||
"tower-service",
|
||||
"wasm-bindgen",
|
||||
"webp",
|
||||
"worker",
|
||||
"worker-macros",
|
||||
]
|
||||
|
|
|
@ -35,6 +35,7 @@ env-local = ["axum/http1", "axum/http2",
|
|||
"governor",
|
||||
"clap", "toml",
|
||||
"image/ico",
|
||||
"lossy-webp",
|
||||
"svg-text", "resvg/system-fonts", "resvg/raster-images", "fontdb/fontconfig"
|
||||
]
|
||||
reuse-port = []
|
||||
|
@ -48,6 +49,7 @@ tokio = ["dep:tokio", "axum/tokio"]
|
|||
env_logger = ["dep:env_logger"]
|
||||
governor = ["dep:governor"]
|
||||
axum-server = ["dep:axum-server"]
|
||||
lossy-webp = ["dep:webp"]
|
||||
|
||||
[dependencies]
|
||||
worker = { version="0.4.2", features=['http', 'axum'], optional = true }
|
||||
|
@ -74,6 +76,7 @@ wasm-bindgen = { version = "0.2" }
|
|||
libc = { version = "0.2.162", optional = true }
|
||||
axum-server = { version = "0.7.1", optional = true }
|
||||
fontdb = { version = "0.23", optional = true }
|
||||
webp = { version = "0.3.0", optional = true }
|
||||
|
||||
[patch.crates-io]
|
||||
# licensing and webp dependencies
|
||||
|
|
|
@ -16,6 +16,7 @@ Work in progress! Currently to do:
|
|||
- [X] Rate-limiting on local deployment (untested)
|
||||
- [X] Read config from Cloudflare
|
||||
- [X] Timing and Rate-limiting headers (some not available on Cloudflare Workers)
|
||||
- [ ] Lossy WebP on CF Workers
|
||||
- [ ] Cache Results on Cloudflare KV.
|
||||
- [ ] Handle all possible panics reported by Clippy
|
||||
- [X] Sandboxing the image rendering
|
||||
|
|
27
src/lib.rs
27
src/lib.rs
|
@ -24,7 +24,7 @@ use governor::{
|
|||
clock::SystemClock, middleware::StateInformationMiddleware, state::keyed::DashMapStateStore,
|
||||
RateLimiter,
|
||||
};
|
||||
use post_process::MediaResponse;
|
||||
use post_process::{CompressionLevel, MediaResponse};
|
||||
use sandbox::Sandboxing;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
@ -337,6 +337,31 @@ pub struct ImageOptions {
|
|||
}
|
||||
|
||||
impl ImageOptions {
|
||||
/// Convert options to compression level
|
||||
#[must_use]
|
||||
pub fn compression_level(&self) -> CompressionLevel {
|
||||
if self.badge == Some(true) {
|
||||
return CompressionLevel::Med;
|
||||
}
|
||||
|
||||
if self.avatar == Some(true) {
|
||||
return CompressionLevel::High;
|
||||
}
|
||||
|
||||
if self.emoji == Some(true) {
|
||||
return CompressionLevel::Max;
|
||||
}
|
||||
|
||||
if self.preview == Some(true) {
|
||||
return CompressionLevel::Med;
|
||||
}
|
||||
|
||||
if self.static_ == Some(true) {
|
||||
return CompressionLevel::Low;
|
||||
}
|
||||
|
||||
CompressionLevel::None
|
||||
}
|
||||
/// Whether resizing is requested
|
||||
#[must_use]
|
||||
pub fn requested_resize(&self) -> bool {
|
||||
|
|
|
@ -14,7 +14,10 @@ use axum::{
|
|||
};
|
||||
use futures::StreamExt;
|
||||
use image::{
|
||||
codecs::gif::{GifEncoder, Repeat},
|
||||
codecs::{
|
||||
gif::{GifEncoder, Repeat},
|
||||
jpeg::JpegEncoder,
|
||||
},
|
||||
Frames, ImageFormat,
|
||||
};
|
||||
use sniff::SniffingStream;
|
||||
|
@ -180,6 +183,7 @@ where
|
|||
Ok(MediaResponse::ProcessedStaticImage(StaticImage {
|
||||
data: img,
|
||||
format: ImageFormat::WebP,
|
||||
compression: options.compression_level(),
|
||||
is_https,
|
||||
})
|
||||
.with_timing_info(TIME_TO_FIRST_BYTE_KEY, ttfb)
|
||||
|
@ -285,6 +289,7 @@ where
|
|||
Ok(MediaResponse::ProcessedStaticImage(StaticImage {
|
||||
data: img,
|
||||
format: output_static_format,
|
||||
compression: options.compression_level(),
|
||||
is_https,
|
||||
})
|
||||
.with_timing_info(TIME_TO_FIRST_BYTE_KEY, ttfb)
|
||||
|
@ -312,6 +317,7 @@ where
|
|||
Ok(MediaResponse::ProcessedStaticImage(StaticImage {
|
||||
data: img,
|
||||
format: output_static_format,
|
||||
compression: options.compression_level(),
|
||||
is_https,
|
||||
})
|
||||
.with_timing_info(TIME_TO_FIRST_BYTE_KEY, ttfb)
|
||||
|
@ -337,6 +343,7 @@ where
|
|||
Ok(MediaResponse::ProcessedStaticImage(StaticImage {
|
||||
data: result,
|
||||
format: output_static_format,
|
||||
compression: options.compression_level(),
|
||||
is_https,
|
||||
})
|
||||
.with_timing_info(TIME_TO_FIRST_BYTE_KEY, ttfb)
|
||||
|
@ -527,11 +534,28 @@ impl<R: HTTPResponse> IntoResponse for PassThru<R> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Compression level for static images of possible
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CompressionLevel {
|
||||
/// No compression
|
||||
None,
|
||||
/// Low compression
|
||||
Low,
|
||||
/// Medium compression
|
||||
Med,
|
||||
/// High compression
|
||||
High,
|
||||
/// Maximum compression
|
||||
Max,
|
||||
}
|
||||
|
||||
/// Processed static image
|
||||
pub struct StaticImage {
|
||||
data: image::DynamicImage,
|
||||
format: ImageFormat,
|
||||
|
||||
compression: CompressionLevel,
|
||||
|
||||
is_https: bool,
|
||||
}
|
||||
|
||||
|
@ -541,7 +565,48 @@ impl IntoResponse for StaticImage {
|
|||
let encoding_begin = crate::timing::Instant::now();
|
||||
|
||||
let mut buf = BufWriter::new(Cursor::new(Vec::new()));
|
||||
self.data.write_to(&mut buf, self.format).unwrap();
|
||||
|
||||
if self.compression == CompressionLevel::None {
|
||||
self.data.write_to(&mut buf, self.format).unwrap();
|
||||
} else {
|
||||
match self.format {
|
||||
#[cfg(feature = "lossy-webp")]
|
||||
ImageFormat::WebP => {
|
||||
let enc = match webp::Encoder::from_image(&self.data) {
|
||||
Ok(enc) => enc,
|
||||
Err(e) => {
|
||||
return ErrorResponse::postprocess_failed(e.to_string().into())
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let webp = match self.compression {
|
||||
CompressionLevel::Low => enc.encode(80.),
|
||||
CompressionLevel::Med => enc.encode(60.),
|
||||
CompressionLevel::High => enc.encode(40.),
|
||||
CompressionLevel::Max => enc.encode(25.),
|
||||
_ => enc.encode(100.0),
|
||||
};
|
||||
|
||||
buf.write_all(&webp).unwrap();
|
||||
}
|
||||
ImageFormat::Jpeg => {
|
||||
let mut enc = JpegEncoder::new_with_quality(
|
||||
&mut buf,
|
||||
match self.compression {
|
||||
CompressionLevel::Low => 90,
|
||||
CompressionLevel::Med => 70,
|
||||
CompressionLevel::High => 50,
|
||||
CompressionLevel::Max => 33,
|
||||
_ => 100,
|
||||
},
|
||||
);
|
||||
|
||||
enc.encode_image(&self.data).unwrap();
|
||||
}
|
||||
_ => self.data.write_to(&mut buf, self.format).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
let mut resp =
|
||||
axum::http::Response::new(Body::from(buf.into_inner().unwrap().into_inner()));
|
||||
|
|
Loading…
Reference in a new issue