XMP: preserve Upright warning modes across import paths

This commit is contained in:
Adrian D'souza
2026-10-09 08:54:49 +05:30
parent ec77f60b1b
commit 7d3cd8afe5
14 changed files with 164 additions and 21 deletions
+7 -3
View File
@@ -987,7 +987,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Auto Import Now",
[],
None,
"{listing?: [[path, size]] (the folder as listed by the caller, e.g. on a worker thread), start?: bool (false: return the `library.import` params as `import` instead of importing)} — add the watched folder's new photos (files the library doesn't have yet; partial / still-copying files wait for the next scan) → {imported, folder, import?}",
"{listing?: [[path, size]] (the folder as listed by the caller, e.g. on a worker thread), start?: bool (false: return the `library.import` params as `import` instead of importing)} — add the watched folder's new photos (files the library doesn't have yet; partial / still-copying files wait for the next scan) → {imported, folder, warnings?: [{path, code, profile?, mode?, fields?}], import?}",
always,
|s, p| {
let Some(folder) = s.import_defaults.auto_folder.clone() else { return Ok(json!({"imported": [], "folder": null})) };
@@ -1041,7 +1041,11 @@ pub fn specs() -> Vec<CommandSpec> {
let r = s.execute("library.import", &params)?;
// arriving photos don't take over the selection
s.selection = sel;
Ok(json!({"imported": r["imported"], "folder": folder}))
let mut result = json!({"imported": r["imported"], "folder": folder});
if let Some(warnings) = r.get("warnings") {
result["warnings"] = warnings.clone();
}
Ok(result)
}
),
cmd!("album.removePhotos", "Remove from Album", [], None, "{id: albumId, ids?}", has_selection, |s, p| {
@@ -1103,7 +1107,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Import Photos",
["File"],
Some("Cmd+Shift+I"),
"{paths: [file or folder (recursive)], mode?: add|copy|move (add = reference the files in place; copy = into the library's Originals/YYYY/YYYY-MM-DD/; move = as copy, then each original and its XMP sidecars are removed from the source — only after the copy is verified (a hard link on the same volume, else copied, synced and compared byte for byte) and its catalog record is saved; failed, duplicate and unchecked files keep their sources; a taken name gets -1, -2…; undo removes the photos from the library but leaves the files at the destination), destination?: folder for copies / moves, organize?: date (YYYY/YYYY-MM-DD) | month (YYYY/YYYY-MM) | flat | a folder template, e.g. `{date:%Y}/{date:%Y%m%d}` → 2026/20260114 (the template's `/` make the folders, each level expanded with the rename tokens and made a safe folder name: never outside the destination; must be relative, no `..`; a level with missing metadata is `unknown`) — dated by capture time, else the import time, rename?: file-name template for copies, original extension added (tokens: {name} {num} {seq} {seq:N} {date} {date:%Y%m%d} {folder} {camera} {lens} {iso} {rating} {title} {creator} {ext}; photo.renameTokens explains each; blank = keep names), renameStart?: 1, metadataPreset?: name, dng?: bool (copy raws as DNG; copy only), local?: bool (browsing: the photos stay out of the library, like library.browse; not with move), album?: albumId, albumName?: new album, preset?: presetId, keywords?: [..], onDeleted?: skip|restore|fresh (a file that is in Recently Deleted: skip = leave it there and say so, restore = bring the photo back with its edits, fresh = delete the trashed record and import the file as new)} → {imported, duplicates: [{path, existing, reason, existingDeleted?}], restored?: [photoId], failed, warnings?: [{path, code: cameraProfile|uprightGeometry|unmappedXmp, profile?, fields?}], moved?: [{from, to, sidecars?}], kept?: [{path, reason}] (move: sources left in place and why), album?}",
"{paths: [file or folder (recursive)], mode?: add|copy|move (add = reference the files in place; copy = into the library's Originals/YYYY/YYYY-MM-DD/; move = as copy, then each original and its XMP sidecars are removed from the source — only after the copy is verified (a hard link on the same volume, else copied, synced and compared byte for byte) and its catalog record is saved; failed, duplicate and unchecked files keep their sources; a taken name gets -1, -2…; undo removes the photos from the library but leaves the files at the destination), destination?: folder for copies / moves, organize?: date (YYYY/YYYY-MM-DD) | month (YYYY/YYYY-MM) | flat | a folder template, e.g. `{date:%Y}/{date:%Y%m%d}` → 2026/20260114 (the template's `/` make the folders, each level expanded with the rename tokens and made a safe folder name: never outside the destination; must be relative, no `..`; a level with missing metadata is `unknown`) — dated by capture time, else the import time, rename?: file-name template for copies, original extension added (tokens: {name} {num} {seq} {seq:N} {date} {date:%Y%m%d} {folder} {camera} {lens} {iso} {rating} {title} {creator} {ext}; photo.renameTokens explains each; blank = keep names), renameStart?: 1, metadataPreset?: name, dng?: bool (copy raws as DNG; copy only), local?: bool (browsing: the photos stay out of the library, like library.browse; not with move), album?: albumId, albumName?: new album, preset?: presetId, keywords?: [..], onDeleted?: skip|restore|fresh (a file that is in Recently Deleted: skip = leave it there and say so, restore = bring the photo back with its edits, fresh = delete the trashed record and import the file as new)} → {imported, duplicates: [{path, existing, reason, existingDeleted?}], restored?: [photoId], failed, warnings?: [{path, code: cameraProfile|uprightGeometry|unmappedXmp, profile?, mode?: off|auto|level|vertical|full|guided|unknown, fields?}], moved?: [{from, to, sidecars?}], kept?: [{path, reason}] (move: sources left in place and why), album?}",
always,
|s, p| {
let req = import_params(s, p)?;
+10 -4
View File
@@ -35,11 +35,13 @@ fn read(s: &mut Session, p: &Value) -> Result<Value> {
let mut ops = Vec::new();
let mut read = Vec::new();
let mut failed = Vec::new();
let mut warnings = Vec::new();
for id in s.targets(p) {
match s.read_sidecar_op(id) {
Ok(Some((op, from))) => {
match s.read_sidecar_op_with_warnings(id) {
Ok(Some((op, from, mut photo_warnings))) => {
ops.push(op);
read.push(json!({"id": id.0, "from": from.display().to_string()}));
warnings.append(&mut photo_warnings);
}
Ok(None) => failed.push(json!({"id": id.0, "error": "no XMP sidecar or embedded XMP"})),
Err(e) => failed.push(json!({"id": id.0, "error": e.to_string()})),
@@ -48,7 +50,11 @@ fn read(s: &mut Session, p: &Value) -> Result<Value> {
if !ops.is_empty() {
s.commit("Read Metadata from File", Op::Batch { ops })?;
}
Ok(json!({"read": read, "failed": failed}))
let mut result = json!({"read": read, "failed": failed});
if !warnings.is_empty() {
result["warnings"] = serde_json::to_value(warnings).unwrap_or_default();
}
Ok(result)
}
fn prefs(s: &mut Session, p: &Value) -> Result<Value> {
@@ -84,7 +90,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Read Metadata from File",
["Photo"],
None,
"{ids?} — reads each photo's XMP sidecar (or a raw/DNG's embedded XMP): metadata and develop settings, one undo step → {read, failed}",
"{ids?} — reads each photo's XMP sidecar (or a raw/DNG's embedded XMP): metadata and develop settings, one undo step → {read, failed, warnings?: [{path, code, profile?, mode?, fields?}]}",
has_selection,
read
),
+15
View File
@@ -215,6 +215,19 @@ pub enum ImportWarningCode {
UnmappedXmp,
}
/// The saved Lightroom Upright mode associated with a foreign geometry transform.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ImportUprightMode {
Off,
Auto,
Level,
Vertical,
Full,
Guided,
Unknown,
}
/// One actionable XMP import diagnostic. `path` is filled when its photo is committed.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -223,6 +236,8 @@ pub struct ImportWarning {
pub code: ImportWarningCode,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<ImportUprightMode>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub fields: Vec<String>,
}
+28 -1
View File
@@ -172,15 +172,32 @@ pub fn parse_sidecar(xmp: &str, raw: bool) -> std::result::Result<SidecarData, S
path: String::new(),
code: crate::import::ImportWarningCode::CameraProfile,
profile: Some(name),
mode: None,
fields: Vec::new(),
});
}
let upright = crate::crs::has_saved_upright_transform(&d.properties);
if upright {
let mode = d
.properties
.get("crs:PerspectiveUpright")
.and_then(|values| values.first())
.map(String::as_str)
.map(|mode| match mode {
"0" => crate::import::ImportUprightMode::Off,
"1" => crate::import::ImportUprightMode::Auto,
"2" => crate::import::ImportUprightMode::Level,
"3" => crate::import::ImportUprightMode::Vertical,
"4" => crate::import::ImportUprightMode::Full,
"5" => crate::import::ImportUprightMode::Guided,
_ => crate::import::ImportUprightMode::Unknown,
})
.unwrap_or(crate::import::ImportUprightMode::Unknown);
out.warnings.push(crate::import::ImportWarning {
path: String::new(),
code: crate::import::ImportWarningCode::UprightGeometry,
profile: None,
mode: Some(mode),
fields: Vec::new(),
});
}
@@ -198,6 +215,7 @@ pub fn parse_sidecar(xmp: &str, raw: bool) -> std::result::Result<SidecarData, S
path: String::new(),
code: crate::import::ImportWarningCode::UnmappedXmp,
profile: None,
mode: None,
fields: other.into_iter().map(str::to_string).collect(),
});
}
@@ -511,12 +529,21 @@ impl Session {
/// The op that applies a photo's sidecar (or embedded XMP) to the catalog, if there is one.
pub fn read_sidecar_op(&self, id: PhotoId) -> Result<Option<(Op, PathBuf)>> {
self.read_sidecar_op_with_warnings(id).map(|result| result.map(|(op, from, _)| (op, from)))
}
/// Like [`Session::read_sidecar_op`], retaining transient diagnostics for the command report.
pub fn read_sidecar_op_with_warnings(&self, id: PhotoId) -> Result<Option<(Op, PathBuf, Vec<crate::import::ImportWarning>)>> {
let p = self.catalog.photo(id).ok_or(lightcraft_catalog::CatalogError::NoPhoto(id))?;
let Some(orig) = file_path(p) else { return Ok(None) };
let Some((packet, from)) = read_packet(orig, p.kind, self.sidecar_naming(id)) else { return Ok(None) };
let sc = parse_sidecar(&packet, p.kind == MediaKind::Raw)
.map_err(|e| EngineError::Other(format!("{}: {e}", from.display())))?
.resolve_label(&self.catalog);
let mut warnings = sc.warnings.clone();
for warning in &mut warnings {
warning.path = orig.to_string();
}
let mut q = (**p).clone();
let develop_changed = merge_into(&mut q, &sc, &(self.clock)());
let mut ops = vec![
@@ -531,7 +558,7 @@ impl Session {
if develop_changed {
ops.extend(self.develop_op(id, (*q.develop).clone(), "Read Metadata from File"));
}
Ok(Some((Op::Batch { ops }, from)))
Ok(Some((Op::Batch { ops }, from, warnings)))
}
/// Auto-write: sidecars for photos changed by `ops` (errors are logged, not returned).
+6
View File
@@ -395,11 +395,17 @@ fn auto_import_watched_folder() {
s.execute("library.autoImport", &json!({"folder": dir.to_string_lossy(), "album": "Tethered"})).unwrap();
assert_eq!(s.execute("library.autoImportScan", &json!({})).unwrap()["imported"], json!([]));
write_png(&dir.join("one.png"), 1);
std::fs::write(
dir.join("one.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 xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/" crs:CameraProfile="Color" crs:Exposure2012="0.5"/></rdf:RDF></x:xmpmeta>"#,
)
.unwrap();
std::fs::write(dir.join("notes.txt"), "not a photo").unwrap();
// first sight: wait (it may still be copying)
assert_eq!(s.execute("library.autoImportScan", &json!({})).unwrap()["imported"], json!([]));
let r = s.execute("library.autoImportScan", &json!({})).unwrap();
assert_eq!(r["imported"].as_array().unwrap().len(), 1, "{r}");
assert!(r["warnings"].as_array().unwrap().iter().any(|w| w["code"] == "cameraProfile" && w["profile"] == "Color"), "{r}");
let album = s.catalog.albums().find(|a| a.name == "Tethered").expect("album").id;
assert_eq!(s.catalog.album_count(album), 1);
assert!(s.selection.ids.is_empty(), "arrivals don't take the selection");
+34 -2
View File
@@ -170,6 +170,7 @@ fn foreign_crs_sidecar_on_import_and_read_from_file() {
std::fs::remove_file(src.join("harbour.xmp")).unwrap();
let r = s.execute("photo.readMetadataFromFile", &json!({})).unwrap();
assert_eq!(r["failed"].as_array().unwrap().len(), 1);
assert!(r.get("warnings").is_none(), "{r}");
let _ = std::fs::remove_dir_all(&src);
}
@@ -195,12 +196,43 @@ fn foreign_profile_and_upright_import_diagnostics_are_actionable() {
let r = s.execute("library.import", &json!({"paths": [photo.to_string_lossy()]})).unwrap();
let warnings = r["warnings"].as_array().unwrap();
assert!(warnings.iter().any(|w| w["code"] == "cameraProfile" && w["profile"] == "Camera Faithful"), "{r}");
assert!(warnings.iter().any(|w| w["code"] == "uprightGeometry"), "{r}");
let p = s.catalog.photos().next().unwrap();
assert!(warnings.iter().any(|w| w["code"] == "uprightGeometry" && w["mode"] == "auto"), "{r}");
let p = s.catalog.photos().next().unwrap().clone();
assert_eq!(p.develop.profile.id, "lc.color");
assert_eq!(p.develop.light.exposure, 1.68);
assert_eq!(std::fs::read(&sidecar).unwrap(), before, "import never rewrites foreign XMP");
// Metadata reread carries same structured diagnostics, with each saved Upright mode typed.
let photo_path = photo.to_string_lossy().to_string();
let reread = s.execute("photo.readMetadataFromFile", &json!({"ids": [p.id.0]})).unwrap();
assert!(
reread["warnings"]
.as_array()
.unwrap()
.iter()
.any(|w| w["code"] == "cameraProfile" && w["profile"] == "Camera Faithful" && w["path"] == photo_path),
"{reread}"
);
assert!(reread["warnings"].as_array().unwrap().iter().any(|w| w["code"] == "uprightGeometry" && w["mode"] == "auto"), "{reread}");
for (source, mode) in [("0", "off"), ("2", "level"), ("3", "vertical"), ("4", "full"), ("5", "guided"), ("9", "unknown")] {
let mode_xmp = xmp.replace("crs:PerspectiveUpright=\"1\"", &format!("crs:PerspectiveUpright=\"{source}\""));
std::fs::write(&sidecar, mode_xmp).unwrap();
let reread = s.execute("photo.readMetadataFromFile", &json!({"ids": [p.id.0]})).unwrap();
assert!(reread["warnings"].as_array().unwrap().iter().any(|w| w["code"] == "uprightGeometry" && w["mode"] == mode), "{source}: {reread}");
}
// A foreign name colliding with a built-in profile label remains warning metadata only.
let collision = src.join("collision.png");
write_png(&collision, 8);
std::fs::write(src.join("collision.xmp"), xmp.replace("Camera Faithful", "Color")).unwrap();
let collision_report = s.execute("library.import", &json!({"paths": [collision.to_string_lossy()]})).unwrap();
assert!(
collision_report["warnings"].as_array().unwrap().iter().any(|w| w["code"] == "cameraProfile" && w["profile"] == "Color"),
"{collision_report}"
);
let collision_photo = s.catalog.photos().find(|photo| photo.file_name == "collision.png").unwrap();
assert_eq!(collision_photo.develop.profile.id, "lc.color");
// A second import still diagnoses the untouched foreign packet, while an XMP-free photo does not.
drop(s);
let clean = src.join("clean.png");
+4 -1
View File
@@ -62,7 +62,10 @@
"Import failed: {e}": "Import fehlgeschlagen: {e}",
"Import warnings ({n}):\n{warnings}": "Importwarnungen ({n}):\n{warnings}",
"XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "XMP-Profil \"{profile}\" wurde für {path} nicht angewendet; Farben können von Lightroom abweichen.",
"Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "Lightroom-Geometrie wurde für {path} nicht beibehalten; Upright wird neu berechnet.",
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "Lightroom-Geometrie wurde für {path} nicht beibehalten; {mode} wird neu berechnet.",
"Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "Lightroom-Geometrie für {path} wurde nicht beibehalten; gespeicherte Transformation wird ignoriert, weil Upright deaktiviert ist.",
"Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "Lightroom-Geometrie für {path} wurde nicht beibehalten; gespeicherte Guided-Upright-Transformation wird nicht unterstützt.",
"Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "Lightroom-Geometrie für {path} wurde nicht beibehalten; gespeicherte Upright-Transformation wird nicht unterstützt.",
"XMP adjustments were not imported for {path}: {fields}.": "XMP-Anpassungen wurden für {path} nicht importiert: {fields}.",
"Imported {n} point curve preset{}": "Importierte Punktkurvenvorgaben: {n}{:.0}",
"Imported {n} preset{}": "Importierte Vorgaben: {n}{:.0}",
+4 -1
View File
@@ -62,7 +62,10 @@
"Import failed: {e}": "Error de importación: {e}",
"Import warnings ({n}):\n{warnings}": "Avisos de importación ({n}):\n{warnings}",
"XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "El perfil XMP \"{profile}\" no se aplicó a {path}; los colores pueden diferir de Lightroom.",
"Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "La geometría de Lightroom no se conservó para {path}; Upright se recalculará.",
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "La geometría de Lightroom no se conservó para {path}; {mode} se recalculará.",
"Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "La geometría de Lightroom no se conservó para {path}; la transformación guardada se ignora porque Upright está desactivado.",
"Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "La geometría de Lightroom no se conservó para {path}; la transformación guardada de Upright guiado no es compatible.",
"Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "La geometría de Lightroom no se conservó para {path}; la transformación guardada de Upright no es compatible.",
"XMP adjustments were not imported for {path}: {fields}.": "Los ajustes XMP no se importaron para {path}: {fields}.",
"Imported {n} point curve preset{}": "{n} ajuste{0} preestablecido{0} de curva de puntos importado{0}",
"Imported {n} preset{}": "{n} ajuste{0} preestablecido{0} importado{0}",
+4 -1
View File
@@ -155,7 +155,10 @@
"Import failed: {e}": "読み込みに失敗しました:{e}",
"Import warnings ({n}):\n{warnings}": "読み込みの警告({n}):\n{warnings}",
"XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "{path} に XMP プロファイル \"{profile}\" を適用できませんでした。色は Lightroom と異なる場合があります。",
"Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "{path} の Lightroom ジオメトリは保持されませんでした。Upright を再計算します。",
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "{path} の Lightroom ジオメトリは保持されませんでした。{mode} を再計算します。",
"Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "{path} の Lightroom ジオメトリは保持されませんでした。Upright がオフのため保存された変換は無視されます。",
"Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "{path} の Lightroom ジオメトリは保持されませんでした。ガイド付き Upright の保存された変換はサポートされていません。",
"Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "{path} の Lightroom ジオメトリは保持されませんでした。保存された Upright の変換はサポートされていません。",
"XMP adjustments were not imported for {path}: {fields}.": "{path} の XMP 調整は読み込まれませんでした:{fields}。",
"Imported {profiles} profile{} (Profile browser ▸ their groups)": "{profiles}個のプロファイルを読み込みました{:.0}(プロファイルブラウザー → 各グループ)",
"Insert at the cursor (also written {})": "カーソル位置に挿入({}とも表記可)",
+4 -1
View File
@@ -155,7 +155,10 @@
"Import failed: {e}": "Falha na importação: {e}",
"Import warnings ({n}):\n{warnings}": "Avisos de importação ({n}):\n{warnings}",
"XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "O perfil XMP \"{profile}\" não foi aplicado a {path}; as cores podem diferir do Lightroom.",
"Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "A geometria do Lightroom não foi preservada para {path}; o Upright será recalculado.",
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "A geometria do Lightroom não foi preservada para {path}; {mode} será recalculado.",
"Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "A geometria do Lightroom não foi preservada para {path}; a transformação salva é ignorada porque o Upright está desativado.",
"Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "A geometria do Lightroom não foi preservada para {path}; a transformação salva do Upright guiado não é compatível.",
"Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "A geometria do Lightroom não foi preservada para {path}; a transformação salva do Upright não é compatível.",
"XMP adjustments were not imported for {path}: {fields}.": "Os ajustes XMP não foram importados para {path}: {fields}.",
"Imported {profiles} profile{} (Profile browser ▸ their groups)": "{profiles} perfil(is) importado(s){:.0} (Navegador de perfis → seus grupos)",
"Insert at the cursor (also written {})": "Inserir no cursor (também escrito {})",
+4 -1
View File
@@ -65,7 +65,10 @@
"Import failed: {e}": "Ошибка импорта: {e}",
"Import warnings ({n}):\n{warnings}": "Предупреждения импорта ({n}):\n{warnings}",
"XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "Профиль XMP \"{profile}\" не применён к {path}; цвета могут отличаться от Lightroom.",
"Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "Геометрия Lightroom для {path} не сохранена; Upright будет пересчитан.",
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "Геометрия Lightroom для {path} не сохранена; режим {mode} будет пересчитан.",
"Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "Геометрия Lightroom для {path} не сохранена; сохранённое преобразование игнорируется, поскольку Upright выключен.",
"Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "Геометрия Lightroom для {path} не сохранена; сохранённое преобразование Guided Upright не поддерживается.",
"Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "Геометрия Lightroom для {path} не сохранена; сохранённое преобразование Upright не поддерживается.",
"XMP adjustments were not imported for {path}: {fields}.": "Коррекции XMP для {path} не импортированы: {fields}.",
"Imported {n} point curve preset{}": "Импортировано пресетов точечной кривой: {n}{:.0}",
"Imported {n} preset{}": "Импортировано пресетов: {n}{:.0}",
+4 -1
View File
@@ -155,7 +155,10 @@
"Import failed: {e}": "导入失败:{e}",
"Import warnings ({n}):\n{warnings}": "导入警告({n}):\n{warnings}",
"XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "未对 {path} 应用 XMP 配置文件“{profile}”;颜色可能与 Lightroom 不同。",
"Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "未保留 {path} 的 Lightroom 几何信息;将重新计算 Upright。",
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "未保留 {path} 的 Lightroom 几何信息;将重新计算 {mode}。",
"Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "未保留 {path} 的 Lightroom 几何信息;由于 Upright 已关闭,将忽略保存的变换。",
"Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "未保留 {path} 的 Lightroom 几何信息;不支持保存的引导 Upright 变换。",
"Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "未保留 {path} 的 Lightroom 几何信息;不支持保存的 Upright 变换。",
"XMP adjustments were not imported for {path}: {fields}.": "未导入 {path} 的 XMP 调整:{fields}。",
"Imported {profiles} profile{} (Profile browser ▸ their groups)": "已导入 {profiles} 个配置文件{0:.0}(配置文件浏览器 → 各自的分组)",
"Insert at the cursor (also written {})": "在光标处插入(也可写作 {0})",
+4 -1
View File
@@ -155,7 +155,10 @@
"Import failed: {e}": "匯入失敗:{e}",
"Import warnings ({n}):\n{warnings}": "匯入警告({n}):\n{warnings}",
"XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "未將 XMP 描述檔「{profile}」套用至 {path};色彩可能與 Lightroom 不同。",
"Lightroom geometry was not preserved for {path}; Upright will be recalculated.": "未保留 {path} 的 Lightroom 幾何資料;將重新計算 Upright。",
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "未保留 {path} 的 Lightroom 幾何資料;將重新計算 {mode}。",
"Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.": "未保留 {path} 的 Lightroom 幾何資料;Upright 已關閉,因此忽略儲存的轉換。",
"Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.": "未保留 {path} 的 Lightroom 幾何資料;不支援儲存的引導 Upright 轉換。",
"Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.": "未保留 {path} 的 Lightroom 幾何資料;不支援儲存的 Upright 轉換。",
"XMP adjustments were not imported for {path}: {fields}.": "未匯入 {path} 的 XMP 調整:{fields}。",
"Imported {profiles} profile{} (Profile browser ▸ their groups)": "已匯入 {profiles} 個描述檔{:.0}(描述檔瀏覽器 → 各自的群組)",
"Insert at the cursor (also written {})": "在游標處插入(也可寫成 {})",
+36 -4
View File
@@ -8,7 +8,9 @@
//! catalog between frames, with a progress window and Cancel; the whole import is one undo step.
use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2};
use lightcraft_engine::import::{ImportCandidate, ImportWarning, ImportWarningCode, ScanInput, ScanOutput, ScanProgress, scan_with};
use lightcraft_engine::import::{
ImportCandidate, ImportUprightMode, ImportWarning, ImportWarningCode, ScanInput, ScanOutput, ScanProgress, scan_with,
};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::sync::atomic::Ordering;
@@ -718,9 +720,39 @@ fn xmp_warning_text(warning: &ImportWarning) -> String {
profile = warning.profile.as_deref().unwrap_or_default(),
path = path,
),
ImportWarningCode::UprightGeometry => {
crate::i18n::tr_format!("Lightroom geometry was not preserved for {path}; Upright will be recalculated.", path = path,)
}
ImportWarningCode::UprightGeometry => match warning.mode.unwrap_or(ImportUprightMode::Unknown) {
ImportUprightMode::Off => crate::i18n::tr_format!(
"Lightroom geometry for {path} was not preserved; saved transform is ignored because Upright is Off.",
path = path,
),
ImportUprightMode::Guided => crate::i18n::tr_format!(
"Lightroom geometry for {path} was not preserved; Guided Upright saved transform is unsupported.",
path = path,
),
ImportUprightMode::Auto => crate::i18n::tr_format!(
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.",
path = path,
mode = crate::i18n::tr("Auto")
),
ImportUprightMode::Level => crate::i18n::tr_format!(
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.",
path = path,
mode = crate::i18n::tr("Level")
),
ImportUprightMode::Vertical => crate::i18n::tr_format!(
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.",
path = path,
mode = crate::i18n::tr("Vertical")
),
ImportUprightMode::Full => crate::i18n::tr_format!(
"Lightroom geometry was not preserved for {path}; {mode} will be recalculated.",
path = path,
mode = crate::i18n::tr("Full")
),
ImportUprightMode::Unknown => {
crate::i18n::tr_format!("Lightroom geometry for {path} was not preserved; saved Upright transform is unsupported.", path = path,)
}
},
ImportWarningCode::UnmappedXmp => {
crate::i18n::tr_format!("XMP adjustments were not imported for {path}: {fields}.", path = path, fields = warning.fields.join(", "),)
}