mirror of
https://codeberg.org/gigirassy/image-proxy/
synced 2026-08-30 23:47:40 +00:00
Update src/main.rs
This commit is contained in:
+71
-72
@@ -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())
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user