Add circular thumbnail creation for Map view

This commit is contained in:
Cameron
2026-01-26 20:04:14 -05:00
parent 073b5ed418
commit 1d2f4e3441
3 changed files with 89 additions and 1 deletions

View File

@@ -186,6 +186,8 @@ pub struct ThumbnailRequest {
#[serde(default)]
#[allow(dead_code)] // Part of API contract, may be used in future
pub(crate) format: Option<ThumbnailFormat>,
#[serde(default)]
pub(crate) shape: Option<ThumbnailShape>,
}
#[derive(Debug, Deserialize, PartialEq)]
@@ -196,6 +198,14 @@ pub enum ThumbnailFormat {
Image,
}
#[derive(Debug, Deserialize, PartialEq)]
pub enum ThumbnailShape {
#[serde(rename = "circle")]
Circle,
#[serde(rename = "square")]
Square,
}
#[derive(Deserialize)]
pub struct LoginRequest {
pub username: String,

View File

@@ -957,7 +957,6 @@ pub async fn get_gps_summary(
_: Claims,
request: HttpRequest,
req: web::Query<FilesRequest>,
state: web::Data<AppState>,
exif_dao: web::Data<Mutex<Box<dyn ExifDao>>>,
) -> Result<HttpResponse, actix_web::Error> {
use crate::data::{GpsPhotoSummary, GpsPhotosResponse};

View File

@@ -116,6 +116,26 @@ async fn get_image(
thumb_path.set_extension("gif");
}
// Handle circular thumbnail request
if req.shape == Some(ThumbnailShape::Circle) {
match create_circular_thumbnail(&thumb_path, &thumbs).await {
Ok(circular_path) => {
if let Ok(file) = NamedFile::open(&circular_path) {
span.set_status(Status::Ok);
return file
.use_etag(true)
.use_last_modified(true)
.prefer_utf8(true)
.into_response(&request);
}
}
Err(e) => {
warn!("Failed to create circular thumbnail: {:?}", e);
// Fall through to serve square thumbnail
}
}
}
trace!("Thumbnail path: {:?}", thumb_path);
if let Ok(file) = NamedFile::open(&thumb_path) {
span.set_status(Status::Ok);
@@ -153,6 +173,65 @@ fn is_video_file(path: &Path) -> bool {
file_types::is_video_file(path)
}
async fn create_circular_thumbnail(thumb_path: &Path, thumbs_dir: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
use image::{ImageBuffer, Rgba, GenericImageView};
// Create circular thumbnails directory
let circular_dir = Path::new(thumbs_dir).join("_circular");
// Get relative path from thumbs_dir to create same structure
let relative_to_thumbs = thumb_path.strip_prefix(thumbs_dir)?;
let circular_path = circular_dir.join(relative_to_thumbs).with_extension("png");
// Check if circular thumbnail already exists
if circular_path.exists() {
return Ok(circular_path);
}
// Create parent directory if needed
if let Some(parent) = circular_path.parent() {
std::fs::create_dir_all(parent)?;
}
// Load the square thumbnail
let img = image::open(thumb_path)?;
let (width, height) = img.dimensions();
// Fixed output size for consistency
let output_size = 80u32;
let radius = output_size as f32 / 2.0;
// Calculate crop area to get square center of original image
let crop_size = width.min(height);
let crop_x = (width - crop_size) / 2;
let crop_y = (height - crop_size) / 2;
// Create a new RGBA image with transparency
let output = ImageBuffer::from_fn(output_size, output_size, |x, y| {
let dx = x as f32 - radius;
let dy = y as f32 - radius;
let distance = (dx * dx + dy * dy).sqrt();
if distance <= radius {
// Inside circle - map to cropped source area
// Scale from output coordinates to crop coordinates
let scale = crop_size as f32 / output_size as f32;
let src_x = crop_x + (x as f32 * scale) as u32;
let src_y = crop_y + (y as f32 * scale) as u32;
let pixel = img.get_pixel(src_x, src_y);
Rgba([pixel[0], pixel[1], pixel[2], 255])
} else {
// Outside circle - transparent
Rgba([0, 0, 0, 0])
}
});
// Save as PNG (supports transparency)
output.save(&circular_path)?;
Ok(circular_path)
}
#[get("/image/metadata")]
async fn get_file_metadata(
_: Claims,