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 authored and cameron committed 2026-10-17 04:24:02 +00:00
1 parent bc27aea81a
commit b95adb4f77
5 files changed
+29 -6

No files matched your search

+22
View File
@@ -115,6 +115,16 @@ pub fn direntry_is_media(entry: &DirEntry) -> bool {
is_media_file(entry.path())
}
/// Decode an image by its bytes, not its extension. `image::open` picks
/// the decoder from the extension, so a PNG saved as `.jpg` (Android
/// screenshots, chat exports) fails with "Illegal start bytes: 8950".
/// Sniffing falls back to the extension when the header is unknown.
pub fn open_image(path: &Path) -> image::ImageResult<image::DynamicImage> {
image::ImageReader::open(path)?
.with_guessed_format()?
.decode()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -190,4 +200,16 @@ mod tests {
assert!(is_image_file(Path::new(".cover.jpg")));
assert!(!is_filesystem_metadata(Path::new(".cover.jpg")));
}
#[test]
fn open_image_sniffs_format_over_extension() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("Screenshot_2014-11-17-21-30-31.jpg");
image::RgbImage::new(3, 2)
.save_with_format(&path, image::ImageFormat::Png)
.unwrap();
assert!(image::open(&path).is_err(), "extension-based decode fails");
let img = open_image(&path).unwrap();
assert_eq!((img.width(), img.height()), (3, 2));
}
}