use crate::State; use actix_web::{http::StatusCode, web, HttpRequest, HttpResponse, ResponseError, Scope}; use awc::Client; use hyaenidae_profiles::pictrs::ImageInfo; use hyaenidae_toolkit::Image; use url::Url; pub(crate) use hyaenidae_profiles::pictrs::ImageType; pub(crate) struct IconImage { key: String, title: String, } pub(crate) struct ThumbnailImage { key: String, title: String, } pub(crate) struct FullImage { key: String, title: String, } pub(crate) struct BannerImage { key: String, title: String, } impl Image for IconImage { fn src(&self) -> String { largest_icon(&self.key, ImageType::Png) } fn title(&self) -> String { self.title.clone() } fn jpeg_srcset(&self) -> Option { None } fn png_srcset(&self) -> Option { Some(icon_srcset(&self.key, ImageType::Png)) } fn webp_srcset(&self) -> Option { Some(icon_srcset(&self.key, ImageType::Webp)) } } impl Image for ThumbnailImage { fn src(&self) -> String { largest_thumbnail(&self.key, ImageType::Png) } fn title(&self) -> String { self.title.clone() } fn jpeg_srcset(&self) -> Option { None } fn png_srcset(&self) -> Option { Some(thumbnail_srcset(&self.key, ImageType::Png)) } fn webp_srcset(&self) -> Option { Some(thumbnail_srcset(&self.key, ImageType::Webp)) } } impl Image for FullImage { fn src(&self) -> String { largest_image(&self.key, ImageType::Png) } fn title(&self) -> String { self.title.clone() } fn jpeg_srcset(&self) -> Option { None } fn png_srcset(&self) -> Option { Some(image_srcset(&self.key, ImageType::Png)) } fn webp_srcset(&self) -> Option { Some(image_srcset(&self.key, ImageType::Webp)) } } impl Image for BannerImage { fn src(&self) -> String { largest_banner(&self.key, ImageType::Jpeg) } fn title(&self) -> String { self.title.clone() } fn png_srcset(&self) -> Option { None } fn jpeg_srcset(&self) -> Option { Some(banner_srcset(&self.key, ImageType::Jpeg)) } fn webp_srcset(&self) -> Option { Some(banner_srcset(&self.key, ImageType::Webp)) } } impl IconImage { pub(crate) fn new(key: &str, title: &str) -> Self { IconImage { key: key.to_owned(), title: title.to_owned(), } } } impl ThumbnailImage { pub(crate) fn new(key: &str, title: &str) -> Self { ThumbnailImage { key: key.to_owned(), title: title.to_owned(), } } } impl FullImage { pub(crate) fn new(key: &str, title: &str) -> Self { FullImage { key: key.to_owned(), title: title.to_owned(), } } } impl BannerImage { pub(crate) fn new(key: &str, title: &str) -> Self { BannerImage { key: key.to_owned(), title: title.to_owned(), } } } #[derive(Clone)] pub(super) struct Images { base_url: Url, } impl Images { pub(super) fn new(base_url: Url) -> Self { Images { base_url } } } impl ImageInfo for Images { fn image_url(&self, key: &str) -> Url { let mut url = self.base_url.clone(); url.set_path(&format!("/image/full/{}", key)); url } } pub(super) fn scope() -> Scope { web::scope("/image") .service(web::resource("/full/{key}").route(web::get().to(serve))) .service(web::resource("/icon/{size}.{kind}").route(web::get().to(serve_icon))) .service(web::resource("/banner/{size}.{kind}").route(web::get().to(serve_banner))) .service(web::resource("/image/{size}.{kind}").route(web::get().to(serve_image))) .service(web::resource("/thumb/{size}.{kind}").route(web::get().to(serve_thumbnail))) } async fn serve( req: HttpRequest, key: web::Path, client: web::Data, state: web::Data, ) -> Result { state.profiles.pictrs.serve_full(&req, &key, &client).await } #[derive(Debug, thiserror::Error)] #[error("Invalid image size requested: {0}px")] struct SizeError(u64); impl ResponseError for SizeError { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } fn error_response(&self) -> HttpResponse { HttpResponse::BadRequest() .content_type("text/plain") .body(self.to_string()) } } pub(crate) fn image_srcset(key: &str, kind: ImageType) -> String { VALID_IMAGE_SIZES .iter() .map(|size| format!("{} {}w", image_path(key, *size, kind), size)) .collect::>() .join(", ") } pub(crate) fn icon_srcset(key: &str, kind: ImageType) -> String { VALID_ICON_SIZES .iter() .map(|size| format!("{} {}w", icon_path(key, *size, kind), size)) .collect::>() .join(", ") } pub(crate) fn thumbnail_srcset(key: &str, kind: ImageType) -> String { VALID_THUMBNAIL_SIZES .iter() .map(|size| format!("{} {}w", thumbnail_path(key, *size, kind), size)) .collect::>() .join(", ") } pub(crate) fn banner_srcset(key: &str, kind: ImageType) -> String { VALID_BANNER_SIZES .iter() .map(|size| format!("{} {}w", banner_path(key, *size, kind), size)) .collect::>() .join(", ") } pub(crate) fn largest_image(key: &str, kind: ImageType) -> String { image_path(key, 5040, kind) } pub(crate) fn largest_banner(key: &str, kind: ImageType) -> String { banner_path(key, 1680, kind) } pub(crate) fn largest_icon(key: &str, kind: ImageType) -> String { icon_path(key, 512, kind) } pub(crate) fn largest_thumbnail(key: &str, kind: ImageType) -> String { thumbnail_path(key, 512, kind) } fn icon_path(key: &str, size: u64, kind: ImageType) -> String { format!("/image/icon/{}.{}?src={}", size, kind, key) } fn thumbnail_path(key: &str, size: u64, kind: ImageType) -> String { format!("/image/thumb/{}.{}?src={}", size, kind, key) } fn banner_path(key: &str, size: u64, kind: ImageType) -> String { format!("/image/banner/{}.{}?src={}", size, kind, key) } fn image_path(key: &str, size: u64, kind: ImageType) -> String { format!("/image/image/{}.{}?src={}", size, kind, key) } #[derive(Clone, Debug, serde::Deserialize)] pub struct SrcQuery { src: String, } static VALID_IMAGE_SIZES: &[u64] = &[420, 840, 1260, 1680, 2520, 5040]; async fn serve_image( req: HttpRequest, path: web::Path<(u64, ImageType)>, src: web::Query, client: web::Data, state: web::Data, ) -> Result { let (size, kind) = path.into_inner(); if !VALID_IMAGE_SIZES.contains(&size) { return Err(SizeError(size).into()); } state .profiles .pictrs .serve_thumbnail(&req, &src.src, kind, size, &client) .await } static VALID_ICON_SIZES: &[u64] = &[64, 128, 256, 512]; async fn serve_icon( req: HttpRequest, path: web::Path<(u64, ImageType)>, src: web::Query, client: web::Data, state: web::Data, ) -> Result { let (size, kind) = path.into_inner(); if !VALID_ICON_SIZES.contains(&size) { return Err(SizeError(size).into()); } state .profiles .pictrs .serve_icon(&req, &src.src, kind, size, &client) .await } static VALID_THUMBNAIL_SIZES: &[u64] = &[64, 128, 256, 512]; async fn serve_thumbnail( req: HttpRequest, path: web::Path<(u64, ImageType)>, src: web::Query, client: web::Data, state: web::Data, ) -> Result { let (size, kind) = path.into_inner(); if !VALID_THUMBNAIL_SIZES.contains(&size) { return Err(SizeError(size).into()); } state .profiles .pictrs .serve_thumbnail(&req, &src.src, kind, size, &client) .await } static VALID_BANNER_SIZES: &[u64] = &[420, 840, 1260, 1680]; async fn serve_banner( req: HttpRequest, path: web::Path<(u64, ImageType)>, src: web::Query, client: web::Data, state: web::Data, ) -> Result { let (size, kind) = path.into_inner(); if !VALID_BANNER_SIZES.contains(&size) { return Err(SizeError(size).into()); } state .profiles .pictrs .serve_banner(&req, &src.src, kind, size, &client) .await }