LR-EDIT-OPTICS-PROFILE: Sony embedded corrections on by default for ARW

An ILCE-7RM6 ARW with Sony's distortion table now imports with Enable
Lens Corrections on: Photo::camera_defaults already turns the correction
on for any file whose probe yields embedded lens data, and the ARW
decoder emits it since the previous commit. No new default rule is
needed, so RW2 and DNG keep theirs (on when the file carries a
correction) and stored settings are never rewritten.

Tests pin it: a synthetic ARW imports with the correction on (another
body's tables are not read: off), RW2 / DNG defaults are unchanged, and
an ARW catalogued without lens data or switched off by the user keeps
its settings across a library reopen. The Optics panel's hint drops
"(DNG lens opcodes only)" in every locale.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
dev-null-undefinedandClaude committed 2026-10-13 13:09:40 +02:00
1 parent b677fbac0a
commit d982a073c9
11 files changed
+118 -12

No files matched your search

+108
View File
@@ -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<u8> {
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);
}
}
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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": "メタデータがありません",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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": "Нет подходящих фото",
+1 -1
View File
@@ -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": "Метаданих не знайдено",
+1 -1
View File
@@ -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": "未找到元数据",
+1 -1
View File
@@ -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": "找不到中繼資料",
+1 -3
View File
@@ -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),
);
}
});