LR-IMP-FORMATS: Reload from Disk in the Photo and context menus; fills missing camera metadata
photo.reload had no menu path, so the documented way to refresh CR3s imported before their metadata was read (Photo > Reload from Disk) did not exist in the app. It is now in the Photo menu and the photo context menu, and besides size/preview-only it fills camera fields the catalog lacks (camera, lens, exposure, GPS, capture time) from a fresh probe, never overwriting a set value.
This commit is contained in:
parent
13a3eff2e8
commit
9adb40908a
|
|
@ -183,6 +183,33 @@ pub(crate) fn content_op(id: lightcraft_catalog::PhotoId, ph: &lightcraft_catalo
|
|||
})
|
||||
}
|
||||
|
||||
/// Ops that fill a photo's empty camera fields (camera, lens, exposure, GPS, capture time) from a fresh probe.
|
||||
fn fill_missing_meta(id: lightcraft_catalog::PhotoId, ph: &lightcraft_catalog::Photo, info: &crate::media::ProbeInfo) -> Vec<Op> {
|
||||
let (mut m, src) = (ph.meta.clone(), &info.meta);
|
||||
let before = m.clone();
|
||||
if m.camera.is_empty() {
|
||||
m.camera = src.camera.clone();
|
||||
}
|
||||
if m.lens.is_empty() {
|
||||
m.lens = src.lens.clone();
|
||||
}
|
||||
if m.shutter.is_empty() {
|
||||
m.shutter = src.shutter.clone();
|
||||
}
|
||||
m.focal_mm = m.focal_mm.or(src.focal_mm);
|
||||
m.aperture = m.aperture.or(src.aperture);
|
||||
m.iso = m.iso.or(src.iso);
|
||||
m.gps = m.gps.or(src.gps);
|
||||
let mut ops = Vec::new();
|
||||
if m != before {
|
||||
ops.push(Op::SetMeta { id, meta: Box::new(m) });
|
||||
}
|
||||
if ph.captured.is_none() && info.captured.is_some() {
|
||||
ops.push(Op::SetCaptured { id, captured: info.captured.clone() });
|
||||
}
|
||||
ops
|
||||
}
|
||||
|
||||
/// Re-read photos whose files changed on disk (an external editor saved them): new size,
|
||||
/// dimensions and content hash, cached sources dropped. → {reloaded: [ids]}
|
||||
fn reload(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
|
@ -199,8 +226,18 @@ fn reload(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let mut reloaded = Vec::new();
|
||||
for ((id, _), info) in paths.into_iter().zip(probed) {
|
||||
let (Ok(info), Some(ph)) = (info, s.catalog.photo(id)) else { continue };
|
||||
let Some(op) = content_op(id, ph, info.clone()) else { continue };
|
||||
// camera fields the catalog lacks (e.g. a raw imported before its format was read) are filled in;
|
||||
// nothing already set is overwritten
|
||||
let meta_ops = fill_missing_meta(id, ph, &info);
|
||||
let Some(op) = content_op(id, ph, info.clone()) else {
|
||||
if !meta_ops.is_empty() {
|
||||
ops.extend(meta_ops);
|
||||
reloaded.push(id);
|
||||
}
|
||||
continue;
|
||||
};
|
||||
ops.push(op);
|
||||
ops.extend(meta_ops);
|
||||
// virtual copies share the file
|
||||
for c in s.catalog.photos().filter(|c| c.copy_of == Some(id)) {
|
||||
ops.extend(content_op(c.id, c, info.clone()));
|
||||
|
|
@ -277,9 +314,9 @@ pub fn edit_specs() -> Vec<CommandSpec> {
|
|||
cmd!(
|
||||
"photo.reload",
|
||||
"Reload from Disk",
|
||||
[],
|
||||
["Photo"],
|
||||
None,
|
||||
"{ids?} — re-read photos whose files changed on disk (e.g. saved by an external editor), or raws shown from their embedded preview that can be decoded now → {reloaded}",
|
||||
"{ids?} — re-read photos whose files changed on disk (e.g. saved by an external editor), or raws shown from their embedded preview that can be decoded now; camera fields the catalog lacks are filled in → {reloaded}",
|
||||
has_selection,
|
||||
reload
|
||||
),
|
||||
|
|
@ -294,3 +331,32 @@ pub fn edit_specs() -> Vec<CommandSpec> {
|
|||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use lightcraft_catalog::{Photo, PhotoId, Source};
|
||||
|
||||
/// Reload fills camera fields a photo lacks (a CR3 imported before CR3 metadata was read) and keeps
|
||||
/// everything already set.
|
||||
#[test]
|
||||
fn reload_fills_only_missing_camera_fields() {
|
||||
let mut ph = Photo::new(PhotoId(1), Source::File { path: "/x.cr3".into() }, "x.cr3", "CR3", 1620, 1080, "2026-10-06T00:00:00");
|
||||
ph.meta.title = "Mine".into();
|
||||
ph.meta.iso = Some(800);
|
||||
let mut info = crate::media::ProbeInfo { captured: Some("2026-10-04T08:16:11".into()), ..Default::default() };
|
||||
info.meta.camera = "Canon EOS R6 Mark III".into();
|
||||
info.meta.lens = "RF24-105mm".into();
|
||||
info.meta.iso = Some(400);
|
||||
info.meta.aperture = Some(6.3);
|
||||
let ops = fill_missing_meta(PhotoId(1), &ph, &info);
|
||||
let Some(Op::SetMeta { meta, .. }) = ops.first() else { panic!("{ops:?}") };
|
||||
assert_eq!((meta.camera.as_str(), meta.lens.as_str(), meta.aperture), ("Canon EOS R6 Mark III", "RF24-105mm", Some(6.3)));
|
||||
assert_eq!((meta.title.as_str(), meta.iso), ("Mine", Some(800)), "set fields are kept");
|
||||
assert!(matches!(ops.get(1), Some(Op::SetCaptured { captured: Some(_), .. })));
|
||||
// nothing missing → nothing to do
|
||||
ph.meta = (**meta).clone();
|
||||
ph.captured = Some("2026-10-04T08:16:11".into());
|
||||
assert!(fill_missing_meta(PhotoId(1), &ph, &info).is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1017,5 +1017,6 @@
|
|||
"Quit": "終了",
|
||||
"Temporary session — your library isn't open, and nothing you do here is saved.": "一時セッション:ライブラリが開かれていないため、ここでの操作は保存されません。",
|
||||
"Back Up Library…": "ライブラリをバックアップ…",
|
||||
"Restore Library from Backup…": "バックアップからライブラリを復元…"
|
||||
"Restore Library from Backup…": "バックアップからライブラリを復元…",
|
||||
"Reload from Disk": "ディスクから再読み込み"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ const LAYOUT: &[(&str, &[&str])] = &[
|
|||
"---",
|
||||
"photo.saveMetadataToFile",
|
||||
"photo.readMetadataFromFile",
|
||||
"photo.reload",
|
||||
"app.showInFinder",
|
||||
"dialog.rename",
|
||||
"dialog.captureTime",
|
||||
|
|
|
|||
|
|
@ -821,6 +821,9 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
|||
if ui.button(crate::i18n::tr("Rename…")).clicked() {
|
||||
let _ = app.run("dialog.rename", json!({}));
|
||||
}
|
||||
if ui.button(crate::i18n::tr("Reload from Disk")).clicked() {
|
||||
let _ = app.run("photo.reload", json!({}));
|
||||
}
|
||||
if ui.button(crate::i18n::tr("Create Virtual Copy")).clicked() {
|
||||
let _ = app.run("photo.virtualCopy", json!({}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,8 +85,9 @@
|
|||
### Library
|
||||
- Canon CR3 files show their full-size embedded JPEG (e.g. 6960 × 4640 on an EOS R6 Mark III) instead of the
|
||||
1620 × 1080 preview, and import with their metadata: capture time, camera, lens, exposure, GPS and XMP. Their raw
|
||||
data is not decoded yet, so they stay preview-only. For CR3s imported earlier, Photo ▸ Reload from Disk picks
|
||||
up the full-size preview; their camera metadata comes with a fresh import.
|
||||
data is not decoded yet, so they stay preview-only. For CR3s imported earlier, Photo ▸ Reload from Disk (now in
|
||||
the Photo menu and the photo context menu) picks up the full-size preview and fills in the camera metadata they
|
||||
were missing, without touching anything already set.
|
||||
- Rename Photos never overwrites another photo when only the letter case changes (issue #95): on case-sensitive
|
||||
volumes (Linux, case-sensitive APFS) `img_1.JPG` next to `IMG_1.JPG` is a different photo and the renamed one gets
|
||||
`img_1-1.JPG`; on case-insensitive volumes the case change still goes through.
|
||||
|
|
|
|||
Loading…
Reference in New Issue