Update src/main.rs

This commit is contained in:
gigirassy
2026-05-28 18:16:25 +02:00
parent ffab06bf5f
commit 757427ebbc
+71 -72
View File
@@ -1,4 +1,3 @@
use std::{io::Cursor, net::SocketAddr, sync::Arc, time::Duration}; use std::{io::Cursor, net::SocketAddr, sync::Arc, time::Duration};
use anyhow::Result; use anyhow::Result;
@@ -9,9 +8,11 @@ use axum::{
routing::get, routing::get,
Router, Router,
}; };
use bytes::Bytes;
use image::{ use image::{
codecs::jpeg::JpegEncoder, codecs::{gif::GifDecoder, jpeg::JpegEncoder},
imageops::FilterType, imageops::FilterType,
AnimationDecoder,
GenericImageView, GenericImageView,
}; };
use mimalloc::MiMalloc; use mimalloc::MiMalloc;
@@ -69,7 +70,6 @@ async fn main() -> Result<()> {
info!("image backend listening on {}", addr); info!("image backend listening on {}", addr);
let listener = TcpListener::bind(addr).await?; let listener = TcpListener::bind(addr).await?;
axum::serve(listener, app).await?; axum::serve(listener, app).await?;
Ok(()) Ok(())
@@ -87,12 +87,7 @@ async fn fetch(
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
error!("fetch error: {:?}", e); error!("fetch error: {:?}", e);
(StatusCode::BAD_GATEWAY, "failed to process image").into_response()
(
StatusCode::BAD_GATEWAY,
"failed to process image",
)
.into_response()
} }
} }
} }
@@ -105,12 +100,7 @@ async fn thumb(
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
error!("thumb error: {:?}", e); error!("thumb error: {:?}", e);
(StatusCode::BAD_GATEWAY, "failed to process thumbnail").into_response()
(
StatusCode::BAD_GATEWAY,
"failed to process thumbnail",
)
.into_response()
} }
} }
} }
@@ -123,7 +113,6 @@ fn validate_url(url: &str) -> Result<()> {
} }
let host = parsed.host_str().unwrap_or(""); let host = parsed.host_str().unwrap_or("");
if !host.eq_ignore_ascii_case("i.pinimg.com") { if !host.eq_ignore_ascii_case("i.pinimg.com") {
anyhow::bail!("invalid host"); anyhow::bail!("invalid host");
} }
@@ -131,7 +120,7 @@ fn validate_url(url: &str) -> Result<()> {
Ok(()) Ok(())
} }
async fn download_image(client: &Client, url: &str) -> Result<bytes::Bytes> { async fn download_image(client: &Client, url: &str) -> Result<Bytes> {
let resp = client.get(url).send().await?; let resp = client.get(url).send().await?;
if !resp.status().is_success() { if !resp.status().is_success() {
@@ -141,72 +130,99 @@ async fn download_image(client: &Client, url: &str) -> Result<bytes::Bytes> {
Ok(resp.bytes().await?) Ok(resp.bytes().await?)
} }
async fn process_fetch( fn is_gif(data: &[u8]) -> bool {
state: Arc<AppState>, data.starts_with(b"GIF87a") || data.starts_with(b"GIF89a")
query: ImageQuery,
) -> Result<Response> {
validate_url(&query.url)?;
let data = download_image(&state.client, &query.url).await?;
let img = image::load_from_memory(&data)?;
let rgb = img.to_rgb8();
let mut out = Vec::with_capacity(data.len() / 2);
{
let mut cursor = Cursor::new(&mut out);
let mut encoder =
JpegEncoder::new_with_quality(&mut cursor, 75);
encoder.encode_image(&rgb)?;
} }
fn gif_passthrough(data: Bytes) -> Response {
let mut headers = HeaderMap::new();
headers.insert(
"content-type",
HeaderValue::from_static("image/gif"),
);
headers.insert(
"cache-control",
HeaderValue::from_static("public, max-age=86400, immutable"),
);
(headers, data).into_response()
}
fn jpeg_response(data: Vec<u8>) -> Response {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert( headers.insert(
"content-type", "content-type",
HeaderValue::from_static("image/jpeg"), HeaderValue::from_static("image/jpeg"),
); );
headers.insert( headers.insert(
"cache-control", "cache-control",
HeaderValue::from_static( HeaderValue::from_static("public, max-age=86400, immutable"),
"public, max-age=86400, immutable",
),
); );
Ok((headers, out).into_response()) (headers, data).into_response()
} }
async fn process_thumb( fn decode_first_gif_frame(data: &[u8]) -> Result<image::DynamicImage> {
state: Arc<AppState>, let decoder = GifDecoder::new(Cursor::new(data))?;
query: ThumbQuery, let frames = decoder.into_frames().collect_frames()?;
) -> Result<Response> {
let frame = frames
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("empty gif"))?;
Ok(image::DynamicImage::ImageRgba8(frame.into_buffer()))
}
async fn process_fetch(state: Arc<AppState>, query: ImageQuery) -> Result<Response> {
validate_url(&query.url)?;
let data = download_image(&state.client, &query.url).await?;
if is_gif(&data) {
return Ok(gif_passthrough(data));
}
let img = image::load_from_memory(&data)?;
let rgb = img.to_rgb8();
let mut out = Vec::with_capacity(data.len() / 2);
{
let mut cursor = Cursor::new(&mut out);
let mut encoder = JpegEncoder::new_with_quality(&mut cursor, 75);
encoder.encode_image(&rgb)?;
}
Ok(jpeg_response(out))
}
async fn process_thumb(state: Arc<AppState>, query: ThumbQuery) -> Result<Response> {
validate_url(&query.url)?; validate_url(&query.url)?;
let width = query.w.unwrap_or(260).clamp(64, 2048); let width = query.w.unwrap_or(260).clamp(64, 2048);
let data = download_image(&state.client, &query.url).await?; let data = download_image(&state.client, &query.url).await?;
let img = image::load_from_memory(&data)?; let img = if is_gif(&data) {
decode_first_gif_frame(&data)?
} else {
image::load_from_memory(&data)?
};
let (src_w, src_h) = img.dimensions(); let (src_w, src_h) = img.dimensions();
if width >= src_w { if width >= src_w {
return process_fetch( return process_fetch(
state, state,
ImageQuery { ImageQuery { url: query.url },
url: query.url,
},
) )
.await; .await;
} }
let height = let height = ((src_h as f32 * width as f32) / src_w as f32) as u32;
((src_h as f32 * width as f32) / src_w as f32) as u32;
let resized = img.resize_exact( let resized = img.resize_exact(
width, width,
@@ -220,26 +236,9 @@ async fn process_thumb(
{ {
let mut cursor = Cursor::new(&mut out); let mut cursor = Cursor::new(&mut out);
let mut encoder = JpegEncoder::new_with_quality(&mut cursor, 72);
let mut encoder =
JpegEncoder::new_with_quality(&mut cursor, 72);
encoder.encode_image(&rgb)?; encoder.encode_image(&rgb)?;
} }
let mut headers = HeaderMap::new(); Ok(jpeg_response(out))
headers.insert(
"content-type",
HeaderValue::from_static("image/jpeg"),
);
headers.insert(
"cache-control",
HeaderValue::from_static(
"public, max-age=86400, immutable",
),
);
Ok((headers, out).into_response())
} }