Thumb orientation + library filter on /photos/exif
Two follow-ups on the same feature branch: 1. Bake EXIF orientation into generated thumbnails. The `image` crate doesn't apply Orientation on load, and `save_with_format(..Jpeg)` drops EXIF — so portrait phone shots ended up sideways in any client that displays the cached thumb directly (no EXIF tag for the browser to compensate from). New `exif::read_orientation` reads the tag cheaply (no full EXIF parse) and `exif::apply_orientation` does the rotate/flip via image's existing `rotate90/180/270` + `fliph/flipv`. Applied in both branches of `generate_image_thumbnail` (RAW embedded- JPEG path and the regular `image::open` path). Existing thumbnails in the cache are still wrong-orientation; wipe the thumb dir or run a one-off backfill once this lands. 2. Optional `library` query param on `/photos/exif`. Accepts numeric id or name (same shape as `/image?library=...`), resolved via the existing `resolve_library_param` helper so a bad value 400s before we touch the DAO. Filter is applied post-query in the handler rather than pushed into `query_by_exif` to keep the DAO trait (and its test mocks) unchanged. Cheap enough at typical library counts; can be moved into SQL later if it ever isn't. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
32
src/exif.rs
32
src/exif.rs
@@ -4,6 +4,7 @@ use std::path::Path;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use exif::{In, Reader, Tag, Value};
|
||||
use image::DynamicImage;
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -177,6 +178,37 @@ pub fn extract_exif_from_path(path: &Path) -> Result<ExifData> {
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Read just the EXIF Orientation tag (1..=8) from a file. Cheaper than a
|
||||
/// full `extract_exif_from_path` when the caller only needs orientation —
|
||||
/// e.g. the thumbnail pipeline, which has to bake the rotation into the
|
||||
/// resized pixels because the saved thumb has no EXIF chunk for the browser
|
||||
/// to apply.
|
||||
pub fn read_orientation(path: &Path) -> Option<i32> {
|
||||
let file = File::open(path).ok()?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let exif = Reader::new().read_from_container(&mut reader).ok()?;
|
||||
let field = exif.get_field(Tag::Orientation, In::PRIMARY)?;
|
||||
get_u32_value(field).map(|v| v as i32)
|
||||
}
|
||||
|
||||
/// Apply an EXIF Orientation (1..=8) to a `DynamicImage`, returning a
|
||||
/// canonically-oriented copy. Orientations:
|
||||
/// 1 → as-is, 2 → flipH, 3 → rot180, 4 → flipV,
|
||||
/// 5 → rot90CW + flipH, 6 → rot90CW, 7 → rot270CW + flipH, 8 → rot270CW.
|
||||
/// Anything else (missing tag, garbage values) is returned unchanged.
|
||||
pub fn apply_orientation(img: DynamicImage, orientation: i32) -> DynamicImage {
|
||||
match orientation {
|
||||
2 => img.fliph(),
|
||||
3 => img.rotate180(),
|
||||
4 => img.flipv(),
|
||||
5 => img.rotate90().fliph(),
|
||||
6 => img.rotate90(),
|
||||
7 => img.rotate270().fliph(),
|
||||
8 => img.rotate270(),
|
||||
_ => img,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_string_value(field: &exif::Field) -> Option<String> {
|
||||
match &field.value {
|
||||
Value::Ascii(vec) => {
|
||||
|
||||
Reference in New Issue
Block a user