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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "governor"
|
name = "governor"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
@ -1281,6 +1287,16 @@ version = "0.2.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
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]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
|
@ -2880,6 +2896,16 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "weezl"
|
name = "weezl"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
@ -3174,6 +3200,7 @@ dependencies = [
|
||||||
"toml",
|
"toml",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
"webp",
|
||||||
"worker",
|
"worker",
|
||||||
"worker-macros",
|
"worker-macros",
|
||||||
]
|
]
|
||||||
|
|
|
@ -35,6 +35,7 @@ env-local = ["axum/http1", "axum/http2",
|
||||||
"governor",
|
"governor",
|
||||||
"clap", "toml",
|
"clap", "toml",
|
||||||
"image/ico",
|
"image/ico",
|
||||||
|
"lossy-webp",
|
||||||
"svg-text", "resvg/system-fonts", "resvg/raster-images", "fontdb/fontconfig"
|
"svg-text", "resvg/system-fonts", "resvg/raster-images", "fontdb/fontconfig"
|
||||||
]
|
]
|
||||||
reuse-port = []
|
reuse-port = []
|
||||||
|
@ -48,6 +49,7 @@ tokio = ["dep:tokio", "axum/tokio"]
|
||||||
env_logger = ["dep:env_logger"]
|
env_logger = ["dep:env_logger"]
|
||||||
governor = ["dep:governor"]
|
governor = ["dep:governor"]
|
||||||
axum-server = ["dep:axum-server"]
|
axum-server = ["dep:axum-server"]
|
||||||
|
lossy-webp = ["dep:webp"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
worker = { version="0.4.2", features=['http', 'axum'], optional = true }
|
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 }
|
libc = { version = "0.2.162", optional = true }
|
||||||
axum-server = { version = "0.7.1", optional = true }
|
axum-server = { version = "0.7.1", optional = true }
|
||||||
fontdb = { version = "0.23", optional = true }
|
fontdb = { version = "0.23", optional = true }
|
||||||
|
webp = { version = "0.3.0", optional = true }
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# licensing and webp dependencies
|
# licensing and webp dependencies
|
||||||
|
|
|
@ -16,6 +16,7 @@ Work in progress! Currently to do:
|
||||||
- [X] Rate-limiting on local deployment (untested)
|
- [X] Rate-limiting on local deployment (untested)
|
||||||
- [X] Read config from Cloudflare
|
- [X] Read config from Cloudflare
|
||||||
- [X] Timing and Rate-limiting headers (some not available on Cloudflare Workers)
|
- [X] Timing and Rate-limiting headers (some not available on Cloudflare Workers)
|
||||||
|
- [ ] Lossy WebP on CF Workers
|
||||||
- [ ] Cache Results on Cloudflare KV.
|
- [ ] Cache Results on Cloudflare KV.
|
||||||
- [ ] Handle all possible panics reported by Clippy
|
- [ ] Handle all possible panics reported by Clippy
|
||||||
- [X] Sandboxing the image rendering
|
- [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,
|
clock::SystemClock, middleware::StateInformationMiddleware, state::keyed::DashMapStateStore,
|
||||||
RateLimiter,
|
RateLimiter,
|
||||||
};
|
};
|
||||||
use post_process::MediaResponse;
|
use post_process::{CompressionLevel, MediaResponse};
|
||||||
use sandbox::Sandboxing;
|
use sandbox::Sandboxing;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -337,6 +337,31 @@ pub struct ImageOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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
|
/// Whether resizing is requested
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn requested_resize(&self) -> bool {
|
pub fn requested_resize(&self) -> bool {
|
||||||
|
|
|
@ -14,7 +14,10 @@ use axum::{
|
||||||
};
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use image::{
|
use image::{
|
||||||
codecs::gif::{GifEncoder, Repeat},
|
codecs::{
|
||||||
|
gif::{GifEncoder, Repeat},
|
||||||
|
jpeg::JpegEncoder,
|
||||||
|
},
|
||||||
Frames, ImageFormat,
|
Frames, ImageFormat,
|
||||||
};
|
};
|
||||||
use sniff::SniffingStream;
|
use sniff::SniffingStream;
|
||||||
|
@ -180,6 +183,7 @@ where
|
||||||
Ok(MediaResponse::ProcessedStaticImage(StaticImage {
|
Ok(MediaResponse::ProcessedStaticImage(StaticImage {
|
||||||
data: img,
|
data: img,
|
||||||
format: ImageFormat::WebP,
|
format: ImageFormat::WebP,
|
||||||
|
compression: options.compression_level(),
|
||||||
is_https,
|
is_https,
|
||||||
})
|
})
|
||||||
.with_timing_info(TIME_TO_FIRST_BYTE_KEY, ttfb)
|
.with_timing_info(TIME_TO_FIRST_BYTE_KEY, ttfb)
|
||||||
|
@ -285,6 +289,7 @@ where
|
||||||
Ok(MediaResponse::ProcessedStaticImage(StaticImage {
|
Ok(MediaResponse::ProcessedStaticImage(StaticImage {
|
||||||
data: img,
|
data: img,
|
||||||
format: output_static_format,
|
format: output_static_format,
|
||||||
|
compression: options.compression_level(),
|
||||||
is_https,
|
is_https,
|
||||||
})
|
})
|
||||||
.with_timing_info(TIME_TO_FIRST_BYTE_KEY, ttfb)
|
.with_timing_info(TIME_TO_FIRST_BYTE_KEY, ttfb)
|
||||||
|
@ -312,6 +317,7 @@ where
|
||||||
Ok(MediaResponse::ProcessedStaticImage(StaticImage {
|
Ok(MediaResponse::ProcessedStaticImage(StaticImage {
|
||||||
data: img,
|
data: img,
|
||||||
format: output_static_format,
|
format: output_static_format,
|
||||||
|
compression: options.compression_level(),
|
||||||
is_https,
|
is_https,
|
||||||
})
|
})
|
||||||
.with_timing_info(TIME_TO_FIRST_BYTE_KEY, ttfb)
|
.with_timing_info(TIME_TO_FIRST_BYTE_KEY, ttfb)
|
||||||
|
@ -337,6 +343,7 @@ where
|
||||||
Ok(MediaResponse::ProcessedStaticImage(StaticImage {
|
Ok(MediaResponse::ProcessedStaticImage(StaticImage {
|
||||||
data: result,
|
data: result,
|
||||||
format: output_static_format,
|
format: output_static_format,
|
||||||
|
compression: options.compression_level(),
|
||||||
is_https,
|
is_https,
|
||||||
})
|
})
|
||||||
.with_timing_info(TIME_TO_FIRST_BYTE_KEY, ttfb)
|
.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
|
/// Processed static image
|
||||||
pub struct StaticImage {
|
pub struct StaticImage {
|
||||||
data: image::DynamicImage,
|
data: image::DynamicImage,
|
||||||
format: ImageFormat,
|
format: ImageFormat,
|
||||||
|
|
||||||
|
compression: CompressionLevel,
|
||||||
|
|
||||||
is_https: bool,
|
is_https: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -541,7 +565,48 @@ impl IntoResponse for StaticImage {
|
||||||
let encoding_begin = crate::timing::Instant::now();
|
let encoding_begin = crate::timing::Instant::now();
|
||||||
|
|
||||||
let mut buf = BufWriter::new(Cursor::new(Vec::new()));
|
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 =
|
let mut resp =
|
||||||
axum::http::Response::new(Body::from(buf.into_inner().unwrap().into_inner()));
|
axum::http::Response::new(Body::from(buf.into_inner().unwrap().into_inner()));
|
||||||
|
|
Loading…
Reference in a new issue