diff --git a/src-tauri/src/formats.rs b/src-tauri/src/formats.rs index 7c73b9fdb8..fa1a0b5342 100644 --- a/src-tauri/src/formats.rs +++ b/src-tauri/src/formats.rs @@ -89,6 +89,16 @@ pub fn is_raw_file>(path: P) -> bool { .any(|(raw_ext, _)| raw_ext.eq_ignore_ascii_case(ext)) } +/// Returns true when the bytes start with the signature of a standard (non-RAW) image +/// format. Used to catch files whose RAW extension does not match their contents, e.g. +/// JPEGs that cloud photo exports save under the original `.ARW`/`.RW2` filename. +pub fn has_non_raw_image_signature(bytes: &[u8]) -> bool { + const JPEG: &[u8] = &[0xFF, 0xD8, 0xFF]; + const PNG: &[u8] = &[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]; + + bytes.starts_with(JPEG) || bytes.starts_with(PNG) +} + pub fn is_supported_image_file>(path: P) -> bool { let path = path.as_ref(); @@ -116,3 +126,24 @@ pub fn is_supported_image_file>(path: P) -> bool { .iter() .any(|non_raw_ext| non_raw_ext.eq_ignore_ascii_case(ext)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn detects_standard_image_signatures() { + assert!(has_non_raw_image_signature(&[0xFF, 0xD8, 0xFF, 0xE0, 0x00])); + assert!(has_non_raw_image_signature(&[ + 0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A + ])); + } + + #[test] + fn ignores_raw_signatures() { + // Little-endian TIFF header used by ARW, NEF, DNG, ... + assert!(!has_non_raw_image_signature(&[0x49, 0x49, 0x2A, 0x00])); + assert!(!has_non_raw_image_signature(b"FUJIFILMCCD-RAW")); + assert!(!has_non_raw_image_signature(&[])); + } +} diff --git a/src-tauri/src/image_loader.rs b/src-tauri/src/image_loader.rs index 955a341ca1..5f277fe577 100644 --- a/src-tauri/src/image_loader.rs +++ b/src-tauri/src/image_loader.rs @@ -3,7 +3,7 @@ use crate::app_settings::{AppSettings, load_settings}; use crate::app_state::{AppState, LoadedImage}; use crate::exif_processing; use crate::file_management::{parse_virtual_path, read_file_mapped}; -use crate::formats::is_raw_file; +use crate::formats::{has_non_raw_image_signature, is_raw_file}; use crate::image_processing::ImageMetadata; use crate::image_processing::{ apply_orientation, apply_srgb_to_linear, remove_raw_artifacts_and_enhance, @@ -104,10 +104,16 @@ pub fn load_base_image_from_bytes( bytes, ); - if is_raw_file(path_for_ext_check) - && !use_fast_raw_dev - && settings.use_apple_raw9.unwrap_or(false) - { + let mut decode_as_raw = is_raw_file(path_for_ext_check); + if decode_as_raw && has_non_raw_image_signature(bytes) { + log::info!( + "'{}' has a RAW extension but contains a standard image, decoding it as such", + path_for_ext_check + ); + decode_as_raw = false; + } + + if decode_as_raw && !use_fast_raw_dev && settings.use_apple_raw9.unwrap_or(false) { if let Some((tracker, generation)) = &cancel_token && tracker.load(Ordering::SeqCst) != *generation { @@ -128,7 +134,7 @@ pub fn load_base_image_from_bytes( } } - if is_raw_file(path_for_ext_check) { + if decode_as_raw { match panic::catch_unwind(move || { crate::raw_processing::develop_raw_image( bytes,