Lossy webp encoding

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
ゆめ 2024-11-15 01:05:02 -06:00
parent 961b30ae4e
commit 7577cf3acd
No known key found for this signature in database
5 changed files with 124 additions and 3 deletions

27
Cargo.lock generated
View file

@ -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",
]

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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()));