diff --git a/apps/lightcraft-web/src/web.rs b/apps/lightcraft-web/src/web.rs index 14a5d050b..2ca6d7105 100644 --- a/apps/lightcraft-web/src/web.rs +++ b/apps/lightcraft-web/src/web.rs @@ -23,7 +23,11 @@ use crate::store::{Originals, content_hash, download_name, storage_key}; use crate::wire::ThumbIndex; use crate::workers::{THUMB_INDEX, Workers}; -const ACCEPT: &str = ".jpg,.jpeg,.png,.tif,.tiff,.webp,.dng,.cr2,.nef,.arw,.psd,.jxl,.gif,.bmp"; +/// The web picker's accept list, derived from the engine's import extensions (both +/// cases) so no format the engine can import is silently filtered out of the dialog. +fn accept_list() -> String { + lightcraft_engine::import::accept_list() +} /// After a failed save, wait this long before trying again (ms). const SAVE_RETRY_MS: f64 = 2000.0; @@ -123,7 +127,7 @@ fn open_picker(originals: Originals, backend: Option, ctx: egui::Contex let Ok(input) = doc.create_element("input").map(|e| e.unchecked_into::()) else { return }; input.set_type("file"); input.set_multiple(true); - input.set_accept(ACCEPT); + input.set_accept(&accept_list()); let inp = input.clone(); let on_change = Closure::::new(move || { if let Some(files) = inp.files() { diff --git a/crates/engine/src/import.rs b/crates/engine/src/import.rs index bc8c1c962..3134d4e98 100644 --- a/crates/engine/src/import.rs +++ b/crates/engine/src/import.rs @@ -374,6 +374,26 @@ pub fn is_supported(path: &Path) -> bool { path.extension().is_some_and(|e| EXTENSIONS.contains(&e.to_string_lossy().to_lowercase().as_str())) } +/// The web file picker's accept list, derived from [`EXTENSIONS`]. Each extension +/// is listed in both cases: browsers' `accept` matching is case-insensitive per +/// spec, but some implementations (e.g. Windows/Edge) match case-sensitively and +/// rejected uppercase `.NEF` files, so a lowercase-only list silently hid raws +/// such as CR3, NRW, RAF, ORF, RW2, RWL, RAW, PEF and SRW from the picker. +pub fn accept_list() -> String { + let mut out = String::new(); + for &ext in EXTENSIONS { + if !out.is_empty() { + out.push(','); + } + out.push('.'); + out.push_str(ext); + out.push(','); + out.push('.'); + out.push_str(&ext.to_uppercase()); + } + out +} + /// Expand files and folders (recursively) into supported files. `skip` (e.g. the library folder) /// is never descended into. Each folder's walk is bounded ([`crate::walk::Limits::default`]); a /// walk that stops at a bound is logged. @@ -1607,6 +1627,15 @@ mod tests_known; mod prepared_tests { use super::*; + #[test] + fn accept_list_covers_every_extension_in_both_cases() { + let list = accept_list(); + for &ext in EXTENSIONS { + assert!(list.contains(&format!(".{ext}")), "lowercase .{ext} missing from accept list"); + assert!(list.contains(&format!(".{}", ext.to_uppercase())), "uppercase .{} missing from accept list", ext.to_uppercase()); + } + } + #[test] fn revalidate_add_only_converts_matching_ready_item() { let mut s = Session::new();