diff --git a/crates/engine/src/files.rs b/crates/engine/src/files.rs index dc79b8b31..3256ee61c 100644 --- a/crates/engine/src/files.rs +++ b/crates/engine/src/files.rs @@ -2296,4 +2296,112 @@ mod tests { assert_eq!(s.quick_view_job(id, 1600, true).unwrap().run().quick, Some(QuickSource::Small)); let _ = std::fs::remove_dir_all(&dir); } + + /// A Sony ARW of `model`: a 64 × 48 uncompressed 14-bit CFA strip in a SubIFD (as `crates/raw/tests/vendor_robust.rs` + /// lays it out) whose IFD carries the full-frame 16–35 mm distortion table (tag 0x7037). + fn sony_arw(model: &str) -> Vec { + use lightcraft_tiff::tags::{self as t, photometric}; + use lightcraft_tiff::{ByteOrder, IfdBuilder, ImageData, TiffWriter, Value}; + // 3:2, the aspect Sony's lens opcodes are limited to + let (w, h) = (72u32, 48u32); + let mut raw = IfdBuilder::new(); + raw.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0])); + raw.set(t::IMAGE_WIDTH, Value::Long(vec![w])); + raw.set(t::IMAGE_LENGTH, Value::Long(vec![h])); + raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![14])); + raw.set(t::COMPRESSION, Value::Short(vec![1])); + raw.set(t::PHOTOMETRIC, Value::Short(vec![photometric::CFA])); + raw.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![2, 2])); + raw.set(t::CFA_PATTERN_EP, Value::Byte(vec![0, 1, 1, 2])); + raw.set(0x7037, Value::SShort(vec![16, 0, 2, 7, 16, 28, 43, 61, 83, 109, 139, 175, 217, 267, 325, 395, 479])); + raw.set_image(ImageData::Strips { rows_per_strip: h, strips: vec![2000u16.to_le_bytes().repeat((w * h) as usize)] }); + let mut ifd0 = IfdBuilder::new(); + ifd0.set(t::MAKE, Value::Ascii("SONY".into())); + ifd0.set(t::MODEL, Value::Ascii(model.into())); + ifd0.add_sub_ifd(raw); + TiffWriter::new(ByteOrder::Little, false).write(&[ifd0]).unwrap() + } + + /// Sony's embedded distortion correction is on by default: an ARW of a verified body imports with "Enable Lens + /// Corrections" on and still counts as unedited; another body's tables aren't read, so its correction is off. + #[test] + fn arw_with_lens_data_imports_with_profile_corrections_on() { + let dir = std::env::temp_dir().join(format!("lc-arw-lens-default-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let mut s = crate::Session::new().with_fs(); + for (model, on) in [("ILCE-7RM6", true), ("ILCE-7M4", false)] { + let path = dir.join(format!("{model}.arw")); + std::fs::write(&path, sony_arw(model)).unwrap(); + let r = s.execute("library.import", &serde_json::json!({"paths": [path.to_string_lossy()]})).unwrap(); + let p = s.catalog.photo(lightcraft_catalog::PhotoId(r["imported"][0].as_u64().unwrap())).unwrap(); + assert_eq!((p.format.as_str(), p.embedded_lens.is_some(), p.develop.optics.lens_profile), ("ARW", on, on), "{model}"); + assert!(!p.is_edited(), "{model}: as imported"); + } + let _ = std::fs::remove_dir_all(&dir); + } + + /// RW2 and DNG keep their default: the correction is on when the file carries one (RW2 distortion, a DNG's + /// opcodes), off when it doesn't. + #[test] + fn rw2_and_dng_defaults_unchanged() { + use lightcraft_catalog::{Photo, PhotoId, Source}; + let s = crate::Session::new(); + let lens = lightcraft_develop::EmbeddedLens { warp: Some(Default::default()), ..Default::default() }; + for format in ["RW2", "DNG"] { + for embedded in [None, Some(lens)] { + let name = format!("x.{format}"); + let mut p = Photo::new(PhotoId(1), Source::File { path: format!("/{name}") }, &name, format, 64, 48, ""); + p.kind = MediaKind::Raw; + p.embedded_lens = embedded; + crate::import::apply_import_defaults(&s, &mut p); + assert_eq!(p.develop.optics.lens_profile, embedded.is_some(), "{format} {embedded:?}"); + } + } + } + + /// Stored settings stay as they are when the library opens again: an ARW catalogued before its lens data was read + /// (none, correction off) keeps the correction off and counts as unedited, and one whose user switched the + /// correction off keeps it off. + #[test] + fn existing_photo_settings_are_not_rewritten() { + use lightcraft_catalog::{Op, PhotoId}; + let dir = std::env::temp_dir().join(format!("lc-arw-lens-existing-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let (lib, files) = (dir.join("lib"), dir.join("files")); + std::fs::create_dir_all(&files).unwrap(); + let mut s = crate::Session::new().with_fs(); + s.open_library(&lib, false).unwrap(); + let import = |s: &mut crate::Session, name: &str| { + let path = files.join(name); + // trailing bytes of its own: not a duplicate of the other file + std::fs::write(&path, [sony_arw("ILCE-7RM6"), name.as_bytes().to_vec()].concat()).unwrap(); + let r = s.execute("library.import", &serde_json::json!({"paths": [path.to_string_lossy()]})).unwrap(); + PhotoId(r["imported"][0].as_u64().unwrap()) + }; + let old = import(&mut s, "old.arw"); + // as a release that didn't read the Sony tables catalogued it + let mut d = (*s.catalog.photo(old).unwrap().develop).clone(); + d.optics.lens_profile = false; + let ops = vec![ + Op::SetEmbeddedLens { id: old, lens: None }, + Op::SetDevelop { id: old, settings: std::sync::Arc::new(d), label: "x".into(), edited: None }, + ]; + s.commit("x", Op::Batch { ops }).unwrap(); + let off = import(&mut s, "off.arw"); + s.execute("develop.merge", &serde_json::json!({"settings": {"optics": {"lens_profile": false}}})).unwrap(); + let before: Vec<_> = [old, off].iter().map(|id| s.catalog.photo(*id).unwrap().clone()).collect(); + assert!(before.iter().all(|p| !p.develop.optics.lens_profile)); + s.persist().unwrap(); + drop(s); + let mut s = crate::Session::new().with_fs(); + s.open_library(&lib, false).unwrap(); + for b in &before { + let p = s.catalog.photo(b.id).unwrap(); + assert_eq!((&p.develop, &p.embedded_lens), (&b.develop, &b.embedded_lens), "{}", p.file_name); + } + assert!(!s.catalog.photo(old).unwrap().is_edited(), "still as imported"); + drop(s); + let _ = std::fs::remove_dir_all(&dir); + } } diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index 25c411ce0..ea37b0403 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -945,7 +945,7 @@ "No edits yet.": "Noch keine Bearbeitungen.", "No exposure information": "Keine Belichtungsinformationen", "No filter presets yet": "Noch keine Filtervorgaben", - "No lens data embedded in this file (DNG lens opcodes only).": "Keine Objektivdaten in dieser Datei eingebettet (nur DNG-Objektiv-Opcodes).", + "No lens data embedded in this file.": "Keine Objektivdaten in dieser Datei eingebettet.", "No masks yet. Choose a mask type above.": "Noch keine Masken. Wähle oben einen Maskentyp.", "No matching commands": "Keine passenden Befehle", "No matching photos": "Keine passenden Fotos", diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index ba3e1e331..0df2ebecb 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -920,7 +920,7 @@ "No edits yet.": "Aún no hay ediciones.", "No exposure information": "Sin información de exposición", "No filter presets yet": "Aún no hay ajustes preestablecidos de filtro", - "No lens data embedded in this file (DNG lens opcodes only).": "Este archivo no incluye datos de objetivo (solo opcodes de objetivo DNG).", + "No lens data embedded in this file.": "Este archivo no incluye datos de objetivo.", "No masks yet. Choose a mask type above.": "Aún no hay máscaras. Elegir un tipo de máscara arriba.", "No matching commands": "Ningún comando coincide", "No matching photos": "No hay fotos coincidentes", diff --git a/crates/ui-egui/locales/fr.json b/crates/ui-egui/locales/fr.json index b0becb154..2035f615c 100644 --- a/crates/ui-egui/locales/fr.json +++ b/crates/ui-egui/locales/fr.json @@ -897,7 +897,7 @@ "No edits yet.": "Aucune retouche pour l'instant.", "No exposure information": "Aucune information d'exposition", "No filter presets yet": "Aucun préréglage de filtre pour l'instant", - "No lens data embedded in this file (DNG lens opcodes only).": "Aucune donnée d'objectif intégrée à ce fichier (opcodes d'objectif DNG uniquement).", + "No lens data embedded in this file.": "Aucune donnée d'objectif intégrée à ce fichier.", "No masks yet. Choose a mask type above.": "Aucun masque pour l'instant. Choisissez un type de masque ci-dessus.", "No matching commands": "Aucune commande correspondante", "No matching photos": "Aucune photo correspondante", diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 7a282e676..41f5d9004 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -949,7 +949,7 @@ "No edits yet.": "まだ編集していません", "No exposure information": "露出情報がありません", "No filter presets yet": "フィルタープリセットはまだありません", - "No lens data embedded in this file (DNG lens opcodes only).": "このファイルにレンズ情報は含まれていません(DNGのレンズ情報のみ対応)", + "No lens data embedded in this file.": "このファイルにレンズ情報は含まれていません", "No masks yet. Choose a mask type above.": "マスクはありません。上から種類を選択してください", "No matching photos": "一致する写真がありません", "No metadata found": "メタデータがありません", diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index 2e21c727c..ea90aee8f 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -909,7 +909,7 @@ "No edits yet.": "Ainda sem edições.", "No exposure information": "Sem informações de exposição", "No filter presets yet": "Ainda não há predefinições de filtro", - "No lens data embedded in this file (DNG lens opcodes only).": "Nenhum dado de lente incorporado neste arquivo (somente opcodes de lente DNG).", + "No lens data embedded in this file.": "Nenhum dado de lente incorporado neste arquivo.", "No masks yet. Choose a mask type above.": "Ainda não há máscaras. Escolha um tipo de máscara acima.", "No matching commands": "Nenhum comando corresponde", "No matching photos": "Nenhuma foto correspondente", diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index c840218a0..8107b2d0a 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -752,7 +752,7 @@ "No edits yet.": "Правок пока нет.", "No exposure information": "Нет данных об экспозиции", "No filter presets yet": "Пресетов фильтров пока нет", - "No lens data embedded in this file (DNG lens opcodes only).": "В этом файле нет данных об объективе (только опкоды объектива DNG).", + "No lens data embedded in this file.": "В этом файле нет данных об объективе.", "No masks yet. Choose a mask type above.": "Масок пока нет. Выберите тип маски выше.", "No matching commands": "Подходящих команд нет", "No matching photos": "Нет подходящих фото", diff --git a/crates/ui-egui/locales/uk.json b/crates/ui-egui/locales/uk.json index 85a838098..6fa4c2e46 100644 --- a/crates/ui-egui/locales/uk.json +++ b/crates/ui-egui/locales/uk.json @@ -1086,7 +1086,7 @@ "No edits yet.": "Змін поки немає.", "No exposure information": "Немає інформації про експозицію", "No filter presets yet": "Наборів налаштувань фільтрів поки немає", - "No lens data embedded in this file (DNG lens opcodes only).": "У цьому файлі немає даних об’єктива (лише інструкції об’єктива DNG).", + "No lens data embedded in this file.": "У цьому файлі немає даних об’єктива.", "No masks yet. Choose a mask type above.": "Масок поки немає. Виберіть тип маски вище.", "No matching photos": "Немає відповідних фото", "No metadata found": "Метаданих не знайдено", diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index ad379f1ef..8c68cb7e3 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -971,7 +971,7 @@ "No edits yet.": "尚未编辑。", "No exposure information": "无曝光信息", "No filter presets yet": "暂无筛选器预设", - "No lens data embedded in this file (DNG lens opcodes only).": "此文件中未嵌入镜头数据 (仅支持 DNG 镜头操作码)。", + "No lens data embedded in this file.": "此文件中未嵌入镜头数据。", "No masks yet. Choose a mask type above.": "尚无蒙版。请在上方选择蒙版类型。", "No matching photos": "没有匹配的照片", "No metadata found": "未找到元数据", diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 647d153d0..6f3cf4389 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -971,7 +971,7 @@ "No edits yet.": "尚未編輯。", "No exposure information": "沒有曝光資訊", "No filter presets yet": "尚無篩選預設集", - "No lens data embedded in this file (DNG lens opcodes only).": "此檔案未內嵌鏡頭資料(僅支援 DNG 鏡頭操作碼)。", + "No lens data embedded in this file.": "此檔案未內嵌鏡頭資料。", "No masks yet. Choose a mask type above.": "尚無遮色片。請在上方選擇遮色片類型。", "No matching photos": "沒有符合的照片", "No metadata found": "找不到中繼資料", diff --git a/crates/ui-egui/src/panels/edit.rs b/crates/ui-egui/src/panels/edit.rs index 76a54171f..bce9b1c2d 100644 --- a/crates/ui-egui/src/panels/edit.rs +++ b/crates/ui-egui/src/panels/edit.rs @@ -367,9 +367,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { } if lp && !has_lens { ui.label( - egui::RichText::new(crate::i18n::tr("No lens data embedded in this file (DNG lens opcodes only).")) - .size(11.0) - .color(Tokens::get(ui.ctx()).text_dim), + egui::RichText::new(crate::i18n::tr("No lens data embedded in this file.")).size(11.0).color(Tokens::get(ui.ctx()).text_dim), ); } });