Merge pull request #7 from stevedylandev/chore/cellar-add-image-compression
a8f2b996
cellar - add image compression
3 file(s) · +28 −5
cellar - add image compression
| 644 | 644 | ||
| 645 | 645 | [[package]] |
|
| 646 | 646 | name = "cellar" |
|
| 647 | - | version = "0.1.0" |
|
| 647 | + | version = "0.1.1" |
|
| 648 | 648 | dependencies = [ |
|
| 649 | 649 | "andromeda-auth", |
|
| 650 | 650 | "askama 0.15.6", |
|
| 652 | 652 | "axum", |
|
| 653 | 653 | "base64", |
|
| 654 | 654 | "dotenvy", |
|
| 655 | + | "image", |
|
| 655 | 656 | "nanoid", |
|
| 656 | 657 | "rand 0.8.5", |
|
| 657 | 658 | "reqwest 0.12.28", |
|
| 1 | 1 | [package] |
|
| 2 | 2 | name = "cellar" |
|
| 3 | - | version = "0.1.0" |
|
| 3 | + | version = "0.1.1" |
|
| 4 | 4 | edition = "2024" |
|
| 5 | 5 | description = "Personal wine tasting log" |
|
| 6 | 6 | license = "MIT" |
|
| 25 | 25 | askama_web = { version = "0.15", features = ["axum-0.8"] } |
|
| 26 | 26 | reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } |
|
| 27 | 27 | base64 = "0.22" |
|
| 28 | + | image = "0.25" |
|
| 1 | 1 | use askama::Template; |
|
| 2 | + | use image::ImageDecoder; |
|
| 2 | 3 | use askama_web::WebTemplate; |
|
| 3 | 4 | use axum::{ |
|
| 4 | 5 | extract::{DefaultBodyLimit, Multipart, Path, Query, State}, |
|
| 424 | 425 | } |
|
| 425 | 426 | } |
|
| 426 | 427 | ||
| 428 | + | // --- Image processing --- |
|
| 429 | + | ||
| 430 | + | fn process_image(data: &[u8]) -> Result<Vec<u8>, String> { |
|
| 431 | + | let reader = image::ImageReader::new(std::io::Cursor::new(data)) |
|
| 432 | + | .with_guessed_format() |
|
| 433 | + | .map_err(|e| format!("Failed to read image: {}", e))?; |
|
| 434 | + | let mut decoder = reader |
|
| 435 | + | .into_decoder() |
|
| 436 | + | .map_err(|e| format!("Failed to create decoder: {}", e))?; |
|
| 437 | + | let orientation = decoder.orientation().unwrap_or(image::metadata::Orientation::NoTransforms); |
|
| 438 | + | let mut img = image::DynamicImage::from_decoder(decoder) |
|
| 439 | + | .map_err(|e| format!("Failed to decode image: {}", e))?; |
|
| 440 | + | img.apply_orientation(orientation); |
|
| 441 | + | let mut output = Vec::new(); |
|
| 442 | + | let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut output, 75); |
|
| 443 | + | img.write_with_encoder(encoder) |
|
| 444 | + | .map_err(|e| format!("JPEG encoding failed: {}", e))?; |
|
| 445 | + | Ok(output) |
|
| 446 | + | } |
|
| 447 | + | ||
| 427 | 448 | // --- Multipart parsing --- |
|
| 428 | 449 | ||
| 429 | 450 | struct WineFormData { |
|
| 459 | 480 | let field_name = field.name().unwrap_or("").to_string(); |
|
| 460 | 481 | match field_name.as_str() { |
|
| 461 | 482 | "image" => { |
|
| 462 | - | let content_type = field.content_type().unwrap_or("application/octet-stream").to_string(); |
|
| 463 | 483 | let bytes = field.bytes().await.map_err(|e| format!("Failed to read image: {}", e))?; |
|
| 464 | 484 | if !bytes.is_empty() { |
|
| 465 | - | image = Some(bytes.to_vec()); |
|
| 466 | - | image_mime = Some(content_type); |
|
| 485 | + | let processed = process_image(&bytes)?; |
|
| 486 | + | image = Some(processed); |
|
| 487 | + | image_mime = Some("image/jpeg".to_string()); |
|
| 467 | 488 | } |
|
| 468 | 489 | } |
|
| 469 | 490 | "name" => name = field.text().await.unwrap_or_default(), |
|