fix(images): pick the decoder from the bytes, not the extension

`image::open` chooses the decoder by extension, so a PNG saved as .jpg
(Android screenshots, chat exports) failed thumbnails, previews,
perceptual hashes, insights and face crops with "Illegal start bytes:
8950". New `file_types::open_image` sniffs the header and falls back to
the extension when it's unknown; every decode site uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-10-17 00:20:45 -04:00
1 parent a605ccd6f8
commit 9222d3e3ae
5 files changed
+29 -6

No files matched your search

+3 -2
View File
@@ -2179,7 +2179,7 @@ async fn person_faces_handler<D: FaceDao>(
// ── Helpers ─────────────────────────────────────────────────────────────────
/// Crop `abs_path` to the normalized bbox and re-encode as JPEG for the
/// face service. `image::open` decodes most photo formats Apollo will see;
/// face service. `open_image` decodes most photo formats Apollo will see;
/// HEIC/RAW are out of scope for the manual flow (the user can't draw a
/// face on a thumbnail of a non-decodable file anyway).
fn crop_image_to_bbox(
@@ -2195,7 +2195,8 @@ fn crop_image_to_bbox(
if nw <= 0.0 || nh <= 0.0 || nx + nw > 1.001 || ny + nh > 1.001 {
return Err(anyhow!("bbox wh out of bounds or zero"));
}
let raw = image::open(abs_path).with_context(|| format!("open {:?}", abs_path))?;
let raw =
crate::file_types::open_image(abs_path).with_context(|| format!("open {:?}", abs_path))?;
// EXIF rotation: the bbox arrives in display space (the carousel /
// overlay are rendered post-rotation by the browser), but the
// `image` crate hands us raw pre-rotation pixels. For any phone