Integrate batch 22: fix cross-PR breaks

- #540's crs::Target replaces the raw flag: #597/#604's folder sync and sidecar_op and #533's
  process tests pass the photo's target instead of a bool.
- #568's DRO test fits through #528's fit_look (proxies now return the clipped mask).
- #541 (French) and #564 (Ukrainian) gain the 15 keyword format strings #578-#592 added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-09 21:04:48 -04:00
parent 987a572512
commit b4ed033c2d
7 changed files with 50 additions and 18 deletions

View File

@ -1742,10 +1742,10 @@ mod tests {
let transform = lightcraft_raw::color::camera_transform(&raw, lightcraft_raw::color::as_shot_white_xy(&raw));
let look = fit_preview(&raw, &bytes, &transform).expect("a camera look");
// the look fitted to the JPEG, as before this change
let (sensor, reference) = proxies(&raw, &bytes, &transform, PROXY).unwrap();
let (sensor, reference, clipped) = proxies(&raw, &bytes, &transform, PROXY).unwrap();
let profile = raw.metadata.model.as_deref().and_then(crate::camera_profiles::get);
let colour = profile.as_ref().and_then(|p| Some((p.matrix().mul(&transform.matrix.inverse()?), p.hue_sat.clone())));
let fitted = fit_pairs_with(&sensor, &reference, colour).unwrap();
let fitted = fit_look(&sensor, &reference, &clipped, colour).unwrap();
if dro {
assert_lowered(&fitted, &look, name);
for x in lowered {

View File

@ -545,7 +545,7 @@ mod tests {
hierarchical_keywords: vec!["Places|Lisbon".into()],
..Default::default()
};
let sc = super::parse_sidecar(&lightcraft_meta::write_xmp(&m, None), false).unwrap();
let sc = super::parse_sidecar(&lightcraft_meta::write_xmp(&m, None), crate::crs::Target::Rendered).unwrap();
assert_eq!(sc.keywords, Some(vec!["Places|Lisbon".to_string()]));
}

View File

@ -427,7 +427,7 @@ fn sidecar_has_news(labels: &Catalog, p: &Photo, naming: SidecarNaming) -> bool
return false;
}
let Ok(text) = std::fs::read_to_string(&file) else { return false };
let Ok(sc) = crate::sidecar::parse_sidecar(&text, p.kind == lightcraft_catalog::MediaKind::Raw) else { return false };
let Ok(sc) = crate::sidecar::parse_sidecar(&text, crate::crs::Target::for_photo(p)) else { return false };
let sc = sc.resolve_label(labels);
let mut q = p.clone();
crate::sidecar::merge_into(&mut q, &sc, "");
@ -464,7 +464,7 @@ pub struct SyncWork {
moved: Vec<FolderMoved>,
missing: Vec<FolderPhoto>,
/// (photo, its file, how its sidecar is named)
sidecars: Vec<(PhotoId, String, SidecarNaming, bool)>,
sidecars: Vec<(PhotoId, String, SidecarNaming, crate::crs::Target)>,
}
/// The catalog half of a synchronize (see [`SyncJob`]).
@ -518,7 +518,7 @@ impl SyncJob {
.zip(namings)
.filter_map(|(m, naming)| {
let p = s.catalog.photo(PhotoId(m.id))?;
Some((p.id, m.path, naming, p.kind == lightcraft_catalog::MediaKind::Raw))
Some((p.id, m.path, naming, crate::crs::Target::for_photo(p)))
})
.collect()
} else {
@ -590,11 +590,11 @@ impl SyncWork {
return;
}
let mut read = Vec::with_capacity(sidecars.len());
for (id, path, naming, raw) in sidecars {
for (id, path, naming, target) in sidecars {
if cancel.load(Relaxed) {
break;
}
read.push((id, path.clone(), read_sidecar(&path, naming, raw)));
read.push((id, path.clone(), read_sidecar(&path, naming, target)));
step(1);
}
if !read.is_empty() {
@ -604,14 +604,14 @@ impl SyncWork {
}
/// Read and parse photo file `path`'s XMP sidecar file.
fn read_sidecar(path: &str, naming: SidecarNaming, raw: bool) -> std::result::Result<crate::sidecar::SidecarData, String> {
fn read_sidecar(path: &str, naming: SidecarNaming, target: crate::crs::Target) -> std::result::Result<crate::sidecar::SidecarData, String> {
let file = crate::sidecar::find_sidecar(path, naming).ok_or_else(|| "no XMP sidecar".to_string())?;
let len = std::fs::metadata(&file).map_err(|e| format!("{}: {e}", file.display()))?.len();
if len > MAX_SIDECAR_BYTES {
return Err(format!("{}: too large for an XMP sidecar", file.display()));
}
let text = std::fs::read_to_string(&file).map_err(|e| format!("{}: {e}", file.display()))?;
crate::sidecar::parse_sidecar(&text, raw).map_err(|e| format!("{}: {e}", file.display()))
crate::sidecar::parse_sidecar(&text, target).map_err(|e| format!("{}: {e}", file.display()))
}
impl SyncCommit {

View File

@ -196,19 +196,21 @@ fn sidecars_keep_the_process_and_interchange_edits_get_the_latest() {
// versions, so sidecars written then read as V1)
put(&mut s, id, NEWER, 0.3);
let p = s.catalog.photo(id).unwrap().clone();
let back = parse_sidecar(&sidecar_packet(&p, &s.catalog), false).unwrap();
let back = parse_sidecar(&sidecar_packet(&p, &s.catalog), crate::crs::Target::Rendered).unwrap();
let Some(DevelopPatch::Full(d)) = &back.develop else { panic!("no lc:settings") };
assert_eq!((d.process, d.light.exposure), (NEWER, 0.3));
put(&mut s, id, ProcessVersion::V1, 0.3);
let packet = sidecar_packet(s.catalog.photo(id).unwrap(), &s.catalog);
assert!(!packet.contains("process"), "{packet}");
let Some(DevelopPatch::Full(d)) = parse_sidecar(&packet, false).unwrap().develop else { panic!("no lc:settings") };
let Some(DevelopPatch::Full(d)) = parse_sidecar(&packet, crate::crs::Target::Rendered).unwrap().develop else { panic!("no lc:settings") };
assert_eq!(d.process, ProcessVersion::V1);
// a damaged process value reads as V1 and costs nothing else: the sidecar's edits still come back
for bad in ["-1", "1.5", "\"two\"", "null", "4294967296"] {
let json = format!(r#"{{"process": {bad}, "light": {{"exposure": 0.5}}}}"#);
let packet = lightcraft_meta::write_xmp(&lightcraft_meta::Metadata::default(), Some(&json));
let Some(DevelopPatch::Full(d)) = parse_sidecar(&packet, false).unwrap().develop else { panic!("{bad}: the edit was dropped") };
let Some(DevelopPatch::Full(d)) = parse_sidecar(&packet, crate::crs::Target::Rendered).unwrap().develop else {
panic!("{bad}: the edit was dropped")
};
assert_eq!((d.process, d.light.exposure), (ProcessVersion::V1, 0.5), "{bad}");
}
// `crs:` edits (other raw developers, Lightroom): their ProcessVersion numbers another
@ -217,7 +219,7 @@ fn sidecars_keep_the_process_and_interchange_edits_get_the_latest() {
let xmp = r#"<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about="" xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/"
crs:ProcessVersion="11.0" crs:Exposure2012="+1.10" crs:Contrast2012="+20"/></rdf:RDF></x:xmpmeta>"#;
let sc = parse_sidecar(xmp, true).unwrap();
let sc = parse_sidecar(xmp, crate::crs::Target::RawAbsolute).unwrap();
let Some(DevelopPatch::Partial(partial)) = &sc.develop else { panic!("no crs: edit") };
assert!(partial.get("process").is_none(), "{partial}");
let mut fresh = Photo::new(PhotoId(999), Source::Demo { scene: 1 }, "a.jpg", "JPEG", 10, 10, "2026-10-09T00:00:00");

View File

@ -206,5 +206,20 @@
"In line for AI Denoise ({ahead} ahead)…": "En attente de la réduction du bruit par IA ({ahead} devant)…",
"Running on {}.": "En cours d’exécution sur {}.",
"{file} · {size}": "{file} · {size}",
"• {assumption}": "• {assumption}"
"• {assumption}": "• {assumption}",
"Put inside “{parent}”": "Placer dans « {parent} »",
"Delete the keyword “{name}”?": "Supprimer le mot-clé « {name} » ?",
"It is taken off {count} photo{}, with the keywords below it. Undo brings it back.": "Il est retiré de {count} photo{}, avec les mots-clés qu’il contient. Annuler le rétablit.",
"Create Keyword Tag Inside “{name}”…": "Créer un mot-clé dans « {name} »…",
"“{parent}” has a “{name}” already. Merge the two?": "« {parent} » contient déjà « {name} ». Fusionner les deux ?",
"There is a “{name}” at the top level already. Merge the two?": "Il existe déjà « {name} » au premier niveau. Fusionner les deux ?",
"Added “{keyword}” to {n} photo{}": "« {keyword} » ajouté à {n} photo{}",
"On {have} of {of} selected photos": "Sur {have} des {of} photos sélectionnées",
"Remove “{name}”": "Retirer « {name} »",
"There is a keyword set “{name}” already": "Un jeu de mots-clés « {name} » existe déjà",
"Added {n} keyword{}": "Mots-clés ajoutés : {n}{:.0}",
"; {n} gained synonyms": " ; avec de nouveaux synonymes : {n}",
"Exported {n} keyword{}": "Mots-clés exportés : {n}{:.0}",
" — Capture One won't import a list with ; , < or > in a keyword: {names}": " — Capture One n’importe pas une liste dont un mot-clé contient ; , < ou > : {names}",
" — left out, as a keyword list can't hold them: {names}": " — omis, car une liste de mots-clés ne peut pas les contenir : {names}"
}

View File

@ -206,5 +206,20 @@
"In line for AI Denoise ({ahead} ahead)…": "У черзі на усунення шуму ШІ (попереду: {ahead})…",
"Running on {}.": "Працює на {}.",
"{file} · {size}": "{file} · {size}",
"• {assumption}": "• {assumption}"
"• {assumption}": "• {assumption}",
"Put inside “{parent}”": "Помістити в «{parent}»",
"Delete the keyword “{name}”?": "Видалити ключове слово «{name}»?",
"It is taken off {count} photo{}, with the keywords below it. Undo brings it back.": "Його буде знято з фото (кількість: {count}{:.0}) разом із вкладеними ключовими словами. Скасування поверне його.",
"Create Keyword Tag Inside “{name}”…": "Створити ключове слово в «{name}»…",
"“{parent}” has a “{name}” already. Merge the two?": "«{parent}» уже містить «{name}». Об’єднати їх?",
"There is a “{name}” at the top level already. Merge the two?": "На верхньому рівні вже є «{name}». Об’єднати їх?",
"Added “{keyword}” to {n} photo{}": "«{keyword}» додано до фото: {n}{:.0}",
"On {have} of {of} selected photos": "На {have} з {of} вибраних фото",
"Remove “{name}”": "Вилучити «{name}»",
"There is a keyword set “{name}” already": "Набір ключових слів «{name}» уже існує",
"Added {n} keyword{}": "Додано ключових слів: {n}{:.0}",
"; {n} gained synonyms": "; отримали синоніми: {n}",
"Exported {n} keyword{}": "Експортовано ключових слів: {n}{:.0}",
" — Capture One won't import a list with ; , < or > in a keyword: {names}": " — Capture One не імпортує список, у якому ключове слово містить ; , < або >: {names}",
" — left out, as a keyword list can't hold them: {names}": " — пропущено, бо список ключових слів не може їх містити: {names}"
}

View File

@ -36,11 +36,11 @@ pub mod theme;
pub mod titlebar;
pub mod widgets;
#[cfg(test)]
mod tests_crop_rotate;
#[cfg(test)]
mod tests_album_picker;
#[cfg(test)]
mod tests_crop_rotate;
#[cfg(test)]
mod tests_curve;
#[cfg(test)]
mod tests_date_picker;