From 20b6eb885e66200d7de0a398aa8515a831b77959 Mon Sep 17 00:00:00 2001 From: Adrian D'souza Date: Fri, 9 Oct 2026 08:45:25 +0530 Subject: [PATCH 01/24] XMP: report unsupported photo import settings --- crates/engine/src/cmd/library.rs | 2 +- crates/engine/src/crs.rs | 16 ++++++++++++ crates/engine/src/import.rs | 4 +++ crates/engine/src/sidecar.rs | 42 ++++++++++++++++++++++++++++- crates/engine/src/tests_xmp.rs | 45 ++++++++++++++++++++++++++++++++ docs/xmp-interop.md | 10 +++++-- 6 files changed, 115 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/library.rs b/crates/engine/src/cmd/library.rs index 436ad123..a634f37f 100644 --- a/crates/engine/src/cmd/library.rs +++ b/crates/engine/src/cmd/library.rs @@ -1103,7 +1103,7 @@ pub fn specs() -> Vec { "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, 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?: [per-photo XMP diagnostic], moved?: [{from, to, sidecars?}], kept?: [{path, reason}] (move: sources left in place and why), album?}", always, |s, p| { let req = import_params(s, p)?; diff --git a/crates/engine/src/crs.rs b/crates/engine/src/crs.rs index 0b0c83b9..32e3c3ea 100644 --- a/crates/engine/src/crs.rs +++ b/crates/engine/src/crs.rs @@ -416,6 +416,22 @@ pub fn to_partial_report(props: &Props, values: Option<&crate::crs_masks::Values (out, unmapped) } +/// The selected camera profile name from an XMP packet. The name is metadata only: it does not +/// identify or carry the profile's rendering data. +pub fn camera_profile_name(props: &Props) -> Option<&str> { + first(props, "crs:CameraProfile") +} + +/// Whether an XMP packet contains a saved Lightroom Upright matrix. Lightroom's matrix uses a +/// coordinate convention that LightCraft does not currently import. +pub fn has_saved_upright_transform(props: &Props) -> bool { + props.keys().any(|key| { + key.strip_prefix("crs:").is_some_and(|name| { + name.starts_with("UprightTransform_") && props.get(key).is_some_and(|values| values.iter().any(|v| !v.trim().is_empty())) + }) + }) +} + /// Read an XMP preset (`crs:` fields + `crs:Name` / `crs:Group`) into one of our presets. /// Returns `None` when the packet has no adjustments we understand. pub fn preset_from_xmp(xmp: &str, fallback_name: &str) -> Option { diff --git a/crates/engine/src/import.rs b/crates/engine/src/import.rs index 282d93b7..b12b19e8 100644 --- a/crates/engine/src/import.rs +++ b/crates/engine/src/import.rs @@ -198,6 +198,9 @@ pub struct ImportReport { pub scanned: usize, /// Photos whose metadata/develop settings were read from an XMP sidecar (or embedded XMP). pub sidecars: usize, + /// Per-photo diagnostics from foreign XMP develop settings that could not be fully carried over. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub warnings: Vec, } /// A file moved by an import. @@ -1235,6 +1238,7 @@ pub fn commit_prepared(s: &mut Session, opts: &ImportOptions, now: &str, prepare if let Some(sc) = &sidecar { crate::sidecar::merge_into(&mut p, sc, &now); report.sidecars += 1; + report.warnings.extend(sc.warnings.iter().map(|warning| format!("{path}: {warning}"))); } if let Some(mp) = opts.metadata_preset.as_ref().and_then(|n| s.metadata_presets.iter().find(|m| m.name.eq_ignore_ascii_case(n))) { crate::cmd::metadata::apply_to(&mut p.meta, &mp.fields); diff --git a/crates/engine/src/sidecar.rs b/crates/engine/src/sidecar.rs index 7e924948..3cd0d132 100644 --- a/crates/engine/src/sidecar.rs +++ b/crates/engine/src/sidecar.rs @@ -104,6 +104,9 @@ pub struct SidecarData { /// `xmp:CreateDate` (first found). Used only when the file itself has no capture time. pub captured: Option, pub develop: Option, + /// Actionable diagnostics for foreign develop settings that were not fully carried over. + /// These are surfaced in the photo import report and never persisted in the catalog. + pub warnings: Vec, } #[derive(Clone, Debug, PartialEq)] @@ -162,7 +165,44 @@ pub fn parse_sidecar(xmp: &str, raw: bool) -> std::result::Result Some(DevelopPatch::Full(Box::new(s))), None if crate::crs::has_adjustments(&d.properties) => { - Some(DevelopPatch::Partial(crate::crs::to_partial_report(&d.properties, Some(&d.values), Some(raw), crate::crs_masks::DEFAULT_ASPECT).0)) + let (mut partial, unmapped) = crate::crs::to_partial_report(&d.properties, Some(&d.values), Some(raw), crate::crs_masks::DEFAULT_ASPECT); + let profile = crate::crs::camera_profile_name(&d.properties).map(str::to_string); + if let Some(name) = &profile + && let Some(profile) = partial.get_mut("profile").and_then(Value::as_object_mut) + { + // Keep the selected name for inspection and round trips. An unknown id renders + // as LightCraft's neutral profile; the warning below makes that limitation clear. + profile.insert("id".into(), Value::String(name.clone())); + } + if let Some(name) = profile { + out.warnings.push(format!("{name} could not be applied; colors may differ from Lightroom.")); + } + 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).unwrap_or_default(); + let mode = match mode { + "1" => "Auto", + "2" => "Level", + "3" => "Vertical", + "4" => "Full", + "5" => "Guided", + _ => "Upright", + }; + out.warnings.push(format!("Lightroom geometry was not preserved; {mode} will be recalculated.")); + } + let other: Vec<&str> = unmapped + .iter() + .map(String::as_str) + .filter(|name| { + *name != "CameraProfile" + && !name.starts_with("UprightTransform_") + && !matches!(*name, "UprightTransformCount" | "UprightCenterNormX" | "UprightCenterNormY" | "UprightVersion") + }) + .collect(); + if !other.is_empty() { + out.warnings.push(format!("XMP adjustments not imported: {}.", other.join(", "))); + } + Some(DevelopPatch::Partial(partial)) } None => None, }; diff --git a/crates/engine/src/tests_xmp.rs b/crates/engine/src/tests_xmp.rs index fede1fec..962042d9 100644 --- a/crates/engine/src/tests_xmp.rs +++ b/crates/engine/src/tests_xmp.rs @@ -173,6 +173,51 @@ fn foreign_crs_sidecar_on_import_and_read_from_file() { let _ = std::fs::remove_dir_all(&src); } +/// Foreign profile names and saved Upright matrices stay visible without pretending their +/// proprietary colour or coordinate data was imported. Diagnostics are per-photo report data; +/// importing a file without XMP remains quiet, and the source packet is never rewritten. +#[test] +fn foreign_profile_and_upright_import_diagnostics_are_actionable() { + let src = temp_dir("xmp-diagnostics"); + let photo = src.join("faithful.png"); + write_png(&photo, 6); + let xmp = r#" + + "#; + let sidecar = src.join("faithful.xmp"); + std::fs::write(&sidecar, xmp).unwrap(); + let before = std::fs::read(&sidecar).unwrap(); + let mut s = Session::new().with_fs(); + 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.as_str().is_some_and(|w| w.contains("Camera Faithful could not be applied"))), "{r}"); + assert!( + warnings + .iter() + .any(|w| w.as_str().is_some_and(|w| w.contains("Lightroom geometry was not preserved") && w.contains("Auto will be recalculated"))), + "{r}" + ); + let p = s.catalog.photos().next().unwrap(); + assert_eq!(p.develop.profile.id, "Camera Faithful"); + assert_eq!(p.develop.light.exposure, 1.68); + assert_eq!(std::fs::read(&sidecar).unwrap(), before, "import never rewrites foreign XMP"); + + // A second import still diagnoses the untouched foreign packet, while an XMP-free photo does not. + drop(s); + let clean = src.join("clean.png"); + write_png(&clean, 7); + let mut s = Session::new().with_fs(); + let clean_report = s.execute("library.import", &json!({"paths": [clean.to_string_lossy()]})).unwrap(); + assert!(clean_report.get("warnings").is_none(), "{clean_report}"); + let repeat = s.execute("library.import", &json!({"paths": [photo.to_string_lossy()]})).unwrap(); + assert_eq!(repeat["warnings"].as_array().unwrap().len(), 2, "{repeat}"); + let _ = std::fs::remove_dir_all(&src); +} + const MWG_REGIONS: &str = r#" diff --git a/docs/xmp-interop.md b/docs/xmp-interop.md index 73543231..ebdbb5d2 100644 --- a/docs/xmp-interop.md +++ b/docs/xmp-interop.md @@ -37,6 +37,10 @@ used — on import (it then also files a copied photo in its date folder) and by `xmp:Rating="-1"` (the XMP convention for rejected) sets the reject flag. Develop settings come from `lc:settings` when present (exact); otherwise from the `crs:` fields below (approximate). +When foreign `crs:` settings are only partly transferable, `library.import` includes per-photo `warnings` in its +report. Warnings identify unsupported profile names, saved Upright geometry that will be recalculated, and other +unmapped adjustments; they do not change the source packet or become catalog edits. + ## Saving into an existing sidecar A sidecar may already hold another application's data — e.g. its `crs:` develop settings and `xmpMM:History` — often @@ -133,12 +137,14 @@ names are read (e.g. `Exposure2012`, not the older `Exposure`). | `DefringePurple/GreenAmount/HueLo/HueHi` | `optics.defringe_*` | | | `ShadowTint`, `RedHue/Saturation`, `GreenHue/Saturation`, `BlueHue/Saturation` | `calibration.shadows_tint`, `calibration.red_hue/red_sat`, … | Calibration panel, 1:1 | | `PerspectiveVertical/Horizontal/Rotate/Scale/Aspect/X/Y` | `geometry.vertical/horizontal/rotate/scale/aspect/offset_x/offset_y` | | -| `PerspectiveUpright` | `geometry.upright` | 0 off, 1 auto, 2 level, 3 vertical, 4 full, 5 guided | +| `PerspectiveUpright` | `geometry.upright` | 0 off, 1 auto, 2 level, 3 vertical, 4 full, 5 guided; saved `UprightTransform_*` matrices are not imported, so photo import reports that Lightroom geometry was not preserved and the selected mode will be recalculated | | `HasCrop`, `CropLeft/Top/Right/Bottom`, `CropAngle` | `crop.geometry` | normalized edges → rect; angle in degrees; `HasCrop="False"` → no crop | Values pass through our control specs, so anything outside our slider ranges gets clamped. -**Not mapped:** camera profiles and looks (`CameraProfile`, `Look`; we have our own profile set), local adjustments +`crs:CameraProfile` is retained as its selected name in the photo's profile settings, but the name carries no +rendering data and does not select an Adobe profile. Photo import reports that the profile could not be applied and +colours may differ from Lightroom. Camera profiles and looks (`Look`; we have our own profile set), local adjustments (masks, gradients, brushes), spot removal, red eye, lens blur, process-version 2010 field names, and AI features. ## Local corrections (masks) From ec77f60b1bd4c61fce5ec51414c34528f7940a68 Mon Sep 17 00:00:00 2001 From: Adrian D'souza Date: Fri, 9 Oct 2026 08:49:38 +0530 Subject: [PATCH 02/24] XMP: surface structured import warnings in UI --- crates/engine/src/cmd/library.rs | 2 +- crates/engine/src/import.rs | 31 +++++++++++- crates/engine/src/sidecar.rs | 41 ++++++++-------- crates/engine/src/tests_xmp.rs | 11 ++--- crates/ui-egui/locales/de-formats.json | 4 ++ crates/ui-egui/locales/es-formats.json | 4 ++ crates/ui-egui/locales/ja-formats.json | 4 ++ crates/ui-egui/locales/pt-br-formats.json | 4 ++ crates/ui-egui/locales/ru-formats.json | 4 ++ crates/ui-egui/locales/zh-hans-formats.json | 4 ++ crates/ui-egui/locales/zh-hant-formats.json | 4 ++ crates/ui-egui/src/import.rs | 54 +++++++++++++++++++-- docs/xmp-interop.md | 6 +-- 13 files changed, 133 insertions(+), 40 deletions(-) diff --git a/crates/engine/src/cmd/library.rs b/crates/engine/src/cmd/library.rs index a634f37f..0eafa49a 100644 --- a/crates/engine/src/cmd/library.rs +++ b/crates/engine/src/cmd/library.rs @@ -1103,7 +1103,7 @@ pub fn specs() -> Vec { "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?: [per-photo XMP diagnostic], 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?, 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)?; diff --git a/crates/engine/src/import.rs b/crates/engine/src/import.rs index b12b19e8..ad0ca934 100644 --- a/crates/engine/src/import.rs +++ b/crates/engine/src/import.rs @@ -200,7 +200,31 @@ pub struct ImportReport { pub sidecars: usize, /// Per-photo diagnostics from foreign XMP develop settings that could not be fully carried over. #[serde(skip_serializing_if = "Vec::is_empty")] - pub warnings: Vec, + pub warnings: Vec, +} + +/// A structured diagnostic from foreign XMP develop settings. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ImportWarningCode { + /// A named camera profile was found but cannot be applied as Lightroom does. + CameraProfile, + /// Lightroom geometry data was found but its saved transform is not retained. + UprightGeometry, + /// One or more other XMP develop fields have no LightCraft mapping. + UnmappedXmp, +} + +/// One actionable XMP import diagnostic. `path` is filled when its photo is committed. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ImportWarning { + pub path: String, + pub code: ImportWarningCode, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub fields: Vec, } /// A file moved by an import. @@ -1238,7 +1262,10 @@ pub fn commit_prepared(s: &mut Session, opts: &ImportOptions, now: &str, prepare if let Some(sc) = &sidecar { crate::sidecar::merge_into(&mut p, sc, &now); report.sidecars += 1; - report.warnings.extend(sc.warnings.iter().map(|warning| format!("{path}: {warning}"))); + report.warnings.extend(sc.warnings.iter().cloned().map(|mut warning| { + warning.path = path.clone(); + warning + })); } if let Some(mp) = opts.metadata_preset.as_ref().and_then(|n| s.metadata_presets.iter().find(|m| m.name.eq_ignore_ascii_case(n))) { crate::cmd::metadata::apply_to(&mut p.meta, &mp.fields); diff --git a/crates/engine/src/sidecar.rs b/crates/engine/src/sidecar.rs index 3cd0d132..0ad3ec16 100644 --- a/crates/engine/src/sidecar.rs +++ b/crates/engine/src/sidecar.rs @@ -106,7 +106,7 @@ pub struct SidecarData { pub develop: Option, /// Actionable diagnostics for foreign develop settings that were not fully carried over. /// These are surfaced in the photo import report and never persisted in the catalog. - pub warnings: Vec, + pub warnings: Vec, } #[derive(Clone, Debug, PartialEq)] @@ -165,30 +165,24 @@ pub fn parse_sidecar(xmp: &str, raw: bool) -> std::result::Result Some(DevelopPatch::Full(Box::new(s))), None if crate::crs::has_adjustments(&d.properties) => { - let (mut partial, unmapped) = crate::crs::to_partial_report(&d.properties, Some(&d.values), Some(raw), crate::crs_masks::DEFAULT_ASPECT); + let (partial, unmapped) = crate::crs::to_partial_report(&d.properties, Some(&d.values), Some(raw), crate::crs_masks::DEFAULT_ASPECT); let profile = crate::crs::camera_profile_name(&d.properties).map(str::to_string); - if let Some(name) = &profile - && let Some(profile) = partial.get_mut("profile").and_then(Value::as_object_mut) - { - // Keep the selected name for inspection and round trips. An unknown id renders - // as LightCraft's neutral profile; the warning below makes that limitation clear. - profile.insert("id".into(), Value::String(name.clone())); - } if let Some(name) = profile { - out.warnings.push(format!("{name} could not be applied; colors may differ from Lightroom.")); + out.warnings.push(crate::import::ImportWarning { + path: String::new(), + code: crate::import::ImportWarningCode::CameraProfile, + profile: Some(name), + 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).unwrap_or_default(); - let mode = match mode { - "1" => "Auto", - "2" => "Level", - "3" => "Vertical", - "4" => "Full", - "5" => "Guided", - _ => "Upright", - }; - out.warnings.push(format!("Lightroom geometry was not preserved; {mode} will be recalculated.")); + out.warnings.push(crate::import::ImportWarning { + path: String::new(), + code: crate::import::ImportWarningCode::UprightGeometry, + profile: None, + fields: Vec::new(), + }); } let other: Vec<&str> = unmapped .iter() @@ -200,7 +194,12 @@ pub fn parse_sidecar(xmp: &str, raw: bool) -> std::result::Result, undo0: usize, first: Option, /// Reading a folder for the Local view: the photos stay out of the library, and nothing is @@ -693,6 +695,7 @@ fn commit_batch(app: &mut LightcraftApp, task: &mut ImportTask, prepared: lightc task.failed += len("failed"); task.moved += len("moved"); task.kept += len("kept"); + task.warnings.extend(v["warnings"].as_array().into_iter().flatten().filter_map(|warning| serde_json::from_value(warning.clone()).ok())); for k in v["kept"].as_array().into_iter().flatten() { log::warn!("import: kept {}: {}", k["path"].as_str().unwrap_or(""), k["reason"].as_str().unwrap_or("")); } @@ -707,6 +710,31 @@ fn commit_batch(app: &mut LightcraftApp, task: &mut ImportTask, prepared: lightc } } +fn xmp_warning_text(warning: &ImportWarning) -> String { + let path = &warning.path; + match warning.code { + ImportWarningCode::CameraProfile => crate::i18n::tr_format!( + "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.", + 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::UnmappedXmp => { + crate::i18n::tr_format!("XMP adjustments were not imported for {path}: {fields}.", path = path, fields = warning.fields.join(", "),) + } + } +} + +fn xmp_warnings_text(warnings: &[ImportWarning]) -> String { + crate::i18n::tr_format!( + "Import warnings ({n}):\n{warnings}", + n = warnings.len(), + warnings = warnings.iter().map(xmp_warning_text).collect::>().join("\n"), + ) +} + /// The import is done (or cancelled): one undo step, select the first photo, say what happened. fn finish(app: &mut LightcraftApp, ctx: &egui::Context, task: ImportTask) { let steps = app.session.undo.len().saturating_sub(task.undo0); @@ -720,13 +748,17 @@ fn finish(app: &mut LightcraftApp, ctx: &egui::Context, task: ImportTask) { }; app.session.merge_undo(steps, &label); if task.auto { - if task.imported > 0 { + if !task.warnings.is_empty() { + app.toast_for(ctx, xmp_warnings_text(&task.warnings), 8.0); + } else if task.imported > 0 { app.toast(ctx, crate::i18n::tr_format!("Auto Import: added {} photo{}", task.imported, if task.imported == 1 { "" } else { "s" })); } return; } if task.browse { - if task.failed > 0 { + if !task.warnings.is_empty() { + app.toast_for(ctx, xmp_warnings_text(&task.warnings), 8.0); + } else if task.failed > 0 { app.toast(ctx, crate::i18n::tr_format!("{} photo{} not readable", task.failed, if task.failed == 1 { "" } else { "s" })); } return; @@ -737,7 +769,13 @@ fn finish(app: &mut LightcraftApp, ctx: &egui::Context, task: ImportTask) { let plural = |n: usize| if n == 1 { "" } else { "s" }; // nothing new, only files the library already has: show where they are (Recently Deleted is // easy to miss, and the side panel that lists it starts collapsed) - if task.imported == 0 && task.restored == 0 && task.failed == 0 && !task.cancelled && show_existing(app, ctx, &task.existing) { + if task.imported == 0 + && task.restored == 0 + && task.failed == 0 + && task.warnings.is_empty() + && !task.cancelled + && show_existing(app, ctx, &task.existing) + { return; } let mut msg = if task.cancelled { @@ -764,7 +802,13 @@ fn finish(app: &mut LightcraftApp, ctx: &egui::Context, task: ImportTask) { if task.failed > 0 { msg.push_str(&crate::i18n::tr_format!(" · {} not readable", task.failed)); } - app.toast(ctx, msg); + if task.warnings.is_empty() { + app.toast(ctx, msg); + } else { + msg.push_str("\n\n"); + msg.push_str(&xmp_warnings_text(&task.warnings)); + app.toast_for(ctx, msg, 8.0); + } } /// The photos among `ids` that are in Recently Deleted (each once). diff --git a/docs/xmp-interop.md b/docs/xmp-interop.md index ebdbb5d2..a95893e4 100644 --- a/docs/xmp-interop.md +++ b/docs/xmp-interop.md @@ -142,9 +142,9 @@ names are read (e.g. `Exposure2012`, not the older `Exposure`). Values pass through our control specs, so anything outside our slider ranges gets clamped. -`crs:CameraProfile` is retained as its selected name in the photo's profile settings, but the name carries no -rendering data and does not select an Adobe profile. Photo import reports that the profile could not be applied and -colours may differ from Lightroom. Camera profiles and looks (`Look`; we have our own profile set), local adjustments +`crs:CameraProfile` is retained as its selected name in the photo import warning metadata, while active profile +settings stay unchanged: the name carries no rendering data and does not select an Adobe profile. Photo import +reports that the profile could not be applied and colours may differ from Lightroom. Camera profiles and looks (`Look`; we have our own profile set), local adjustments (masks, gradients, brushes), spot removal, red eye, lens blur, process-version 2010 field names, and AI features. ## Local corrections (masks) From 7d3cd8afe5813849fb0cb3175f3b877b7d919f71 Mon Sep 17 00:00:00 2001 From: Adrian D'souza Date: Fri, 9 Oct 2026 08:54:49 +0530 Subject: [PATCH 03/24] XMP: preserve Upright warning modes across import paths --- crates/engine/src/cmd/library.rs | 10 ++++-- crates/engine/src/cmd/xmp.rs | 14 +++++--- crates/engine/src/import.rs | 15 ++++++++ crates/engine/src/sidecar.rs | 29 ++++++++++++++- crates/engine/src/tests_import.rs | 6 ++++ crates/engine/src/tests_xmp.rs | 36 +++++++++++++++++-- crates/ui-egui/locales/de-formats.json | 5 ++- crates/ui-egui/locales/es-formats.json | 5 ++- crates/ui-egui/locales/ja-formats.json | 5 ++- crates/ui-egui/locales/pt-br-formats.json | 5 ++- crates/ui-egui/locales/ru-formats.json | 5 ++- crates/ui-egui/locales/zh-hans-formats.json | 5 ++- crates/ui-egui/locales/zh-hant-formats.json | 5 ++- crates/ui-egui/src/import.rs | 40 ++++++++++++++++++--- 14 files changed, 164 insertions(+), 21 deletions(-) diff --git a/crates/engine/src/cmd/library.rs b/crates/engine/src/cmd/library.rs index 0eafa49a..39e81db9 100644 --- a/crates/engine/src/cmd/library.rs +++ b/crates/engine/src/cmd/library.rs @@ -987,7 +987,7 @@ pub fn specs() -> Vec { "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 { let r = s.execute("library.import", ¶ms)?; // 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 { "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)?; diff --git a/crates/engine/src/cmd/xmp.rs b/crates/engine/src/cmd/xmp.rs index b83e7275..317a87ac 100644 --- a/crates/engine/src/cmd/xmp.rs +++ b/crates/engine/src/cmd/xmp.rs @@ -35,11 +35,13 @@ fn read(s: &mut Session, p: &Value) -> Result { 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 { 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 { @@ -84,7 +90,7 @@ pub fn specs() -> Vec { "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 ), diff --git a/crates/engine/src/import.rs b/crates/engine/src/import.rs index ad0ca934..650ebbda 100644 --- a/crates/engine/src/import.rs +++ b/crates/engine/src/import.rs @@ -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, + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, #[serde(skip_serializing_if = "Vec::is_empty")] pub fields: Vec, } diff --git a/crates/engine/src/sidecar.rs b/crates/engine/src/sidecar.rs index 0ad3ec16..b2c53148 100644 --- a/crates/engine/src/sidecar.rs +++ b/crates/engine/src/sidecar.rs @@ -172,15 +172,32 @@ pub fn parse_sidecar(xmp: &str, raw: bool) -> std::result::Result 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 Result> { + 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)>> { 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). diff --git a/crates/engine/src/tests_import.rs b/crates/engine/src/tests_import.rs index 4a4cad53..40a20f67 100644 --- a/crates/engine/src/tests_import.rs +++ b/crates/engine/src/tests_import.rs @@ -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#""#, + ) + .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"); diff --git a/crates/engine/src/tests_xmp.rs b/crates/engine/src/tests_xmp.rs index 8ed9a4e2..d47647ca 100644 --- a/crates/engine/src/tests_xmp.rs +++ b/crates/engine/src/tests_xmp.rs @@ -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"); diff --git a/crates/ui-egui/locales/de-formats.json b/crates/ui-egui/locales/de-formats.json index 58bf68eb..720bf269 100644 --- a/crates/ui-egui/locales/de-formats.json +++ b/crates/ui-egui/locales/de-formats.json @@ -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}", diff --git a/crates/ui-egui/locales/es-formats.json b/crates/ui-egui/locales/es-formats.json index a28d85db..32d8d74d 100644 --- a/crates/ui-egui/locales/es-formats.json +++ b/crates/ui-egui/locales/es-formats.json @@ -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}", diff --git a/crates/ui-egui/locales/ja-formats.json b/crates/ui-egui/locales/ja-formats.json index 84f60973..5b598c6b 100644 --- a/crates/ui-egui/locales/ja-formats.json +++ b/crates/ui-egui/locales/ja-formats.json @@ -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 {})": "カーソル位置に挿入({}とも表記可)", diff --git a/crates/ui-egui/locales/pt-br-formats.json b/crates/ui-egui/locales/pt-br-formats.json index 15e1f796..a73bbbb6 100644 --- a/crates/ui-egui/locales/pt-br-formats.json +++ b/crates/ui-egui/locales/pt-br-formats.json @@ -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 {})", diff --git a/crates/ui-egui/locales/ru-formats.json b/crates/ui-egui/locales/ru-formats.json index b8be34f4..1e516c4e 100644 --- a/crates/ui-egui/locales/ru-formats.json +++ b/crates/ui-egui/locales/ru-formats.json @@ -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}", diff --git a/crates/ui-egui/locales/zh-hans-formats.json b/crates/ui-egui/locales/zh-hans-formats.json index 01dba1ab..4090e92f 100644 --- a/crates/ui-egui/locales/zh-hans-formats.json +++ b/crates/ui-egui/locales/zh-hans-formats.json @@ -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})", diff --git a/crates/ui-egui/locales/zh-hant-formats.json b/crates/ui-egui/locales/zh-hant-formats.json index ab34b129..39b8834f 100644 --- a/crates/ui-egui/locales/zh-hant-formats.json +++ b/crates/ui-egui/locales/zh-hant-formats.json @@ -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 {})": "在游標處插入(也可寫成 {})", diff --git a/crates/ui-egui/src/import.rs b/crates/ui-egui/src/import.rs index d8aceb01..40ee2ba5 100644 --- a/crates/ui-egui/src/import.rs +++ b/crates/ui-egui/src/import.rs @@ -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(", "),) } From aa31ecdc56a0ebde03c432f18361f51fd77784f3 Mon Sep 17 00:00:00 2001 From: Twdhsi-666 <340561342+Twdhsi-666@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:50:44 +0800 Subject: [PATCH 04/24] locales: complete Simplified Chinese (zh-hans) catalog --- crates/ui-egui/locales/zh-hans.json | 827 ++++++++++++++-------------- 1 file changed, 425 insertions(+), 402 deletions(-) diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index 21fa4e4e..4bc80d5a 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -4,6 +4,7 @@ " · best of its burst": " · 连拍中的最佳照片", " · in a burst": " · 属于一组连拍", "% of all commits": "占全部提交的百分比", + "& Containing": "及上级", "+ Group": "+ 组", "+ Rule": "+ 条件", ", picked": ",留用", @@ -14,15 +15,25 @@ "= exactly": "= 等于", "A blank template keeps the original names.": "模板留空则保留原始名称。", "A filter is hiding this view's photos: change it, or clear it (View → Clear Filters)": "筛选器正在隐藏此视图中的照片:请修改筛选条件,或清除筛选(视图 → 清除筛选)", + "A folder is already being synchronized": "已有文件夹正在同步", + "A folder is being synchronized": "正在同步文件夹", + "A keyword set needs a name": "关键字集需要名称", "A one-time download of about": "一次性下载,大小约", + "AI Denoise": "AI Denoise", + "AI Denoise Model": "AI Denoise Model", + "AI Denoise is enabled on the requesting photo": "已在发起请求的照片上启用 AI 降噪", + "AI Denoise is off: press Use on a model.": "AI Denoise is off: press Use on a model.", + "AI Denoise needs a model. Install one in Settings.": "AI Denoise needs a model. Install one in Settings.", "AI Mask Model Status": "AI 蒙版模型状态", "About": "关于", "About LightCraft": "关于 LightCraft", + "Accept & Download": "Accept & Download", "Active": "当前照片", "Activity": "活动", "Actual size": "实际大小", "Add": "添加", "Add Catchlight": "添加眼神光", + "Add Face Model…": "添加人脸模型…", "Add Folder to Local": "将文件夹添加到本地", "Add Mask": "添加蒙版", "Add Object Click": "添加对象点击", @@ -37,6 +48,7 @@ "Add number": "添加编号", "Add to": "添加到", "Add to Album": "添加到相册", + "Add to All Selected Photos": "添加到所有所选照片", "Add to Favorites": "添加到收藏夹", "Add to Local": "添加到本地", "Add to My Photos": "添加到我的照片", @@ -53,6 +65,7 @@ "After 7 days": "7 天后", "After 90 days": "90 天后", "After a year": "一年后", + "After installation, AI Denoise will turn on for the requesting photo.": "安装完成后,将为发起请求的照片开启 AI 降噪。", "Album": "相册", "Albums": "相册", "All": "全部", @@ -60,11 +73,14 @@ "All Metadata…": "所有元数据…", "All Photos": "所有照片", "All rights reserved": "保留所有权利", + "All the photos imported from this disk are included.": "包括从此磁盘导入的所有照片。", "All types": "所有类型", + "Already installed. Remove it before installing a replacement.": "Already installed. Remove it before installing a replacement.", "Alt Text": "替代文本", "Always": "始终", "Amount": "数量", "Analyzing the photo for AI masks…": "正在分析照片以生成 AI 蒙版…", + "Angle": "角度", "Any Searchable Text": "任意可搜索文本", "Any camera": "任意相机", "Any keyword": "任意关键字", @@ -78,6 +94,7 @@ "Apply Point Curve Preset": "应用点曲线预设", "Apply Preset": "应用预设", "Apply Settings JSON": "应用设置 JSON", + "Apply the local tone mapping stored in the raw (Apple ProRAW), as the camera renders it": "按相机的渲染方式应用 raw 中保存的局部色调映射(Apple ProRAW)", "Approved": "已批准", "Aqua": "青色", "Aqua Hue": "青色色相", @@ -116,6 +133,8 @@ "Auto-Tag from Tracklog": "从轨迹日志自动标记", "Auto-Tag from Tracklog…": "从轨迹日志自动标记…", "Automatic": "自动", + "Automatic renders the Detail view at the size it is shown, up to the photo's own pixels. Choose a size to cap it: smaller is faster.": "“自动”按详细视图的显示大小渲染(最高为照片自身像素)。选择一个大小可设上限:越小越快。", + "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.", "Automatically Write Changes into XMP": "自动将更改写入 XMP", "Automatically write changes into XMP sidecars": "自动将更改写入 XMP 附属文件", "Autumn Gold": "秋日金黄", @@ -127,6 +146,7 @@ "Back to Edit": "返回编辑", "Back to Edit (E)": "返回编辑 (E)", "Back to Grid": "返回网格视图", + "Back to this person": "Back to this person", "Background": "背景", "Balance": "平衡", "Basic": "基础", @@ -159,6 +179,7 @@ "Boundary Warp": "边界变形", "Bright & Airy": "明亮通透", "Bring All to Front": "全部置于顶层", + "Bring the library up to date with this folder and the folders inside it:": "让图库与此文件夹及其中的文件夹保持一致:", "Browse Folder": "浏览文件夹", "Browse Folder…": "浏览文件夹…", "Browse Profiles": "浏览配置文件", @@ -172,15 +193,21 @@ "By day (YYYY/YYYY-MM-DD)": "按日 (YYYY/YYYY-MM-DD)", "By month (YYYY/YYYY-MM)": "按月 (YYYY/YYYY-MM)", "CPU threads": "CPU 线程数", + "Cache": "Cache", "Calibration": "校准", "Camera": "相机", + "Camera Info": "相机信息", + "Camera local tone mapping": "相机局部色调映射", "Camera make and model": "相机品牌和型号", "Cameras and Cards": "相机和存储卡", "Cancel": "取消", "Cancel AI Mask Model Download": "取消下载 AI 蒙版模型", + "Cancel Denoise": "取消降噪", + "Cancel Denoise Model Download": "取消降噪模型下载", "Cancel Download": "取消下载", "Cancel Interaction": "取消交互", "Cancel Preview Build": "取消生成预览", + "Cancel pending action": "取消待处理操作", "Candidate": "候选", "Caption": "题注", "Capture Date": "拍摄日期", @@ -197,12 +224,16 @@ "Check All": "全部选中", "Check Output Path": "检查输出路径", "Check the files of this type": "勾选此类型的所有文件", + "Checking and installing the model…": "Checking and installing the model…", "Checking names…": "正在检查名称…", + "Checking…": "正在检查…", "Choose Another Library…": "选择其他图库…", + "Choose Denoise Model": "选择降噪模型", "Choose Edit Settings to Copy": "选择要复制的编辑设置", "Choose Edit Settings to Copy…": "选择要复制的编辑设置…", "Choose Folder…": "选择文件夹…", "Choose a photo in the grid or filmstrip": "请在网格视图或胶片窗格中选择一张照片", + "Choose an album…": "选择相册…", "Choose…": "选择…", "Cinematic": "电影感", "City": "城市", @@ -210,6 +241,7 @@ "Clean Studio": "纯净棚拍", "Clear": "清除", "Clear Cache": "清除缓存", + "Clear Denoise Cache": "清除降噪缓存", "Clear Filters": "清除过滤器", "Clear Guides": "清除参考线", "Clear History": "清除历史记录", @@ -217,6 +249,7 @@ "Clear Quick Collection": "清空快速集合", "Clear Reference": "清除参考照片", "Clear all": "全部清除", + "Clear cache": "清除缓存", "Clear form": "清除表单", "Clear rating": "清除星级", "Clear rating and advance": "清除星级并前进", @@ -230,6 +263,7 @@ "Click the photo to pick a colour; ⇧-click adds more (up to 5)": "单击照片选择颜色;按 ⇧ 单击可添加更多(最多 5 个)", "Click to clear these filters": "单击以清除这些筛选条件", "Click to restore · double-click to rename": "单击以恢复 · 双击以重命名", + "Click to type a value": "单击以输入数值", "Clone": "仿制", "Close": "关闭", "Close Panel": "关闭面板", @@ -237,18 +271,19 @@ "Close all": "全部关闭", "Close file": "关闭文件", "Cloudy": "阴天", + "Collapse": "折叠", "Collapse All Stacks": "折叠所有堆叠", "Collapse Stack": "折叠堆叠", "Color": "颜色", - "Color Label": "颜色标签", - "Color label cleared": "已清除颜色标签", "Color Grading": "颜色分级", + "Color Label": "颜色标签", "Color Label Names": "颜色标签名称", "Color Label Sets": "颜色标签集", "Color Mixer": "颜色混合器", "Color Noise Reduction": "颜色降噪", "Color Priority": "颜色优先", "Color Range": "颜色范围", + "Color label cleared": "已清除颜色标签", "Color space": "色彩空间", "Color space: sRGB (AVIF)": "色彩空间:sRGB(AVIF)", "Colors": "颜色", @@ -281,6 +316,7 @@ "Copy Edit Settings": "复制编辑设置", "Copy History Step Settings to Before": "将历史记录步骤的设置复制到修改前", "Copy Metadata": "复制元数据", + "Copy Name": "副本名称", "Copy as DNG": "拷贝为 DNG", "Copy into the library (else use the files where they are)": "拷贝到图库中(否则使用文件当前位置)", "Copy pages": "复制页面", @@ -291,11 +327,14 @@ "Copyright": "版权", "Copyright Info URL": "版权信息 URL", "Copyright Status": "版权状态", + "Copyrighted": "受版权保护", "Country": "国家/地区", + "Create": "创建", "Create Album": "创建相册", "Create Album…": "创建相册…", "Create Folder": "创建文件夹", "Create Folder…": "创建文件夹…", + "Create Keyword Tag": "创建关键字标记", "Create New Mask": "创建新蒙版", "Create PDF from clipboard": "从剪贴板创建 PDF", "Create PDF from file…": "从文件创建 PDF…", @@ -319,6 +358,7 @@ "Crop & Rotate (C)": "裁剪和旋转(C)", "Crop Aspect": "裁切长宽比", "Crop, Rotate, Geometry": "裁剪、旋转、几何", + "Cropped": "已裁剪", "Cross Process": "交叉冲洗", "Culling": "挑片", "Curve": "曲线", @@ -342,6 +382,7 @@ "Daylight": "日光", "Decrease Brush Feather": "减小画笔羽化", "Decrease Brush Size": "减小画笔尺寸", + "Decrease Rating": "降低星级", "Deep Sky": "深邃天空", "Defringe": "去边", "Deghost Amount": "去伪影量", @@ -357,6 +398,7 @@ "Delete Imported Profile": "删除导入的配置文件", "Delete Keyword": "删除关键字", "Delete Keyword Set": "删除关键字集", + "Delete Keyword…": "删除关键字…", "Delete Mask": "删除蒙版", "Delete Metadata Preset": "删除元数据预设", "Delete Permanently": "永久删除", @@ -374,6 +416,13 @@ "Delete all spots": "删除所有污点", "Delete mask": "删除蒙版", "Delete them": "删除它们", + "Denoise Model Downloads": "降噪模型下载", + "Denoise Models": "降噪模型", + "Denoise Photos": "照片降噪", + "Denoise Pump": "降噪队列", + "Denoise Settings": "降噪设置", + "Denoise Status": "降噪状态", + "Denoise all photos with an Amount": "对所有设置了数量的照片进行降噪", "Density": "密度", "Depth Range": "深度范围", "Descending": "降序", @@ -382,6 +431,7 @@ "Describe what to leave out": "描述要排除的内容", "Describe what to select": "描述要选择的内容", "Describe…": "描述…", + "Description": "描述", "Deselect": "取消选择", "Deselect All": "取消全选", "Desert Heat": "沙漠热浪", @@ -390,6 +440,8 @@ "Detail": "细节", "Detail (D)": "细节 (D)", "Details": "详细信息", + "Detect Faces": "检测人脸", + "Develop": "编辑", "Diagonal": "对角线", "Discard Preview Cache": "丢弃预览缓存", "Display P3": "Display P3", @@ -401,11 +453,16 @@ "Done": "完成", "Download": "下载", "Download AI Mask Model": "下载 AI 蒙版模型", + "Download Denoise Model": "下载降噪模型", + "Download Face Model": "下载人脸模型", "Download the SAM 3 Model?": "要下载 SAM 3 模型吗?", + "Downloaded and checked. Installing…": "已下载并校验完成。正在安装…", "Downloads": "下载", + "Drag Crop Handle": "拖动裁切手柄", "Drag along the horizon; double-click for Auto": "沿地平线拖动;双击以自动校正", "Drag over an eye on the photo; the pupil inside is found automatically.": "在照片中的眼睛上拖动;会自动找到其中的瞳孔。", "Draw Guides": "绘制参考线", + "Drop here for the top level": "拖放到此处以移至顶层", "Duplicate": "创建副本", "Duplicate Component": "复制组件", "Duplicate Mask": "复制蒙版", @@ -421,11 +478,15 @@ "Edit Copy for External Editor": "编辑外部编辑器副本", "Edit Date": "编辑日期", "Edit Info": "编辑信息", + "Edit Keyword Set": "编辑关键字集", + "Edit Keyword Tag": "编辑关键字标记", + "Edit Keyword Tag…": "编辑关键字标记…", "Edit Label Names": "编辑标签名称", "Edit Label Names…": "编辑标签名称…", "Edit Mask": "编辑蒙版", "Edit Mask Component": "编辑蒙版组件", "Edit Sections": "编辑板块", + "Edit Set…": "编辑关键字集…", "Edit Smart Album": "编辑智能相册", "Edit Smart Album…": "编辑智能相册…", "Edit Spot": "编辑污点", @@ -447,6 +508,7 @@ "Every EXIF, GPS and XMP field in the file": "文件中的每个 EXIF、GPS 和 XMP 字段", "Existing previews": "现有预览", "Exit": "退出", + "Expand": "展开", "Expand All Stacks": "展开所有堆叠", "Expand Stack": "展开堆叠", "Expand/Collapse Stack": "展开/折叠堆叠", @@ -454,12 +516,16 @@ "Export Album with Preset": "使用预设导出相册", "Export Album…": "导出相册…", "Export As…": "导出为…", + "Export Containing Keywords": "导出上级关键字", "Export Group…": "导出分组…", + "Export Keywords": "导出关键字", + "Export Keywords…": "导出关键字…", "Export Now": "立即导出", "Export Point Curve Presets": "导出点曲线预设", "Export Point Curve Presets…": "导出点曲线预设…", "Export Presets": "导出预设", "Export Presets…": "导出预设…", + "Export Synonyms": "导出同义词", "Export User Presets…": "导出用户预设…", "Export stopped unexpectedly": "导出意外停止", "Export with Preset": "使用预设导出", @@ -471,178 +537,43 @@ "Extended Description": "扩展描述", "External Editor": "外部编辑器", "Face Boxes": "人脸框", + "Face Model Downloads": "人脸模型下载", + "Face Models": "人脸模型", + "Face Recognition On/Off": "人脸识别开关", "Face boxes": "人脸框", + "Face detector": "人脸检测器", "Face names written to XMP by Lightroom and other apps show up here.": "Lightroom 及其他应用写入 XMP 的人脸名称会显示在这里。", + "Face recognition": "人脸识别", + "Faces": "人脸", + "Faces of a Person": "某个人的人脸", "Faded Matte": "褪色哑光", "Faded Slide": "褪色正片", "Faded Urban": "褪色都市", + "Failed": "失败", + "Failed its last test": "上次检测未通过", "Favorite Preset": "收藏预设", "Favorite Profile": "收藏配置文件", "Favorites": "收藏夹", "Feather": "羽化", "File": "文件", "File & date": "文件 & 日期", + "File Extension": "文件扩展名", "File Format": "文件格式", "File Name": "文件名", "File Path": "文件路径", "File Size": "文件大小", "File Type": "文件类型", + "File name": "文件名", "File types:": "文件类型:", "Filename": "文件名", "Fill": "填满", - "First": "最早", - "First commit": "首次提交", - "Five stars": "五星", - "Fixed keys": "固定按键", - "Flagged as Pick": "已标记为留用", - "Flagged as Reject": "已标记为排除", - "Focal Length": "焦距", - "Focus (assisted culling)": "对焦(辅助筛选)", - "Frame time": "帧耗时", - "Frame update": "帧更新", - "GPU rendering": "GPU 渲染", - "Generated demo photo": "生成的演示照片", - "Grab bag": "名单", - "Green label": "绿色标签", - "Group name": "分组名称", - "HDR Merge Preview": "HDR 合并预览", - "HDR Panorama Merge Preview": "HDR 全景合并预览", - "HDR Panoramas": "HDR 全景", - "HDR panorama": "HDR 全景", - "HDR panoramas": "HDR 全景", - "Has Edits": "有编辑", - "Has GPS": "有 GPS", - "Home": "主文件夹", - "Hours": "小时", - "ISO Speed": "ISO 感光度", - "ISO speed": "ISO 感光度", - "Import Lightroom Catalog": "导入 Lightroom 目录", - "Import Lightroom Catalog…": "导入 Lightroom 目录…", - "Import as new": "作为新照片导入", - "Imported": "已导入", - "Importing Lightroom Catalog": "正在导入 Lightroom 目录", - "In Photo Grid and Square Grid: Flag as Pick and advance": "在照片网格和正方形网格中:标记为留用并前进", - "In Photo Grid and Square Grid: expand or collapse the stack": "在照片网格和正方形网格中:展开或折叠堆叠", - "Info Overlay Off": "关闭信息叠加", - "Info Overlay: Exposure": "信息叠加:曝光", - "Info Overlay: File": "信息叠加:文件", - "Insert at the cursor": "在光标处插入", - "Inspect Lightroom Catalog": "检查 Lightroom 目录", - "Inspecting Lightroom Catalog": "正在检查 Lightroom 目录", - "Invert Component": "反相组件", - "Join the ArtCraft community on Discord": "加入 Discord 上的 ArtCraft 社区", - "Label": "标签", - "Label Set": "标签集", - "Last GPU fallback": "最近一次 GPU 回退", - "Last commit": "最近一次提交", - "Last loupe render": "最近一次放大视图渲染", - "Lens Blur": "镜头模糊", - "Library saved": "图库已保存", - "Licence:": "许可证:", - "LightCraft could not start": "LightCraft 无法启动", - "Lightroom Classic Catalog": "Lightroom Classic 目录", - "Lightroom import cancelled": "已取消 Lightroom 导入", - "Lightroom inspection cancelled": "已取消 Lightroom 检查", - "Line delta": "行数差", - "Linear": "线性", - "Lines +/−": "行数 +/−", - "Lines added": "新增行数", - "Lines deleted": "删除行数", - "Lock": "锁定", - "Lock Crop Aspect": "锁定裁剪长宽比", - "Locked": "已锁定", - "Mask Adjustment": "蒙版调整", - "Memory budget": "内存预算", - "Merged": "已合并", - "Merged PRs": "已合并的 PR", - "Meta's terms, not LightCraft's. Downloading it means accepting them.": "适用的是 Meta 的条款,而非 LightCraft 的条款。下载即表示接受这些条款。", - "Missing metadata ({camera}, {title}…) leaves an empty gap; a name that comes out empty keeps the original name.": "缺少的元数据({camera}、{title}…)会留空;如果生成的名称为空,则保留原始名称。", - "Model": "型号", - "Models": "模型", - "Move Down": "下移", - "Move Preset to New Group": "将预设移动到新分组", - "Move Up": "上移", - "Move in Stack": "在堆叠中移动", - "Name (A–Z)": "名称(A–Z)", - "Name of the folder the original is in": "原始文件所在文件夹的名称", - "Named People": "已命名的人物", - "Nested group": "嵌套组", - "New time": "新时间", - "No contributor data was built into this copy.": "此版本未内置贡献者数据。", - "No matching commands": "没有匹配的命令", - "No model credits were built into this copy.": "此版本未内置模型致谢信息。", - "No named people in these photos": "这些照片中没有已命名的人物", - "No named people yet": "还没有已命名的人物", - "Not Now": "以后再说", - "Object Mask": "对象蒙版", - "Object and Describe masks use SAM 3, Meta's segmentation model. It isn't part of LightCraft, and everything else works without it.": "对象蒙版和描述蒙版使用 Meta 的分割模型 SAM 3。它不属于 LightCraft,其他所有功能无需它也能使用。", - "October 2026": "2026年10月", - "Only": "仅限", - "Original extension without the dot": "不含点的原始扩展名", - "Original file name without its extension": "不含扩展名的原始文件名", - "Panorama": "全景", - "Panorama Merge Preview": "全景合并预览", - "Panoramas": "全景", - "Paste Settings": "粘贴设置", - "Paste from Previous": "粘贴上一张照片的设置", - "People": "人物", - "Person": "人物", - "Photos per bracket (0 = auto)": "每组包围曝光的照片数(0 = 自动)", - "Pick Flag": "留用旗标", - "Pictures": "图片", - "Prepare AI Masks": "准备 AI 蒙版", - "Presets and profiles": "预设和配置文件", - "Press keys…": "请按键…", - "Preview only: editing the camera's embedded JPEG": "仅预览:正在编辑相机内嵌的 JPEG", - "Proof copy created": "已创建校样副本", - "Quit Anyway": "仍然退出", - "Quit with unsaved changes?": "有未保存的更改,确定退出吗?", - "RAW decoding": "RAW 解码", - "Radial": "径向", - "Random": "随机", - "Rate ★": "评为 ★", - "Rate ★ and advance": "评为 ★ 并前进", - "Rate ★★": "评为 ★★", - "Rate ★★ and advance": "评为 ★★ 并前进", - "Rate ★★★": "评为 ★★★", - "Rate ★★★ and advance": "评为 ★★★ 并前进", - "Rate ★★★★": "评为 ★★★★", - "Rate ★★★★ and advance": "评为 ★★★★ 并前进", - "Rate ★★★★★": "评为 ★★★★★", - "Rate ★★★★★ and advance": "评为 ★★★★★ 并前进", - "Rating cleared": "已清除星级", - "Raw": "RAW", - "Read the SAM License": "阅读 SAM 许可证", - "Reading Lightroom catalog…": "正在读取 Lightroom 目录…", - "Real name": "真实姓名", - "Recent": "最近使用", - "Recent Keywords": "最近使用的关键字", - "Red label": "红色标签", - "Refine": "细化", - "Refine AI Mask Detail": "细化 AI 蒙版细节", - "Refine Edges": "细化边缘", - "Refine Saturation": "细化饱和度", - "Refining the mask's detail…": "正在细化蒙版细节…", - "Reliability": "可靠性", - "Relink Photo": "重新链接照片", - "Reload Changed Files": "重新载入已更改的文件", - "Remove Face Box": "移除人脸框", - "Remove a filter above, or choose Clear all": "请移除上方的某个筛选条件,或选择“全部清除”", - "Remove from Target Album": "从目标相册中移除", - "Remove shortcut": "移除快捷键", - "Remove this face box (undo with Edit ▸ Undo)": "移除此人脸框(可通过“编辑 → 撤销”恢复)", - "Rename Component": "重命名组件", - "Reorder Masks": "重新排序蒙版", - "Reset All": "全部重置", - "Reset All Keyboard Shortcuts": "重置所有键盘快捷键", - "Reshuffle": "重新随机排序", - "File name": "文件名", "Fill Edges": "填充边缘", "Film": "胶片", "Filmstrip": "胶片窗格", "Filmstrip (/)": "胶片窗格 (/)", "Filter": "过滤器", "Filter Bar": "过滤器栏", + "Filter Keywords": "过滤关键字", "Filter Presets": "过滤器预设", "Filter bar": "过滤器栏", "Filter fields": "过滤字段", @@ -650,16 +581,24 @@ "Find Missing Photos": "查找缺失的照片", "Find Missing Photos…": "查找丢失的照片…", "Find Similar Photos": "查找相似照片", + "Find photos added to or missing from this folder on disk, and XMP sidecars changed by other apps": "查找磁盘上此文件夹中新增或缺失的照片,以及被其他应用更改的 XMP 附属文件", "Find tools and commands…": "查找工具和命令…", "Find…": "查找…", + "First": "最早", + "First commit": "首次提交", "Fit": "适合", "Fit (F)": "适合 (F)", "Fit on Screen": "适合屏幕", "Fit to width": "适合宽度", "Fit visible": "适合可见区域", + "Five stars": "五星", + "Fix the marked rules to save this album.": "请修正标记的规则以保存此相册。", + "Fixed keys": "固定按键", "Flag": "旗标", "Flag as Pick": "标记为留用", "Flag as Reject": "标记为排除", + "Flagged as Pick": "已标记为留用", + "Flagged as Reject": "已标记为排除", "Flash": "闪光灯", "Flip H": "水平翻转", "Flip Horizontal": "水平翻转", @@ -668,7 +607,9 @@ "Flip Vertical": "垂直翻转", "Flow": "流量", "Fluorescent": "荧光灯", + "Focal Length": "焦距", "Focal length": "焦距", + "Focus (assisted culling)": "对焦(辅助筛选)", "Folder": "文件夹", "Folder name (renamed on disk; its photos follow)": "文件夹名称 (在磁盘上重命名;照片随之更新)", "Folders": "文件夹", @@ -679,6 +620,9 @@ "Form fields panel": "表单字段面板", "Format": "格式", "Forward": "前进", + "Fr": "五", + "Frame time": "帧耗时", + "Frame update": "帧更新", "Free": "自由", "Fresh & Bright": "清新明亮", "Friday": "星期五", @@ -688,7 +632,9 @@ "Full size": "完整大小", "GPS Track Log": "GPS 轨迹日志", "GPU Rendering": "GPU 渲染", + "GPU rendering": "GPU 渲染", "General": "常规", + "Generated demo photo": "生成的演示照片", "Geometry": "几何", "Get Develop Settings": "获取编辑设置", "Global Hue": "全局色相", @@ -699,8 +645,10 @@ "Golden": "黄金比例", "Golden Glow": "金色光晕", "Golden Hour": "黄金时刻", + "Grab bag": "名单", "Grain": "颗粒", "Graphic": "图形", + "Graphics card": "显卡", "Green": "绿色", "Green Amount": "绿色量", "Green Hue": "绿色色相", @@ -710,6 +658,7 @@ "Green Primary": "绿色原色", "Green Saturation": "绿色饱和度", "Green channel": "绿色通道", + "Green label": "绿色标签", "Grid": "网格视图", "Grid Info": "网格信息", "Gritty Street": "粗粝街景", @@ -720,15 +669,23 @@ "Group by Year": "按年分组", "Group by date": "按日期分组", "Group into Stack": "合并为堆叠", + "Group name": "分组名称", "Guided": "引导式", "Guided Upright": "引导式透视校正", "Guided Upright Guides": "引导式垂直校正参考线", "HDR Merge": "HDR 合并", + "HDR Merge Preview": "HDR 合并预览", "HDR Panorama Merge": "HDR 全景合并", + "HDR Panorama Merge Preview": "HDR 全景合并预览", "HDR Panorama with Last Settings": "使用上次设置合并 HDR 全景", + "HDR Panoramas": "HDR 全景", "HDR Panorama…": "HDR 全景…", + "HDR panorama": "HDR 全景", + "HDR panoramas": "HDR 全景", "HDR with Last Settings": "使用上次设置合并 HDR", "HDR…": "HDR…", + "Has Edits": "有编辑", + "Has GPS": "有 GPS", "Heal": "修复", "Heal the small, soft dark spots sensor dust leaves on smooth areas": "修复传感器灰尘在平滑区域留下的小而柔和的暗斑", "Height": "高度", @@ -749,14 +706,24 @@ "Histogram": "直方图", "History": "历史记录", "History & Activity (Y)": "历史记录与活动(Y)", + "Home": "主文件夹", "Horizontal": "水平", "Horizontal text": "横排文字", + "Hours": "小时", + "How to install the model": "如何安装模型", "Hue": "色相", "Hue Range": "色相范围", + "I have read these terms and accept them for my own use": "我已阅读这些条款,并为个人使用表示接受", + "ISO Speed": "ISO 感光度", + "ISO speed": "ISO 感光度", "If file exists": "如果文件已存在", "Image": "图像", "Import": "导入", "Import Date": "导入日期", + "Import Keywords": "导入关键字", + "Import Keywords…": "导入关键字…", + "Import Lightroom Catalog": "导入 Lightroom 目录", + "Import Lightroom Catalog…": "导入 Lightroom 目录…", "Import Photos": "导入照片", "Import Photos…": "导入照片…", "Import Point Curve Presets": "导入点曲线预设", @@ -765,23 +732,49 @@ "Import Presets…": "导入预设…", "Import Profiles": "导入配置文件", "Import Profiles & Presets…": "导入配置文件与预设…", + "Import as new": "作为新照片导入", "Import from Device": "从设备导入", "Import from Folder…": "从文件夹导入…", + "Import new photos": "导入新照片", "Import photos with File → Import Photos… (Cmd+Shift+I), or drop them here": "使用“文件 → 导入照片…”(Cmd+Shift+I)导入照片,或将照片拖放到此处", + "Imported": "已导入", "Imported Photos": "已导入的照片", "Importing": "正在导入", + "Importing Lightroom Catalog": "正在导入 Lightroom 目录", "Importing scans the source for photos; it doesn't add the folder to Local (use Local → Browse Folder… to work in a folder without importing).": "导入会扫描源中的照片,但不会将该文件夹添加到“本地”(若想在不导入的情况下处理某个文件夹,请使用“本地 → 浏览文件夹…”)。", + "In Color": "彩色", + "In Photo Grid and Square Grid: Flag as Pick and advance": "在照片网格和正方形网格中:标记为留用并前进", + "In Photo Grid and Square Grid: expand or collapse the stack": "在照片网格和正方形网格中:展开或折叠堆叠", + "In Recently Deleted": "在“最近删除”中", + "In a Stack": "在堆叠中", "In library": "在图库中", "In memory — nothing is saved": "仅存于内存 — 不会保存任何内容", "In use": "使用中", "In-memory session: these settings last until LightCraft quits.": "内存会话:这些设置仅在 LightCraft 退出前有效。", + "Include on Export": "导出时包含", "Include subfolders": "包含子文件夹", "Increase Brush Feather": "增大画笔羽化", "Increase Brush Size": "增大画笔尺寸", + "Increase Rating": "提高星级", + "Index Faces": "索引人脸", "Info": "信息", "Info (I)": "信息(I)", + "Info Overlay Off": "关闭信息叠加", + "Info Overlay: Exposure": "信息叠加:曝光", + "Info Overlay: File": "信息叠加:文件", "Info overlay": "信息叠加显示", + "Insert at the cursor": "在光标处插入", + "Inspect Denoise Model": "查看降噪模型", + "Inspect Face Model File": "查看人脸模型文件", + "Inspect Lightroom Catalog": "检查 Lightroom 目录", "Inspect Photo": "检查照片", + "Inspecting Lightroom Catalog": "正在检查 Lightroom 目录", + "Install Denoise Model": "安装降噪模型", + "Install Face Model": "安装人脸模型", + "Install a denoise model to enable AI Denoise on this photo": "安装降噪模型后即可在此照片上启用 AI 降噪", + "Install from file…": "从文件安装…", + "Install the SAM 3 Model": "安装 SAM 3 模型", + "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "请自行安装 ONNX 模型,并在其旁边放置 denoise-model.json。此版本不提供模型下载。", "Instant ’70s": "70 年代即时成像", "Interface": "界面", "Interface language": "界面语言", @@ -789,55 +782,86 @@ "Intersect with": "相交于", "Into one folder": "合并到一个文件夹", "Invert": "反相", + "Invert Component": "反相组件", "Invert Mask": "反相蒙版", + "It tests this album, so testing it back would loop": "它引用了此相册,反过来引用会形成循环", + "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "该照片的降噪结果尚未生成,而且不会自动生成(设置、AI 降噪)。", "JPEG (Large)": "JPEG(大)", "JPEG (Small)": "JPEG(小)", "Join the ArtCraft Discord": "加入 ArtCraft Discord", "Join the ArtCraft Discord…": "加入 ArtCraft Discord…", + "Join the ArtCraft community on Discord": "加入 Discord 上的 ArtCraft 社区", "Keep in Local": "保留在本地", "Keep it on a drive with room: smart previews are about 1 MB per photo. The thumbnail cache stays in the library.": "请选择空间充足的驱动器:智能预览每张照片约 1 MB。缩略图缓存仍保留在图库中。", "Keyboard Shortcuts": "键盘快捷键", "Keyboard shortcuts": "键盘快捷键", "Keyword": "关键字", + "Keyword Count": "关键字数量", + "Keyword List": "关键字列表", + "Keyword Lists": "关键字列表", + "Keyword Name": "关键字名称", "Keyword Painter": "关键字画笔", "Keyword Set": "关键字集", "Keyword Sets": "关键字集", "Keyword Suggestions": "关键字建议", + "Keyword Tag Options": "关键字标记选项", "Keyword to paint": "要涂抹的关键字", "Keywords": "关键字", + "Keywords & People": "关键字与人物", "Keywords (K)": "关键字 (K)", + "Keywords (⌥1–⌥9)": "关键字 (⌥1–⌥9)", "Keywords you add appear here; ⌥1–⌥9 apply them.": "您添加的关键字会显示在此处;按 ⌥1–⌥9 即可应用。", + "Label": "标签", + "Label Set": "标签集", "Landscape": "横向", + "Landscape (wide)": "横向", "Language": "语言", - "Automatic renders the Detail view at the size it is shown, up to the photo's own pixels. Choose a size to cap it: smaller is faster.": "“自动”按详细视图的显示大小渲染(最高为照片自身像素)。选择一个大小可设上限:越小越快。", "Last": "上次", + "Last GPU fallback": "最近一次 GPU 回退", + "Last commit": "最近一次提交", + "Last loupe render": "最近一次放大视图渲染", "Last view": "上次视图", "Lateral Chromatic Aberration": "横向色差", "Layer": "图层", "Layers": "图层", "Leave them": "保留", "Lens": "镜头", + "Lens Blur": "镜头模糊", "Lens Corrections": "镜头校正", "Level": "水平", "Library": "图库", + "Library Folders": "图库文件夹", "Library Info": "图库信息", "Library Originals": "图库原始文件", "Library Preferences": "图库偏好设置", "Library State": "图库状态", + "Library saved": "图库已保存", + "Licence:": "许可证:", "Light": "亮度", "LightCraft Default": "LightCraft 默认", "LightCraft Help": "LightCraft 帮助", "LightCraft Preset": "LightCraft 预设", "LightCraft Presets": "LightCraft 预设", "LightCraft Website": "LightCraft 网站", + "LightCraft could not start": "LightCraft 无法启动", "LightCraft couldn't find where to keep your library.": "LightCraft 找不到保存图库的位置。", "LightCraft couldn't open the library at": "LightCraft 无法打开以下位置的图库:", + "LightCraft does not know this model, so it assumed:": "LightCraft 不认识此模型,因此按以下假设处理:", "LightCraft on GitHub": "GitHub 上的 LightCraft", "LightCraft website": "LightCraft 网站", "LightCraft — Second Window": "LightCraft — 第二窗口", + "Lightroom Classic Catalog": "Lightroom Classic 目录", + "Lightroom import cancelled": "已取消 Lightroom 导入", + "Lightroom inspection cancelled": "已取消 Lightroom 检查", "Lights": "高光", + "Limit": "上限", "Limit file size (KB, 0 = off)": "限制文件大小(KB,0 = 关闭)", + "Line delta": "行数差", + "Linear": "线性", "Linear Gradient": "线性渐变", + "Lines +/−": "行数 +/−", + "Lines added": "新增行数", + "Lines deleted": "删除行数", "List Albums": "列出相册", "List Develop Controls": "列出编辑控制项", "List History": "列出历史记录", @@ -850,6 +874,9 @@ "Locate Missing File…": "定位缺失的文件…", "Locate Photo": "定位照片", "Location": "位置", + "Lock": "锁定", + "Lock Crop Aspect": "锁定裁剪长宽比", + "Locked": "已锁定", "Long Edge": "长边", "Looking for photos…": "正在查找照片…", "Lossless": "无损", @@ -862,8 +889,11 @@ "Magenta Luminance": "品红色明度", "Magenta Saturation": "品红色饱和度", "Make Candidate the Select": "将候选设为选择", + "Make it now": "立即生成", + "Make the denoised picture of the photos I am looking at": "为我正在查看的照片生成降噪结果", "Manual": "手动", "Marquee zoom": "框选缩放", + "Mask Adjustment": "蒙版调整", "Masking": "蒙版", "Masking (M)": "蒙版 (M)", "Masks": "蒙版", @@ -876,16 +906,21 @@ "Megapixels": "百万像素", "Memory Budget": "内存预算", "Memory Usage": "内存用量", + "Memory budget": "内存预算", "Memory for caches": "缓存内存", "Menu": "菜单", "Merge": "合并", "Merge Keywords": "合并关键字", "Merge into…": "合并到…", + "Merged": "已合并", + "Merged PRs": "已合并的 PR", "Merging…": "正在合并…", + "Meta's terms, not LightCraft's. Downloading it means accepting them.": "适用的是 Meta 的条款,而非 LightCraft 的条款。下载即表示接受这些条款。", "Metadata": "元数据", "Metadata Preset": "元数据预设", "Metadata Presets": "元数据预设", "Metadata preset": "元数据预设", + "Metadata read": "已读取元数据", "Midpoint": "中点", "Midtones": "中间调", "Midtones Hue": "中间调色相", @@ -894,8 +929,13 @@ "Midtones split": "中间调分割点", "Minimize": "最小化", "Missing Photos": "缺失的照片", + "Missing metadata ({camera}, {title}…) leaves an empty gap; a name that comes out empty keeps the original name.": "缺少的元数据({camera}、{title}…)会留空;如果生成的名称为空,则保留原始名称。", + "Missing photos move to Recently Deleted and can be restored; no file on disk is touched.": "缺失的照片会移到“最近删除”,可以恢复;磁盘上的文件不会被改动。", "Misty Morning": "薄雾清晨", + "Mo": "一", "Mode": "模式", + "Model": "型号", + "Models": "模型", "Modified Date": "修改日期", "Monday": "星期一", "Mono Red Filter": "黑白红色滤镜", @@ -909,28 +949,40 @@ "More preset options": "更多预设选项", "Move": "移动", "Move Album": "移动相册", + "Move Down": "下移", "Move Down in Stack": "在堆叠中下移", "Move Folder": "移动文件夹", "Move Folder To…": "将文件夹移动到…", "Move Mask": "移动蒙版", "Move Preset to Group": "将预设移动到分组", + "Move Preset to New Group": "将预设移动到新分组", + "Move Up": "上移", "Move Up in Stack": "在堆叠中上移", + "Move in Stack": "在堆叠中移动", "Move the files (into the library's Originals/, or a folder you choose), removing them from the source": "移动文件 (移到图库的 Originals/,或你选择的文件夹),并从源位置移除", "Move them": "移动它们", "Move to": "移动到", "Move to Group": "移动到组", "Move: the files are moved to the folder below; each original (and its XMP sidecar) is removed from the source only after its copy is verified. Duplicates and files that fail stay where they are.": "移动:文件将移动到下面的文件夹;每个原始文件 (及其 XMP 附属文件) 仅在副本校验通过后才会从源位置移除。重复的文件和失败的文件保留在原处。", + "Moves every photo imported from this disk to Recently Deleted; no file is touched": "将从此磁盘导入的所有照片移到“最近删除”;不会改动任何文件", + "Moves the photos imported from this folder to Recently Deleted; no file is touched": "将从此文件夹导入的照片移到“最近删除”;不会改动任何文件", "Muted": "低饱和", "Muted Film": "低饱和胶片", "My Photos": "我的照片", "My Photos Panel": "我的照片面板", "Name": "名称", + "Name (A–Z)": "名称(A–Z)", + "Name Face": "命名人脸", + "Name Faces": "命名人脸", + "Name of the folder the original is in": "原始文件所在文件夹的名称", "Named": "已命名", + "Named People": "已命名的人物", "Names appear in the label menu, the filter bar and the Info panel, and are written to XMP. Empty = the colour's name.": "名称会显示在标签菜单、筛选栏和信息面板中,并写入 XMP。留空 = 颜色的名称。", "Navigator": "导航器", "Needs Work": "待处理", "Neon Dusk": "霓虹暮色", "Neon Night": "霓虹之夜", + "Nested group": "嵌套组", "Neutral": "中性", "Never": "从不", "New": "新建", @@ -945,26 +997,40 @@ "New album…": "新建相册…", "New blank PDF": "新建空白 PDF", "New name": "新名称", + "New time": "新时间", "New…": "新建…", "Next Photo": "下一张照片", + "Next up for AI Denoise…": "接下来的 AI 降噪任务…", "Night Blue": "夜色蓝调", "No": "否", "No Camera or Card Found": "未找到相机或存储卡", "No Date Groups": "不按日期分组", + "No Label": "无标签", "No Stars": "无星级", "No Tool": "无工具", + "No albums match": "没有匹配的相册", + "No albums yet": "还没有相册", "No automatic versions.": "没有自动版本。", "No camera": "无相机信息", + "No contributor data was built into this copy.": "此版本未内置贡献者数据。", "No date": "无日期", "No dust spots found": "未找到污点", "No edits yet.": "尚未编辑。", "No exposure information": "无曝光信息", "No filter presets yet": "暂无筛选器预设", + "No keywords are exported": "没有要导出的关键字", + "No keywords match": "没有匹配的关键字", + "No keywords yet": "还没有关键字", "No lens data embedded in this file (DNG lens opcodes only).": "此文件中未嵌入镜头数据 (仅支持 DNG 镜头操作码)。", "No masks yet. Choose a mask type above.": "尚无蒙版。请在上方选择蒙版类型。", + "No matching commands": "没有匹配的命令", "No matching photos": "没有匹配的照片", "No metadata found": "未找到元数据", + "No model credits were built into this copy.": "此版本未内置模型致谢信息。", + "No named people in these photos": "这些照片中没有已命名的人物", + "No named people yet": "还没有已命名的人物", "No new presets": "没有新预设", + "No photo has it; the keywords below it go too.": "没有照片使用它;其下的关键字也会一并删除。", "No photo selected": "未选择照片", "No photos": "没有照片", "No photos found": "未找到照片", @@ -979,21 +1045,34 @@ "Noise Reduction": "降噪", "Non-raw photos": "非 RAW 照片", "None": "无", + "Not Now": "以后再说", + "Not saved": "未保存", + "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "在你接受之前不会下载任何内容。模型仅保存在本机;LightCraft 绝不会上传或分享它。", "Nothing to compare": "没有可比较的照片", "Nothing to survey": "没有可筛选的照片", "Nudge Develop Value": "微调编辑数值", "OK": "确定", + "ONNX model": "ONNX 模型", "Object": "对象", + "Object Mask": "对象蒙版", + "Object and Describe masks use SAM 3, Meta's segmentation model. It isn't part of LightCraft, and everything else works without it.": "对象蒙版和描述蒙版使用 Meta 的分割模型 SAM 3。它不属于 LightCraft,其他所有功能无需它也能使用。", + "October 2026": "2026年10月", "Off": "关闭", "Offset X": "水平偏移", "Offset Y": "垂直偏移", + "Only": "仅限", + "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "只对 AI 降噪数量大于 0 的照片生成结果。导出时若缺少结果则会先生成。", "Opacity": "不透明度", "Opacity (%)": "不透明度(%)", "Open Library": "打开图库", "Open Library…": "打开图库…", + "Open Log Folder": "打开日志文件夹", "Open in": "打开方式", "Open in Detail": "在放大视图中打开", + "Open page": "打开页面", + "Open the model's page": "打开该模型的页面", "Open the place in OpenStreetMap": "在 OpenStreetMap 中打开该地点", + "Opens the model's own page in your browser, to read about it or get the file yourself.": "在浏览器中打开该模型的主页,以便查阅说明或自行获取文件。", "Open…": "打开…", "Optics": "镜头校正", "Optimize Library": "优化图库", @@ -1006,6 +1085,8 @@ "Orientation": "方向", "Original": "原始比例", "Original + Settings": "原始文件 + 设置", + "Original extension without the dot": "不含点的原始扩展名", + "Original file name without its extension": "不含扩展名的原始文件名", "Original files": "原始文件", "Original offline · editing the smart preview": "原图离线 · 正在编辑智能预览", "Original offline · no smart preview": "原图离线 · 无智能预览", @@ -1018,8 +1099,11 @@ "Paint only areas like the one under the brush": "仅绘制与画笔下相似的区域", "Paint over a distraction on the photo to remove it.": "在照片上要移除的干扰物上涂抹。", "Paint overlay": "蒙版叠加显示", + "Panorama": "全景", "Panorama Merge": "全景合并", + "Panorama Merge Preview": "全景合并预览", "Panorama with Last Settings": "使用上次设置合并全景", + "Panoramas": "全景", "Panorama…": "全景…", "Parametric curve": "参数曲线", "Paste": "粘贴", @@ -1027,12 +1111,19 @@ "Paste Metadata": "粘贴元数据", "Paste Selected Settings": "粘贴所选设置", "Paste Selected Settings…": "粘贴所选设置…", + "Paste Settings": "粘贴设置", "Paste Settings from Previous": "粘贴上一张照片的设置", + "Paste from Previous": "粘贴上一张照片的设置", "Paste pages": "粘贴页面", "Pastel Haze": "粉彩薄雾", + "Pending photo action cancelled": "已取消待处理的照片操作", + "Pending photo action cancelled because its library or photo changed": "待处理的照片操作已取消,因为其所属图库或照片发生了变化", + "People": "人物", + "People Count": "人物数量", "Percent": "百分比", "Percentage": "百分比", "Performance": "性能", + "Person": "人物", "Perspective": "透视", "Pet Eye": "宠物眼", "Pet Eye Catchlight": "宠物眼神光", @@ -1042,36 +1133,46 @@ "Photo Merge": "照片合并", "Photo ▸ Edit in External Editor (⇧⌘E) renders a 16-bit TIFF copy, stacks it with the original and opens it here (an app name on macOS, a program path elsewhere).": "“照片 » 在外部编辑器中编辑”(⇧⌘E)会渲染一份 16 位 TIFF 副本,与原图堆叠并在此打开(macOS 上填应用名称,其他平台填程序路径)。", "Photos": "照片", + "Photos per bracket (0 = auto)": "每组包围曝光的照片数(0 = 自动)", "Photos seen in Local but never added or changed leave the catalog when their folder hasn't been browsed for this long (checked when the library opens). Files stay on disk; browsing the folder shows them again.": "仅在“本地”中查看过、从未添加或更改的照片,若其文件夹在此时长内未被浏览,将移出目录(打开图库时检查)。文件保留在磁盘上;浏览该文件夹即可再次显示。", "Photos that arrive in this folder (tethering, a scanner, a sync app) are added as soon as they're complete.": "进入此文件夹的照片(联机拍摄、扫描仪、同步应用)在传输完成后即被添加。", "Pick": "留用", + "Pick Flag": "留用旗标", "Pick a colour on the photo with the eyedropper.": "用吸管在照片上取色。", + "Pick a date": "选择日期", "Pick the sharpest photo of each burst": "为每组连拍选取最清晰的照片", "Picked": "已留用", "Picks": "留用", + "Pictures": "图片", "Pixels": "像素", "Point Color": "点颜色", "Point Curve": "点曲线", "Point Curve Presets": "点曲线预设", "Point curve": "点曲线", "Portrait": "人像", + "Portrait (tall)": "纵向", "Position": "位置", "Preferences": "首选项", "Preferences…": "首选项…", + "Prepare AI Masks": "准备 AI 蒙版", "Preset": "预设", "Preset name": "预设名称", "Presets": "预设", "Presets & Profiles": "预设和配置文件", "Presets (Shift+P)": "预设 (Shift+P)", + "Presets and profiles": "预设和配置文件", "Presets ▾": "预设 ⌄", + "Press keys…": "请按键…", "Preview": "预览", "Preview Build Progress": "预览生成进度", "Preview only": "仅预览", + "Preview only: editing the camera's embedded JPEG": "仅预览:正在编辑相机内嵌的 JPEG", "Preview size": "预览大小", "Previews": "预览", "Previous Photo": "上一张照片", "Print": "打印", "Print…": "打印…", + "Processor": "处理器", "Profile": "配置文件", "Profile Browser": "配置文件浏览器", "Profile Distortion": "配置文件扭曲", @@ -1080,9 +1181,12 @@ "Profile:": "配置文件:", "Profiles": "配置文件", "Projection": "投影", + "Proof copy created": "已创建校样副本", "Properties": "属性", + "Public Domain": "公共领域", "Public domain": "公有领域", "Pupil Size": "瞳孔大小", + "Purge Unused Keywords": "清除未使用的关键字", "Purple": "紫色", "Purple Amount": "紫色量", "Purple Hue": "紫色色相", @@ -1090,6 +1194,7 @@ "Purple Hue Low": "紫色色相下限", "Purple Luminance": "紫色亮度", "Purple Saturation": "紫色饱和度", + "Put New Keywords Inside This Keyword": "将新关键字放入此关键字中", "Put the locations you removed from Local back (no files change)": "恢复你从本地移除的位置 (不更改文件)", "Quality": "品质", "Query Photos": "查询照片", @@ -1097,23 +1202,49 @@ "Quick Develop": "快速编辑", "Quiet Green": "静谧绿", "Quit": "退出", + "Quit Anyway": "仍然退出", "Quit LightCraft": "退出 LightCraft", + "Quit with unsaved changes?": "有未保存的更改,确定退出吗?", + "RAW decoding": "RAW 解码", + "Radial": "径向", "Radial Gradient": "径向渐变", "Radius": "半径", + "Random": "随机", "Range": "范围", + "Rate ★": "评为 ★", + "Rate ★ and advance": "评为 ★ 并前进", + "Rate ★★": "评为 ★★", + "Rate ★★ and advance": "评为 ★★ 并前进", + "Rate ★★★": "评为 ★★★", + "Rate ★★★ and advance": "评为 ★★★ 并前进", + "Rate ★★★★": "评为 ★★★★", + "Rate ★★★★ and advance": "评为 ★★★★ 并前进", + "Rate ★★★★★": "评为 ★★★★★", + "Rate ★★★★★ and advance": "评为 ★★★★★ 并前进", "Rating": "星级", + "Rating cleared": "已清除星级", "Ratings & flags": "星级和旗标", + "Raw": "RAW", "Raw default": "RAW 默认值", "Raw defaults": "RAW 默认值", "Raw files": "RAW 文件", "Raw photos": "RAW 照片", "Raw photos as DNG, with the edits embedded. Size, color and output options don't apply.": "将 RAW 照片导出为内嵌编辑设置的 DNG。大小、颜色和输出选项不适用。", "Read Metadata from File": "从文件读取元数据", + "Read metadata updates from XMP sidecars": "从 XMP 附属文件读取元数据更新", "Read mode": "阅读模式", + "Read the SAM License": "阅读 SAM 许可证", "Reading": "正在读取", + "Reading Lightroom catalog…": "正在读取 Lightroom 目录…", + "Reading a sidecar replaces the photo's rating, labels, metadata and edits with what it says.": "读取附属文件会用其内容替换照片的评级、标签、元数据和编辑。", "Reading folder…": "正在读取文件夹…", + "Real name": "真实姓名", + "Recent": "最近使用", + "Recent Keywords": "最近使用的关键字", + "Recent Keywords is built in: choose another name": "“最近使用的关键字”为内置集:请另选名称", "Recently Added": "最近添加", "Recently Deleted": "最近删除", + "Recognition models": "识别模型", "Red": "红", "Red Eye": "红眼", "Red Hue": "红色色相", @@ -1121,38 +1252,61 @@ "Red Primary": "红色原色", "Red Saturation": "红色饱和度", "Red channel": "红色通道", + "Red label": "红色标签", "Red/Cyan Fringe": "红/青边缘", "Redo": "重做", "Reference": "参考", "Reference View": "参考视图", "Reference the files where they are": "在原地引用文件", + "Refine": "细化", + "Refine AI Mask Detail": "细化 AI 蒙版细节", + "Refine Edges": "细化边缘", "Refine Mask Edges": "细化蒙版边缘", + "Refine Saturation": "细化饱和度", + "Refining the mask's detail…": "正在细化蒙版细节…", "Refresh Source": "刷新源", "Refresh Source (/)": "刷新源 (/)", "Reject": "排除", "Reject below focus": "排除对焦低于此值的照片", "Rejected": "已排除", "Rejects": "排除的照片", + "Reliability": "可靠性", + "Relink Photo": "重新链接照片", + "Relink photos whose file was renamed or moved": "重新链接文件被重命名或移动的照片", + "Relinked": "已重新链接", + "Reload Changed Files": "重新载入已更改的文件", "Reload from Disk": "从磁盘重新读取", "Remove": "移除", "Remove (H)": "移除 (H)", "Remove CA": "移除色差", "Remove Chromatic Aberration": "移除色差", - "Camera local tone mapping": "相机局部色调映射", - "Apply the local tone mapping stored in the raw (Apple ProRAW), as the camera renders it": "按相机的渲染方式应用 raw 中保存的局部色调映射(Apple ProRAW)", + "Remove Denoise Model": "移除降噪模型", + "Remove Disk from Library": "从图库中移除磁盘", + "Remove Disk from Library…": "从图库中移除磁盘…", + "Remove Face Box": "移除人脸框", + "Remove Face Model": "移除人脸模型", + "Remove Folder from Library": "从图库中移除文件夹", + "Remove a filter above, or choose Clear all": "请移除上方的某个筛选条件,或选择“全部清除”", "Remove from Album": "从相册中移除", "Remove from Favorites": "从收藏夹中移除", + "Remove from Library…": "从图库中移除…", "Remove from Local": "从本地移除", "Remove from Photo": "从照片中移除", "Remove from Selected Photos": "从所选照片中移除", "Remove from Stack": "从堆叠中移出", + "Remove from Target Album": "从目标相册中移除", "Remove from survey": "从筛选视图中移除", "Remove location info": "移除位置信息", + "Remove missing photos from the library": "从图库中移除缺失的照片", + "Remove shortcut": "移除快捷键", + "Remove this face box (undo with Edit ▸ Undo)": "移除此人脸框(可通过“编辑 → 撤销”恢复)", "Remove this filter": "移除此筛选器", "Remove this group": "删除此组", "Remove this rule": "删除此规则", + "Removed": "已移除", "Rename": "重命名", "Rename Album": "重命名相册", + "Rename Component": "重命名组件", "Rename Folder": "重命名文件夹", "Rename Folder…": "重命名文件夹…", "Rename Keyword": "重命名关键字", @@ -1167,8 +1321,12 @@ "Rename…": "重命名…", "Rendering": "正在渲染", "Rendering…": "正在渲染…", + "Reorder Album": "重排相册", + "Reorder Masks": "重新排序蒙版", "Reset": "重置", + "Reset All": "全部重置", "Reset All Curves": "重置所有曲线", + "Reset All Keyboard Shortcuts": "重置所有键盘快捷键", "Reset Before to Import State": "将修改前重置为导入时的状态", "Reset Control": "重置控件", "Reset Crop": "重置裁剪", @@ -1178,6 +1336,7 @@ "Reset Tone Curve": "重置色调曲线", "Reset all curves: point curves (all channels) and parametric (double-click a channel to reset only that one)": "重置所有曲线:点曲线(所有通道)和参数曲线(双击某个通道可单独重置该通道)", "Reset all edits (Cmd+Shift+R)": "重置所有编辑 (Cmd+Shift+R)", + "Reshuffle": "重新随机排序", "Resize Face Box": "调整人脸框大小", "Resize to": "调整大小为", "Resolution (ppi)": "分辨率 (ppi)", @@ -1204,6 +1363,7 @@ "Roundness": "圆度", "Rules": "规则", "SAM 3 download stopped: it resumes where it left off next time.": "SAM 3 下载已停止:下次将从中断处继续。", + "Sa": "六", "Same as raw default": "与 RAW 默认设置相同", "Sample Mask Color": "取样蒙版颜色", "Sample a colour on the photo": "在照片上取样颜色", @@ -1226,6 +1386,7 @@ "Save as Preset": "保存为预设", "Save as Set…": "存储为标签集…", "Save as Smart Album…": "保存为智能相册…", + "Save as a new set": "另存为新关键字集", "Save as set": "保存为集", "Save as…": "另存为…", "Save preset": "保存预设", @@ -1233,12 +1394,14 @@ "Scale": "缩放", "Scan failed": "扫描失败", "Scanning": "正在扫描", + "Scanning…": "正在扫描…", "Scores stay on the photos: filter or make smart albums with Focus and Best of Similar Shots.": "评分会保留在照片上:可使用“对焦”和“相似照片中的最佳”进行筛选或创建智能相册。", "Screen": "屏幕", "Scroll menu down": "向下滚动菜单", "Scroll menu up": "向上滚动菜单", "Search": "搜索", "Search Photos": "搜索照片", + "Search albums": "搜索相册", "Search commands or keys": "搜索命令或按键", "Search…": "搜索…", "Seasons": "四季", @@ -1252,12 +1415,14 @@ "Select a photo to edit": "选择要编辑的照片", "Select by": "选择方式", "Select by Flag, Rating or Label": "按旗标、星级或颜色标签选择", + "Select photos to give them keywords.": "选择照片以为其添加关键字。", "Select photos, then choose View → Survey (N)": "选择照片,然后选择 视图 → 筛选 (N)", "Select two photos, then choose View → Compare (Shift+C)": "选择两张照片,然后选择 视图 → 比较 (Shift+C)", "Selected": "已选择", "Selecting…": "正在选择…", "Selenium Tone": "硒调色", "Send Feedback…": "发送反馈…", + "Separated by commas": "用逗号分隔", "Sepia Tone": "棕褐色调", "Sequence number zero-padded to N digits (1–9)": "补零至 N 位的序号(1–9)", "Sequence number, counting from the start number": "从起始编号开始计数的序号", @@ -1285,6 +1450,7 @@ "Set date & time": "设置日期和时间", "Set name": "关键字集名称", "Set the mix from the photo's colours": "根据照片颜色设置混合", + "Set up AI Denoise…": "设置 AI 降噪…", "Settings": "设置", "Settings file problem": "设置文件有问题", "Settings new raw photos start from. Changing them doesn't touch photos already in the library.": "新 RAW 照片的初始设置。更改这些设置不会影响图库中已有的照片。", @@ -1315,6 +1481,7 @@ "Show Mask Overlay": "显示蒙版叠加", "Show Mask Pins": "显示蒙版图钉", "Show Original": "显示原图", + "Show Person": "显示人物", "Show Photo Counts": "显示照片数量", "Show Photos with Keyword": "显示包含该关键字的照片", "Show Pins": "显示图钉", @@ -1334,6 +1501,7 @@ "Show the template tags; click one to insert it": "显示模板标签;单击即可插入", "Show/Hide Mask": "显示/隐藏蒙版", "Show/hide My Photos panel": "显示/隐藏我的照片面板", + "Shutter Speed": "快门速度", "Shutter speed": "快门速度", "Side panels": "侧面板", "Sidecar names": "附属文件名", @@ -1341,6 +1509,8 @@ "Size (% of short edge)": "大小 (占短边 %)", "Size limit": "大小限制", "Skip": "跳过", + "Skipped: files already in the library elsewhere": "已跳过:已在图库其他位置的文件", + "Skipped: files that can't be read": "已跳过:无法读取的文件", "Sky": "天空", "Slideshow": "幻灯片播放", "Slideshow paused": "幻灯片播放已暂停", @@ -1358,14 +1528,20 @@ "Soft Pastel": "柔和粉彩", "Soft Proofing": "软打样", "Soft Skin": "柔和肤质", + "Some rules need fixing (Edit Smart Album…):": "有些规则需要修正(编辑智能相册…):", "Sort": "排序", + "Sort Albums A–Z": "相册按 A–Z 排序", + "Sort Albums by Name": "相册按名称排序", + "Sort Contents A–Z": "内容按 A–Z 排序", "Sort by": "排序方式", "Source": "来源", "Source code on GitHub": "GitHub 上的源代码", + "Speed": "速度", "Spherical": "球面", "Spiral": "螺旋", "Split Stack": "拆分堆叠", "Spring Fresh": "春日清新", + "Square": "正方形", "Square Grid": "正方形网格", "Square Grid (G toggles)": "正方形网格(按 G 切换)", "Square Grid: show file names and formats": "正方形网格:显示文件名和格式", @@ -1377,21 +1553,26 @@ "Start at": "起始编号", "Start number": "起始编号", "Starting": "正在开始", + "Starting…": "正在启动…", "Startup": "启动", "State / Province": "省 / 州", "Stop": "停止", "Stop Building Previews": "停止构建预览", "Stop Using as Target Album": "不再用作目标相册", + "Stopped": "已停止", + "Stopped by an error (see the log)": "因错误而停止(见日志)", "Stopping…": "正在停止…", "Straighten": "拉直", "Straighten Tool": "拉直工具", "Strong Contrast": "强烈对比度", "Style": "样式", + "Su": "日", "Subfolder": "子文件夹", "Subfolder name": "子文件夹名称", "Subject": "主题", "Subtract": "减去", "Subtract from this mask": "从此蒙版中减去", + "Suggest Names for Faces": "为人脸建议名称", "Suggestions": "建议", "Summer Haze": "夏日薄雾", "Sunday": "星期日", @@ -1404,6 +1585,11 @@ "Swap Compare Photos": "交换对比照片", "Switch light / dark theme": "切换浅色/深色主题", "Sync Settings": "同步设置", + "Synchronize": "同步", + "Synchronize Folder": "同步文件夹", + "Synchronize Folder…": "同步文件夹…", + "Synchronizing…": "正在同步…", + "Synonyms": "同义词", "System": "系统", "System Info": "系统信息", "System Info…": "系统信息…", @@ -1421,24 +1607,46 @@ "Temperature": "色温", "Template": "模板", "Temporary session — your library isn't open, and nothing you do here is saved.": "临时会话 — 图库未打开,此处所做的任何操作都不会保存。", + "Test Denoise Model": "测试降噪模型", + "Test Face Model": "测试人脸模型", + "Tested and works on this computer": "已在本机测试通过并可正常使用", "Text": "文字", "Text direction": "文字方向", "Texture": "纹理", + "Th": "四", + "That includes the photos in the folders inside it.": "其中文件夹里的照片也包括在内。", "That key can't be used as a shortcut": "该按键不能用作快捷键", "The SAM 3 model is installed: Object and Describe masks are ready.": "SAM 3 模型已安装:对象蒙版和描述蒙版可以使用了。", + "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "降噪模型已安装并正在使用:细节面板中的 AI 降噪数量滑块现已可用", "The download didn't work:": "下载失败:", + "The graphics card is set up when the next photo is made.": "将在生成下一张照片时初始化显卡。", + "The library is up to date with this folder.": "图库与此文件夹一致。", + "The model is kept on this computer only. LightCraft never uploads or shares it.": "模型仅保存在本机。LightCraft 绝不会上传或分享它。", "The number at the end of the original name (IMG_0042 → 0042)": "原始名称末尾的数字(IMG_0042 → 0042)", "The original extension is always added; {ext} only puts it inside the name as well.": "始终会添加原始扩展名;{ext} 只是额外把它放进名称中。", "The original files, unchanged, each with an XMP sidecar holding its edits.": "保留原始文件不变,每个文件附带一个保存其编辑内容的 XMP 附属文件。", "The photo has these settings": "此照片具有这些设置", + "The processor does the work.": "由处理器完成运算。", + "Their photos and the keywords below them come together under one keyword.": "它们的照片和下级关键字将合并到同一个关键字下。", "Theme": "主题", + "There is no model file to install": "没有可安装的模型文件", "They can be restored from Recently Deleted until it is emptied.": "在“最近删除”被清空之前,都可以从那里恢复。", + "They move to Recently Deleted and can be restored; no file on disk is touched.": "它们会移到“最近删除”,可以恢复;磁盘上的文件不会被改动。", "Thirds": "三分法", + "This Computer": "此电脑", + "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚无法下载模型。如需手动安装,请将 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的文件夹:放入后即可使用“对象”和“描述”。", + "This build cannot run denoise models.": "此版本无法运行降噪模型。", + "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "此版本没有存放降噪模型的位置,因此无法使用 AI 降噪。(桌面版可以。)", + "This file cannot be used yet": "此文件暂时无法使用", "This folder is not available (drive disconnected?). Smart previews are not built or used until it is back or you choose another folder.": "此文件夹不可用(驱动器已断开连接?)。在它恢复或您选择其他文件夹之前,不会构建或使用智能预览。", + "This folder isn't there: it was moved or renamed, or its disk isn't connected.": "此文件夹不存在:它已被移动或重命名,或其磁盘未连接。", + "This group is empty: add a rule or remove it.": "此组为空:请添加规则或将其删除。", + "This is the album you're editing": "这是正在编辑的相册", "This photo can't be opened": "无法打开此照片", "Threshold": "阈值", "Thumbnail cache": "缩略图缓存", "Thursday": "星期四", + "Tick the box to accept the model's terms first": "请先勾选以接受该模型的条款", "Time zone": "时区", "Tint": "色调", "Title": "标题", @@ -1455,9 +1663,13 @@ "Triangle": "三角形", "Try Again": "重试", "Try Saving Again": "再次尝试保存", + "Try again": "重试", + "Try the graphics card again": "重新尝试使用显卡", + "Tu": "二", "Tuesday": "星期二", "Tungsten": "钨丝灯", "Turn Off": "关闭", + "Turn off": "关闭", "Turn on Include subfolders, or pick another folder under Local": "开启“包括子文件夹”,或在“本地”下选择其他文件夹", "Type": "类型", "Uncheck All": "全部取消选中", @@ -1468,8 +1680,11 @@ "Unflag": "取消旗标", "Unflagged": "无旗标", "Ungroup Stack": "取消堆叠", + "Unknown": "未知", "Unknown Date": "未知日期", + "Unknown licence": "许可证未知", "Unknown tags stay as typed — check the preview for a {typo}.": "未知标签会按原样保留 — 请在预览中检查是否有{typo}。", + "Unnamed Faces": "未命名人脸", "Unrated": "未评级", "Unreadable": "无法读取", "Update": "更新", @@ -1482,6 +1697,7 @@ "Updated the edits saved in the external editor": "已更新在外部编辑器中保存的编辑", "Upright": "垂直校正", "Urban": "都市", + "Use": "使用", "Use Keyword Set": "使用关键字集", "Use Library Folder": "使用图库文件夹", "Use camera-specific defaults": "使用相机专属默认值", @@ -1497,6 +1713,8 @@ "Vertical": "垂直", "Vertical text": "竖排文字", "Vibrance": "鲜艳度", + "Video": "视频", + "Video Duration": "视频时长", "Videos": "视频", "View": "视图", "Viewing and sharing": "查看与分享", @@ -1510,12 +1728,15 @@ "Visualize range": "显示范围", "Vivid": "鲜艳", "Vivid Pop": "明快跳色", + "Wait for the import to finish": "请等待导入完成", + "Waiting to start…": "等待启动…", "Warm Film": "暖调胶片", "Warm Glow": "暖光", "Warm Print": "暖调印相", "Warm Table": "暖调餐桌", "Watched folder": "监视的文件夹", "Watermark": "水印", + "We": "三", "Wednesday": "星期三", "What's New": "新功能", "When rated or hovered": "评级或悬停时", @@ -1527,10 +1748,12 @@ "Width (% of photo)": "宽度(照片百分比)", "Width (px)": "宽度 (px)", "Width × Height": "宽度 × 高度", + "Will Export": "将导出", "Will be replaced": "将替换", "Will be restored": "将恢复", "Window": "窗口", "Winter Blue": "冬日蓝调", + "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "适用于采用标准(拜耳)传感器的相机 RAW 文件。其他文件仍使用常规降噪。", "XMP Sidecar Preferences": "XMP 附属文件偏好设置", "XMP sidecars": "XMP 附属文件", "Year": "年", @@ -1557,20 +1780,32 @@ "are empty": "为空", "aren't empty": "不为空", "blue": "蓝色", + "cached picture": "张已缓存的照片", + "cached pictures": "张已缓存的照片", + "can't be tested that way": "不能这样判断", + "can't match any rating from 0 to 5": "不匹配 0 到 5 的任何星级", + "can't test a folder; choose an album in it": "无法判断文件夹;请选择其中的相册", "comma, separated": "逗号分隔", + "commercial use allowed": "允许商用", + "commercial use subject to GPL conditions": "商用须遵循 GPL 条款", "contains": "包含", "copyrighted": "受版权保护", "day 01–31": "日 01–31", "days": "天", + "denoiseModel.accept": "denoiseModel.accept", "discarding smart previews": "正在丢弃智能预览", "doesn't contain": "不包含", "e.g. sky · the red car · car, road": "例如(英文):sky · the red car · car, road", "ends with": "结尾为", "false": "否", "green": "绿色", + "has the first date after the second": "第一个日期晚于第二个", "hour 00–23": "时 00–23", "hours": "小时", "image": "图像", + "in use": "正在使用", + "includes the folders inside it": "包含其中的文件夹", + "installed": "已安装", "is": "是", "is <": "小于", "is >": "大于", @@ -1580,23 +1815,45 @@ "is empty": "为空", "is in the last": "在最近", "is unknown": "未知", + "is yes or no": "只能是“是”或“否”", "is ≤": "小于等于", "is ≥": "大于等于", "isn't": "不是", + "isn't a field rules know": "不是已知的字段", "isn't empty": "不为空", "isn't in the last": "不在最近", + "isn't one of the choices": "不是可选项之一", "keep names — or e.g. {date}_{seq:3}": "保留名称 — 或如 {date}_{seq:3}", "latitude, longitude": "纬度, 经度", + "makes AI Denoise available": "以启用 AI 降噪", "memory only": "仅内存", "minute": "分", "month 01–12": "月 01–12", "months": "个月", + "names an album that no longer exists": "指定的相册已不存在", + "needs a date like 2026, 2026-08 or 2026-08-14": "需要类似 2026、2026-08 或 2026-08-14 的日期", + "needs a number": "需要一个数字", + "needs a number more than 0": "需要大于 0 的数字", + "needs a time like 1/250 or 2": "需要类似 1/250 或 2 的快门速度", + "needs an album": "需要选择相册", + "needs hours, days, weeks, months or years": "需要小时、天、周、月或年", + "needs something to look for (or use “are empty”)": "需要要查找的内容(或使用“为空”)", + "needs something to look for (or use “aren't empty”)": "需要要查找的内容(或使用“不为空”)", + "needs something to look for (or use “is empty”)": "需要要查找的内容(或使用“为空”)", + "needs something to look for (or use “isn't empty”)": "需要要查找的内容(或使用“不为空”)", + "needs text": "需要文本", + "needs two values": "需要两个值", + "non-commercial use only": "仅限非商用", "none": "无", "none (CPU rendering)": "无(CPU 渲染)", "none of": "均不符合", "not available here": "此处不可用", "of the following rules:": "以下规则:", "or": "或", + "photo has its denoised picture.": "张照片已有降噪结果。", + "photo waiting": "张照片等待中", + "photos have their denoised picture.": "张照片已有降噪结果。", + "photos waiting": "张照片等待中", "pick": "留用", "previews": "预览", "publicDomain": "公共领域", @@ -1604,10 +1861,17 @@ "raw": "RAW", "red": "红色", "reject": "排除", + "save goes through.": "才能完成保存。", "saved in": "保存在", "second": "秒", + "smart album value|No": "智能相册取值|否", + "smart album value|Yes": "智能相册取值|是", "smart previews": "智能预览", + "starts using it for AI Denoise": "开始用它进行 AI 降噪", "starts with": "开头为", + "terms unclear": "条款不明确", + "the graphics card": "显卡", + "the graphics card is not used": "未使用显卡", "the photos in view": "视图中的照片", "the selected photos": "所选照片", "this photo": "这张照片", @@ -1615,6 +1879,7 @@ "unknown": "未知", "video": "视频", "weeks": "周", + "would make this album include itself through other smart albums": "会通过其他智能相册让此相册包含自身", "year, 2 digits": "年,2 位", "year, 4 digits": "年,4 位", "years": "年", @@ -1627,254 +1892,12 @@ "≥ at least": "≥ 至少", "日本語": "日本語", "繁體中文(台灣)": "繁體中文(台灣)", - "In Recently Deleted": "在“最近删除”中", - "Remove Folder from Library": "从图库中移除文件夹", - "Remove from Library…": "从图库中移除…", - "This Computer": "此电脑", - "includes the folders inside it": "包含其中的文件夹", - "They move to Recently Deleted and can be restored; no file on disk is touched.": "它们会移到“最近删除”,可以恢复;磁盘上的文件不会被改动。", - "Moves the photos imported from this folder to Recently Deleted; no file is touched": "将从此文件夹导入的照片移到“最近删除”;不会改动任何文件", - "Remove Disk from Library…": "从图库中移除磁盘…", - "Moves every photo imported from this disk to Recently Deleted; no file is touched": "将从此磁盘导入的所有照片移到“最近删除”;不会改动任何文件", - "Collapse": "折叠", - "Expand": "展开", - "Remove Disk from Library": "从图库中移除磁盘", - "All the photos imported from this disk are included.": "包括从此磁盘导入的所有照片。", - "That includes the photos in the folders inside it.": "其中文件夹里的照片也包括在内。", - "How to install the model": "如何安装模型", - "Install the SAM 3 Model": "安装 SAM 3 模型", - "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚无法下载模型。如需手动安装,请将 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的文件夹:放入后即可使用“对象”和“描述”。", - "Open Log Folder": "打开日志文件夹", - "Back to this person": "Back to this person", - "AI Denoise": "AI Denoise", - "AI Denoise Model": "AI Denoise Model", - "AI Denoise is off: press Use on a model.": "AI Denoise is off: press Use on a model.", - "AI Denoise needs a model. Install one in Settings.": "AI Denoise needs a model. Install one in Settings.", - "Accept & Download": "Accept & Download", - "Already installed. Remove it before installing a replacement.": "Already installed. Remove it before installing a replacement.", - "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.", - "Cache": "Cache", - "Checking and installing the model…": "Checking and installing the model…", - "Checking…": "Checking…", - "Clear cache": "Clear cache", - "Denoise Models": "Denoise Models", - "Denoise Settings": "Denoise Settings", - "Denoise all photos with an Amount": "Denoise all photos with an Amount", - "Downloaded and checked. Installing…": "Downloaded and checked. Installing…", - "Failed its last test": "Failed its last test", - "Graphics card": "Graphics card", - "I have read these terms and accept them for my own use": "I have read these terms and accept them for my own use", - "Inspect Denoise Model": "Inspect Denoise Model", - "Install Denoise Model": "Install Denoise Model", - "Install from file…": "Install from file…", - "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.", - "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).", - "LightCraft does not know this model, so it assumed:": "LightCraft does not know this model, so it assumed:", - "Limit": "Limit", - "Make it now": "Make it now", - "Make the denoised picture of the photos I am looking at": "Make the denoised picture of the photos I am looking at", - "Next up for AI Denoise…": "Next up for AI Denoise…", - "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.", - "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.", - "Open page": "Open page", - "Open the model's page": "Open the model's page", - "Opens the model's own page in your browser, to read about it or get the file yourself.": "Opens the model's own page in your browser, to read about it or get the file yourself.", - "Processor": "Processor", - "Set up AI Denoise…": "Set up AI Denoise…", - "Speed": "Speed", - "Starting…": "Starting…", - "Tested and works on this computer": "Tested and works on this computer", - "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works", - "The graphics card is set up when the next photo is made.": "The graphics card is set up when the next photo is made.", - "The model is kept on this computer only. LightCraft never uploads or shares it.": "The model is kept on this computer only. LightCraft never uploads or shares it.", - "The processor does the work.": "The processor does the work.", - "There is no model file to install": "There is no model file to install", - "This build cannot run denoise models.": "This build cannot run denoise models.", - "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)", - "This file cannot be used yet": "This file cannot be used yet", - "Tick the box to accept the model's terms first": "Tick the box to accept the model's terms first", - "Try again": "Try again", - "Try the graphics card again": "Try the graphics card again", - "Turn off": "Turn off", - "Unknown licence": "Unknown licence", - "Use": "Use", - "Waiting to start…": "Waiting to start…", - "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.", - "cached picture": "cached picture", - "cached pictures": "cached pictures", - "commercial use allowed": "commercial use allowed", - "denoiseModel.accept": "denoiseModel.accept", - "in use": "in use", - "installed": "installed", - "makes AI Denoise available": "makes AI Denoise available", - "non-commercial use only": "non-commercial use only", - "photo has its denoised picture.": "photo has its denoised picture.", - "photo waiting": "photo waiting", - "photos have their denoised picture.": "photos have their denoised picture.", - "photos waiting": "photos waiting", - "starts using it for AI Denoise": "starts using it for AI Denoise", - "terms unclear": "terms unclear", - "the graphics card": "the graphics card", - "the graphics card is not used": "the graphics card is not used", - "Cancel Denoise": "Cancel Denoise", - "Cancel Denoise Model Download": "Cancel Denoise Model Download", - "Choose Denoise Model": "Choose Denoise Model", - "Clear Denoise Cache": "Clear Denoise Cache", - "Denoise Model Downloads": "Denoise Model Downloads", - "Denoise Photos": "Denoise Photos", - "Denoise Pump": "Denoise Pump", - "Denoise Status": "Denoise Status", - "Download Denoise Model": "Download Denoise Model", - "Remove Denoise Model": "Remove Denoise Model", - "Test Denoise Model": "Test Denoise Model", - "AI Denoise is enabled on the requesting photo": "AI Denoise is enabled on the requesting photo", - "After installation, AI Denoise will turn on for the requesting photo.": "After installation, AI Denoise will turn on for the requesting photo.", - "Cancel pending action": "Cancel pending action", - "Install a denoise model to enable AI Denoise on this photo": "Install a denoise model to enable AI Denoise on this photo", - "Pending photo action cancelled": "Pending photo action cancelled", - "Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed", - "ONNX model": "ONNX model", - "commercial use subject to GPL conditions": "commercial use subject to GPL conditions", - "Decrease Rating": "降低星级", - "Increase Rating": "提高星级", - "Click to type a value": "单击以输入数值", - "Angle": "角度", - "Video": "视频", - "Copyrighted": "受版权保护", - "Public Domain": "公共领域", - "Unknown": "未知", - "Landscape (wide)": "横向", - "Portrait (tall)": "纵向", - "Square": "正方形", - "In Color": "彩色", - "No Label": "无标签", - "Choose an album…": "选择相册…", - "No albums yet": "还没有相册", - "Fix the marked rules to save this album.": "请修正标记的规则以保存此相册。", - "Some rules need fixing (Edit Smart Album…):": "有些规则需要修正(编辑智能相册…):", - "isn't a field rules know": "不是已知的字段", - "can't be tested that way": "不能这样判断", - "is yes or no": "只能是“是”或“否”", - "needs a number": "需要一个数字", - "needs a time like 1/250 or 2": "需要类似 1/250 或 2 的快门速度", - "needs a date like 2026, 2026-08 or 2026-08-14": "需要类似 2026、2026-08 或 2026-08-14 的日期", - "has the first date after the second": "第一个日期晚于第二个", - "needs two values": "需要两个值", - "needs a number more than 0": "需要大于 0 的数字", - "needs hours, days, weeks, months or years": "需要小时、天、周、月或年", - "isn't one of the choices": "不是可选项之一", - "needs something to look for (or use “is empty”)": "需要要查找的内容(或使用“为空”)", - "needs something to look for (or use “isn't empty”)": "需要要查找的内容(或使用“不为空”)", - "needs text": "需要文本", - "can't match any rating from 0 to 5": "不匹配 0 到 5 的任何星级", - "needs an album": "需要选择相册", - "names an album that no longer exists": "指定的相册已不存在", - "can't test a folder; choose an album in it": "无法判断文件夹;请选择其中的相册", - "This group is empty: add a rule or remove it.": "此组为空:请添加规则或将其删除。", - "needs something to look for (or use “are empty”)": "需要要查找的内容(或使用“为空”)", - "needs something to look for (or use “aren't empty”)": "需要要查找的内容(或使用“不为空”)", - "Mo": "一", - "Tu": "二", - "We": "三", - "Th": "四", - "Fr": "五", - "Sa": "六", - "Su": "日", - "Pick a date": "选择日期", - "would make this album include itself through other smart albums": "会通过其他智能相册让此相册包含自身", - "Search albums": "搜索相册", - "No albums match": "没有匹配的相册", - "This is the album you're editing": "这是正在编辑的相册", - "It tests this album, so testing it back would loop": "它引用了此相册,反过来引用会形成循环", - "Copy Name": "副本名称", - "In a Stack": "在堆叠中", - "File Extension": "文件扩展名", - "Video Duration": "视频时长", - "Keyword Count": "关键字数量", - "People Count": "人物数量", - "Shutter Speed": "快门速度", - "Cropped": "已裁剪", - "Keywords & People": "关键字与人物", - "Description": "描述", - "Camera Info": "相机信息", - "Develop": "编辑", - "Keyword List": "关键字列表", - "Filter Keywords": "过滤关键字", - "No keywords yet": "还没有关键字", - "No keywords match": "没有匹配的关键字", - "Keyword Name": "关键字名称", - "Synonyms": "同义词", - "Separated by commas": "用逗号分隔", - "Keyword Tag Options": "关键字标记选项", - "Include on Export": "导出时包含", - "Export Containing Keywords": "导出上级关键字", - "Export Synonyms": "导出同义词", - "Create Keyword Tag": "创建关键字标记", - "Edit Keyword Tag": "编辑关键字标记", - "Create": "创建", - "No photo has it; the keywords below it go too.": "没有照片使用它;其下的关键字也会一并删除。", - "Edit Keyword Tag…": "编辑关键字标记…", - "Put New Keywords Inside This Keyword": "将新关键字放入此关键字中", - "Purge Unused Keywords": "清除未使用的关键字", - "Delete Keyword…": "删除关键字…", - "Drop here for the top level": "拖放到此处以移至顶层", - "Their photos and the keywords below them come together under one keyword.": "它们的照片和下级关键字将合并到同一个关键字下。", - "Select photos to give them keywords.": "选择照片以为其添加关键字。", - "Add to All Selected Photos": "添加到所有所选照片", - "Will Export": "将导出", - "No keywords are exported": "没有要导出的关键字", - "& Containing": "及上级", - "Edit Set…": "编辑关键字集…", - "Edit Keyword Set": "编辑关键字集", - "Keywords (⌥1–⌥9)": "关键字 (⌥1–⌥9)", - "A keyword set needs a name": "关键字集需要名称", - "Save as a new set": "另存为新关键字集", - "Recent Keywords is built in: choose another name": "“最近使用的关键字”为内置集:请另选名称", - "Import Keywords…": "导入关键字…", - "Export Keywords…": "导出关键字…", - "Import Keywords": "导入关键字", - "Export Keywords": "导出关键字", - "Keyword Lists": "关键字列表", - "Synchronize Folder…": "同步文件夹…", - "Synchronize Folder": "同步文件夹", - "Synchronize": "同步", - "Scanning…": "正在扫描…", - "Find photos added to or missing from this folder on disk, and XMP sidecars changed by other apps": "查找磁盘上此文件夹中新增或缺失的照片,以及被其他应用更改的 XMP 附属文件", - "Bring the library up to date with this folder and the folders inside it:": "让图库与此文件夹及其中的文件夹保持一致:", - "Import new photos": "导入新照片", - "Remove missing photos from the library": "从图库中移除缺失的照片", - "Read metadata updates from XMP sidecars": "从 XMP 附属文件读取元数据更新", - "The library is up to date with this folder.": "图库与此文件夹一致。", - "Missing photos move to Recently Deleted and can be restored; no file on disk is touched.": "缺失的照片会移到“最近删除”,可以恢复;磁盘上的文件不会被改动。", - "Reading a sidecar replaces the photo's rating, labels, metadata and edits with what it says.": "读取附属文件会用其内容替换照片的评级、标签、元数据和编辑。", - "Skipped: files already in the library elsewhere": "已跳过:已在图库其他位置的文件", - "Skipped: files that can't be read": "已跳过:无法读取的文件", - "This folder isn't there: it was moved or renamed, or its disk isn't connected.": "此文件夹不存在:它已被移动或重命名,或其磁盘未连接。", - "Relink photos whose file was renamed or moved": "重新链接文件被重命名或移动的照片", - "A folder is already being synchronized": "已有文件夹正在同步", - "Synchronizing…": "正在同步…", - "Relinked": "已重新链接", - "Removed": "已移除", - "Metadata read": "已读取元数据", - "Failed": "失败", - "Stopped": "已停止", - "Stopped by an error (see the log)": "因错误而停止(见日志)", - "Not saved": "未保存", - "A folder is being synchronized": "正在同步文件夹", - "Wait for the import to finish": "请等待导入完成", - "Show less": "收起", - "Quit while tasks are running?": "要在任务运行时退出吗?", - "Exporting contact sheet": "正在导出联系表", - "Reading folder": "正在读取文件夹", - "Scanning folder": "正在扫描文件夹", - "Building previews": "正在构建预览", - "Building smart previews": "正在构建智能预览", - "Discarding smart previews": "正在丢弃智能预览", - "Importing Lightroom catalog": "正在导入 Lightroom 目录", - "Reading Lightroom catalog": "正在读取 Lightroom 目录", - "Merging": "正在合并", - "Downloading model": "正在下载模型", - "Finding faces": "正在查找人脸", - "Synchronizing folder": "正在同步文件夹", - "Looking for changes": "正在查找更改" + "Cancel Face Model Download": "取消人脸模型下载", + "Cancel Pending Photo Action": "取消待处理的照片操作", + "Choose Face Recognition Model": "选择人脸识别模型", + "Choose a model below (Use) to start.": "请在下方选择模型(点击“使用”)以开始。", + "Evaluate Face Recognition": "评估人脸识别", + "Index Faces in the Background": "在后台索引人脸", + "Recognise faces (experimental)": "识别面部(实验功能)", + "Go back to listing them by name": "返回按名称列出" } From 3c5e2f3cb2679345469bc032e2b4d390c06cebcc Mon Sep 17 00:00:00 2001 From: Twdhsi-666 <340561342+Twdhsi-666@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:51:57 +0800 Subject: [PATCH 05/24] locales/zh-hans: translate the remaining AI Denoise strings --- crates/ui-egui/locales/zh-hans.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index 4bc80d5a..592aac17 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -19,15 +19,15 @@ "A folder is being synchronized": "正在同步文件夹", "A keyword set needs a name": "关键字集需要名称", "A one-time download of about": "一次性下载,大小约", - "AI Denoise": "AI Denoise", - "AI Denoise Model": "AI Denoise Model", + "AI Denoise": "AI 降噪", + "AI Denoise Model": "AI 降噪模型", "AI Denoise is enabled on the requesting photo": "已在发起请求的照片上启用 AI 降噪", - "AI Denoise is off: press Use on a model.": "AI Denoise is off: press Use on a model.", - "AI Denoise needs a model. Install one in Settings.": "AI Denoise needs a model. Install one in Settings.", + "AI Denoise is off: press Use on a model.": "AI 降噪已关闭:请在某个模型上点击“使用”。", + "AI Denoise needs a model. Install one in Settings.": "AI 降噪需要模型。请在设置中安装一个。", "AI Mask Model Status": "AI 蒙版模型状态", "About": "关于", "About LightCraft": "关于 LightCraft", - "Accept & Download": "Accept & Download", + "Accept & Download": "接受并下载", "Active": "当前照片", "Activity": "活动", "Actual size": "实际大小", @@ -75,7 +75,7 @@ "All rights reserved": "保留所有权利", "All the photos imported from this disk are included.": "包括从此磁盘导入的所有照片。", "All types": "所有类型", - "Already installed. Remove it before installing a replacement.": "Already installed. Remove it before installing a replacement.", + "Already installed. Remove it before installing a replacement.": "已安装。安装替代模型前请先将其移除。", "Alt Text": "替代文本", "Always": "始终", "Amount": "数量", @@ -134,7 +134,7 @@ "Auto-Tag from Tracklog…": "从轨迹日志自动标记…", "Automatic": "自动", "Automatic renders the Detail view at the size it is shown, up to the photo's own pixels. Choose a size to cap it: smaller is faster.": "“自动”按详细视图的显示大小渲染(最高为照片自身像素)。选择一个大小可设上限:越小越快。", - "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.", + "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "自动模式会在显卡快于处理器时使用显卡:两者都会在首次生成照片时于本机计时比较。", "Automatically Write Changes into XMP": "自动将更改写入 XMP", "Automatically write changes into XMP sidecars": "自动将更改写入 XMP 附属文件", "Autumn Gold": "秋日金黄", @@ -146,7 +146,7 @@ "Back to Edit": "返回编辑", "Back to Edit (E)": "返回编辑 (E)", "Back to Grid": "返回网格视图", - "Back to this person": "Back to this person", + "Back to this person": "返回此人", "Background": "背景", "Balance": "平衡", "Basic": "基础", @@ -193,7 +193,7 @@ "By day (YYYY/YYYY-MM-DD)": "按日 (YYYY/YYYY-MM-DD)", "By month (YYYY/YYYY-MM)": "按月 (YYYY/YYYY-MM)", "CPU threads": "CPU 线程数", - "Cache": "Cache", + "Cache": "缓存", "Calibration": "校准", "Camera": "相机", "Camera Info": "相机信息", @@ -224,7 +224,7 @@ "Check All": "全部选中", "Check Output Path": "检查输出路径", "Check the files of this type": "勾选此类型的所有文件", - "Checking and installing the model…": "Checking and installing the model…", + "Checking and installing the model…": "正在检查并安装模型…", "Checking names…": "正在检查名称…", "Checking…": "正在检查…", "Choose Another Library…": "选择其他图库…", From e40bf5792a059960c1384c934973043d183b91e9 Mon Sep 17 00:00:00 2001 From: Twdhsi-666 <340561342+Twdhsi-666@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:00:00 +0800 Subject: [PATCH 06/24] Rebase onto current main so the 15 upstream progress keys are kept --- crates/ui-egui/locales/zh-hans.json | 851 ++++++++++++++-------------- 1 file changed, 433 insertions(+), 418 deletions(-) diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index 592aac17..9885a09e 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -4,7 +4,6 @@ " · best of its burst": " · 连拍中的最佳照片", " · in a burst": " · 属于一组连拍", "% of all commits": "占全部提交的百分比", - "& Containing": "及上级", "+ Group": "+ 组", "+ Rule": "+ 条件", ", picked": ",留用", @@ -15,25 +14,15 @@ "= exactly": "= 等于", "A blank template keeps the original names.": "模板留空则保留原始名称。", "A filter is hiding this view's photos: change it, or clear it (View → Clear Filters)": "筛选器正在隐藏此视图中的照片:请修改筛选条件,或清除筛选(视图 → 清除筛选)", - "A folder is already being synchronized": "已有文件夹正在同步", - "A folder is being synchronized": "正在同步文件夹", - "A keyword set needs a name": "关键字集需要名称", "A one-time download of about": "一次性下载,大小约", - "AI Denoise": "AI 降噪", - "AI Denoise Model": "AI 降噪模型", - "AI Denoise is enabled on the requesting photo": "已在发起请求的照片上启用 AI 降噪", - "AI Denoise is off: press Use on a model.": "AI 降噪已关闭:请在某个模型上点击“使用”。", - "AI Denoise needs a model. Install one in Settings.": "AI 降噪需要模型。请在设置中安装一个。", "AI Mask Model Status": "AI 蒙版模型状态", "About": "关于", "About LightCraft": "关于 LightCraft", - "Accept & Download": "接受并下载", "Active": "当前照片", "Activity": "活动", "Actual size": "实际大小", "Add": "添加", "Add Catchlight": "添加眼神光", - "Add Face Model…": "添加人脸模型…", "Add Folder to Local": "将文件夹添加到本地", "Add Mask": "添加蒙版", "Add Object Click": "添加对象点击", @@ -48,7 +37,6 @@ "Add number": "添加编号", "Add to": "添加到", "Add to Album": "添加到相册", - "Add to All Selected Photos": "添加到所有所选照片", "Add to Favorites": "添加到收藏夹", "Add to Local": "添加到本地", "Add to My Photos": "添加到我的照片", @@ -65,7 +53,6 @@ "After 7 days": "7 天后", "After 90 days": "90 天后", "After a year": "一年后", - "After installation, AI Denoise will turn on for the requesting photo.": "安装完成后,将为发起请求的照片开启 AI 降噪。", "Album": "相册", "Albums": "相册", "All": "全部", @@ -73,14 +60,11 @@ "All Metadata…": "所有元数据…", "All Photos": "所有照片", "All rights reserved": "保留所有权利", - "All the photos imported from this disk are included.": "包括从此磁盘导入的所有照片。", "All types": "所有类型", - "Already installed. Remove it before installing a replacement.": "已安装。安装替代模型前请先将其移除。", "Alt Text": "替代文本", "Always": "始终", "Amount": "数量", "Analyzing the photo for AI masks…": "正在分析照片以生成 AI 蒙版…", - "Angle": "角度", "Any Searchable Text": "任意可搜索文本", "Any camera": "任意相机", "Any keyword": "任意关键字", @@ -94,7 +78,6 @@ "Apply Point Curve Preset": "应用点曲线预设", "Apply Preset": "应用预设", "Apply Settings JSON": "应用设置 JSON", - "Apply the local tone mapping stored in the raw (Apple ProRAW), as the camera renders it": "按相机的渲染方式应用 raw 中保存的局部色调映射(Apple ProRAW)", "Approved": "已批准", "Aqua": "青色", "Aqua Hue": "青色色相", @@ -133,8 +116,6 @@ "Auto-Tag from Tracklog": "从轨迹日志自动标记", "Auto-Tag from Tracklog…": "从轨迹日志自动标记…", "Automatic": "自动", - "Automatic renders the Detail view at the size it is shown, up to the photo's own pixels. Choose a size to cap it: smaller is faster.": "“自动”按详细视图的显示大小渲染(最高为照片自身像素)。选择一个大小可设上限:越小越快。", - "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "自动模式会在显卡快于处理器时使用显卡:两者都会在首次生成照片时于本机计时比较。", "Automatically Write Changes into XMP": "自动将更改写入 XMP", "Automatically write changes into XMP sidecars": "自动将更改写入 XMP 附属文件", "Autumn Gold": "秋日金黄", @@ -146,7 +127,6 @@ "Back to Edit": "返回编辑", "Back to Edit (E)": "返回编辑 (E)", "Back to Grid": "返回网格视图", - "Back to this person": "返回此人", "Background": "背景", "Balance": "平衡", "Basic": "基础", @@ -179,7 +159,6 @@ "Boundary Warp": "边界变形", "Bright & Airy": "明亮通透", "Bring All to Front": "全部置于顶层", - "Bring the library up to date with this folder and the folders inside it:": "让图库与此文件夹及其中的文件夹保持一致:", "Browse Folder": "浏览文件夹", "Browse Folder…": "浏览文件夹…", "Browse Profiles": "浏览配置文件", @@ -193,21 +172,15 @@ "By day (YYYY/YYYY-MM-DD)": "按日 (YYYY/YYYY-MM-DD)", "By month (YYYY/YYYY-MM)": "按月 (YYYY/YYYY-MM)", "CPU threads": "CPU 线程数", - "Cache": "缓存", "Calibration": "校准", "Camera": "相机", - "Camera Info": "相机信息", - "Camera local tone mapping": "相机局部色调映射", "Camera make and model": "相机品牌和型号", "Cameras and Cards": "相机和存储卡", "Cancel": "取消", "Cancel AI Mask Model Download": "取消下载 AI 蒙版模型", - "Cancel Denoise": "取消降噪", - "Cancel Denoise Model Download": "取消降噪模型下载", "Cancel Download": "取消下载", "Cancel Interaction": "取消交互", "Cancel Preview Build": "取消生成预览", - "Cancel pending action": "取消待处理操作", "Candidate": "候选", "Caption": "题注", "Capture Date": "拍摄日期", @@ -224,16 +197,12 @@ "Check All": "全部选中", "Check Output Path": "检查输出路径", "Check the files of this type": "勾选此类型的所有文件", - "Checking and installing the model…": "正在检查并安装模型…", "Checking names…": "正在检查名称…", - "Checking…": "正在检查…", "Choose Another Library…": "选择其他图库…", - "Choose Denoise Model": "选择降噪模型", "Choose Edit Settings to Copy": "选择要复制的编辑设置", "Choose Edit Settings to Copy…": "选择要复制的编辑设置…", "Choose Folder…": "选择文件夹…", "Choose a photo in the grid or filmstrip": "请在网格视图或胶片窗格中选择一张照片", - "Choose an album…": "选择相册…", "Choose…": "选择…", "Cinematic": "电影感", "City": "城市", @@ -241,7 +210,6 @@ "Clean Studio": "纯净棚拍", "Clear": "清除", "Clear Cache": "清除缓存", - "Clear Denoise Cache": "清除降噪缓存", "Clear Filters": "清除过滤器", "Clear Guides": "清除参考线", "Clear History": "清除历史记录", @@ -249,7 +217,6 @@ "Clear Quick Collection": "清空快速集合", "Clear Reference": "清除参考照片", "Clear all": "全部清除", - "Clear cache": "清除缓存", "Clear form": "清除表单", "Clear rating": "清除星级", "Clear rating and advance": "清除星级并前进", @@ -263,7 +230,6 @@ "Click the photo to pick a colour; ⇧-click adds more (up to 5)": "单击照片选择颜色;按 ⇧ 单击可添加更多(最多 5 个)", "Click to clear these filters": "单击以清除这些筛选条件", "Click to restore · double-click to rename": "单击以恢复 · 双击以重命名", - "Click to type a value": "单击以输入数值", "Clone": "仿制", "Close": "关闭", "Close Panel": "关闭面板", @@ -271,19 +237,18 @@ "Close all": "全部关闭", "Close file": "关闭文件", "Cloudy": "阴天", - "Collapse": "折叠", "Collapse All Stacks": "折叠所有堆叠", "Collapse Stack": "折叠堆叠", "Color": "颜色", - "Color Grading": "颜色分级", "Color Label": "颜色标签", + "Color label cleared": "已清除颜色标签", + "Color Grading": "颜色分级", "Color Label Names": "颜色标签名称", "Color Label Sets": "颜色标签集", "Color Mixer": "颜色混合器", "Color Noise Reduction": "颜色降噪", "Color Priority": "颜色优先", "Color Range": "颜色范围", - "Color label cleared": "已清除颜色标签", "Color space": "色彩空间", "Color space: sRGB (AVIF)": "色彩空间:sRGB(AVIF)", "Colors": "颜色", @@ -316,7 +281,6 @@ "Copy Edit Settings": "复制编辑设置", "Copy History Step Settings to Before": "将历史记录步骤的设置复制到修改前", "Copy Metadata": "复制元数据", - "Copy Name": "副本名称", "Copy as DNG": "拷贝为 DNG", "Copy into the library (else use the files where they are)": "拷贝到图库中(否则使用文件当前位置)", "Copy pages": "复制页面", @@ -327,14 +291,11 @@ "Copyright": "版权", "Copyright Info URL": "版权信息 URL", "Copyright Status": "版权状态", - "Copyrighted": "受版权保护", "Country": "国家/地区", - "Create": "创建", "Create Album": "创建相册", "Create Album…": "创建相册…", "Create Folder": "创建文件夹", "Create Folder…": "创建文件夹…", - "Create Keyword Tag": "创建关键字标记", "Create New Mask": "创建新蒙版", "Create PDF from clipboard": "从剪贴板创建 PDF", "Create PDF from file…": "从文件创建 PDF…", @@ -358,7 +319,6 @@ "Crop & Rotate (C)": "裁剪和旋转(C)", "Crop Aspect": "裁切长宽比", "Crop, Rotate, Geometry": "裁剪、旋转、几何", - "Cropped": "已裁剪", "Cross Process": "交叉冲洗", "Culling": "挑片", "Curve": "曲线", @@ -382,7 +342,6 @@ "Daylight": "日光", "Decrease Brush Feather": "减小画笔羽化", "Decrease Brush Size": "减小画笔尺寸", - "Decrease Rating": "降低星级", "Deep Sky": "深邃天空", "Defringe": "去边", "Deghost Amount": "去伪影量", @@ -398,7 +357,6 @@ "Delete Imported Profile": "删除导入的配置文件", "Delete Keyword": "删除关键字", "Delete Keyword Set": "删除关键字集", - "Delete Keyword…": "删除关键字…", "Delete Mask": "删除蒙版", "Delete Metadata Preset": "删除元数据预设", "Delete Permanently": "永久删除", @@ -416,13 +374,6 @@ "Delete all spots": "删除所有污点", "Delete mask": "删除蒙版", "Delete them": "删除它们", - "Denoise Model Downloads": "降噪模型下载", - "Denoise Models": "降噪模型", - "Denoise Photos": "照片降噪", - "Denoise Pump": "降噪队列", - "Denoise Settings": "降噪设置", - "Denoise Status": "降噪状态", - "Denoise all photos with an Amount": "对所有设置了数量的照片进行降噪", "Density": "密度", "Depth Range": "深度范围", "Descending": "降序", @@ -431,7 +382,6 @@ "Describe what to leave out": "描述要排除的内容", "Describe what to select": "描述要选择的内容", "Describe…": "描述…", - "Description": "描述", "Deselect": "取消选择", "Deselect All": "取消全选", "Desert Heat": "沙漠热浪", @@ -440,8 +390,6 @@ "Detail": "细节", "Detail (D)": "细节 (D)", "Details": "详细信息", - "Detect Faces": "检测人脸", - "Develop": "编辑", "Diagonal": "对角线", "Discard Preview Cache": "丢弃预览缓存", "Display P3": "Display P3", @@ -453,16 +401,11 @@ "Done": "完成", "Download": "下载", "Download AI Mask Model": "下载 AI 蒙版模型", - "Download Denoise Model": "下载降噪模型", - "Download Face Model": "下载人脸模型", "Download the SAM 3 Model?": "要下载 SAM 3 模型吗?", - "Downloaded and checked. Installing…": "已下载并校验完成。正在安装…", "Downloads": "下载", - "Drag Crop Handle": "拖动裁切手柄", "Drag along the horizon; double-click for Auto": "沿地平线拖动;双击以自动校正", "Drag over an eye on the photo; the pupil inside is found automatically.": "在照片中的眼睛上拖动;会自动找到其中的瞳孔。", "Draw Guides": "绘制参考线", - "Drop here for the top level": "拖放到此处以移至顶层", "Duplicate": "创建副本", "Duplicate Component": "复制组件", "Duplicate Mask": "复制蒙版", @@ -478,15 +421,11 @@ "Edit Copy for External Editor": "编辑外部编辑器副本", "Edit Date": "编辑日期", "Edit Info": "编辑信息", - "Edit Keyword Set": "编辑关键字集", - "Edit Keyword Tag": "编辑关键字标记", - "Edit Keyword Tag…": "编辑关键字标记…", "Edit Label Names": "编辑标签名称", "Edit Label Names…": "编辑标签名称…", "Edit Mask": "编辑蒙版", "Edit Mask Component": "编辑蒙版组件", "Edit Sections": "编辑板块", - "Edit Set…": "编辑关键字集…", "Edit Smart Album": "编辑智能相册", "Edit Smart Album…": "编辑智能相册…", "Edit Spot": "编辑污点", @@ -508,7 +447,6 @@ "Every EXIF, GPS and XMP field in the file": "文件中的每个 EXIF、GPS 和 XMP 字段", "Existing previews": "现有预览", "Exit": "退出", - "Expand": "展开", "Expand All Stacks": "展开所有堆叠", "Expand Stack": "展开堆叠", "Expand/Collapse Stack": "展开/折叠堆叠", @@ -516,16 +454,12 @@ "Export Album with Preset": "使用预设导出相册", "Export Album…": "导出相册…", "Export As…": "导出为…", - "Export Containing Keywords": "导出上级关键字", "Export Group…": "导出分组…", - "Export Keywords": "导出关键字", - "Export Keywords…": "导出关键字…", "Export Now": "立即导出", "Export Point Curve Presets": "导出点曲线预设", "Export Point Curve Presets…": "导出点曲线预设…", "Export Presets": "导出预设", "Export Presets…": "导出预设…", - "Export Synonyms": "导出同义词", "Export User Presets…": "导出用户预设…", "Export stopped unexpectedly": "导出意外停止", "Export with Preset": "使用预设导出", @@ -537,43 +471,178 @@ "Extended Description": "扩展描述", "External Editor": "外部编辑器", "Face Boxes": "人脸框", - "Face Model Downloads": "人脸模型下载", - "Face Models": "人脸模型", - "Face Recognition On/Off": "人脸识别开关", "Face boxes": "人脸框", - "Face detector": "人脸检测器", "Face names written to XMP by Lightroom and other apps show up here.": "Lightroom 及其他应用写入 XMP 的人脸名称会显示在这里。", - "Face recognition": "人脸识别", - "Faces": "人脸", - "Faces of a Person": "某个人的人脸", "Faded Matte": "褪色哑光", "Faded Slide": "褪色正片", "Faded Urban": "褪色都市", - "Failed": "失败", - "Failed its last test": "上次检测未通过", "Favorite Preset": "收藏预设", "Favorite Profile": "收藏配置文件", "Favorites": "收藏夹", "Feather": "羽化", "File": "文件", "File & date": "文件 & 日期", - "File Extension": "文件扩展名", "File Format": "文件格式", "File Name": "文件名", "File Path": "文件路径", "File Size": "文件大小", "File Type": "文件类型", - "File name": "文件名", "File types:": "文件类型:", "Filename": "文件名", "Fill": "填满", + "First": "最早", + "First commit": "首次提交", + "Five stars": "五星", + "Fixed keys": "固定按键", + "Flagged as Pick": "已标记为留用", + "Flagged as Reject": "已标记为排除", + "Focal Length": "焦距", + "Focus (assisted culling)": "对焦(辅助筛选)", + "Frame time": "帧耗时", + "Frame update": "帧更新", + "GPU rendering": "GPU 渲染", + "Generated demo photo": "生成的演示照片", + "Grab bag": "名单", + "Green label": "绿色标签", + "Group name": "分组名称", + "HDR Merge Preview": "HDR 合并预览", + "HDR Panorama Merge Preview": "HDR 全景合并预览", + "HDR Panoramas": "HDR 全景", + "HDR panorama": "HDR 全景", + "HDR panoramas": "HDR 全景", + "Has Edits": "有编辑", + "Has GPS": "有 GPS", + "Home": "主文件夹", + "Hours": "小时", + "ISO Speed": "ISO 感光度", + "ISO speed": "ISO 感光度", + "Import Lightroom Catalog": "导入 Lightroom 目录", + "Import Lightroom Catalog…": "导入 Lightroom 目录…", + "Import as new": "作为新照片导入", + "Imported": "已导入", + "Importing Lightroom Catalog": "正在导入 Lightroom 目录", + "In Photo Grid and Square Grid: Flag as Pick and advance": "在照片网格和正方形网格中:标记为留用并前进", + "In Photo Grid and Square Grid: expand or collapse the stack": "在照片网格和正方形网格中:展开或折叠堆叠", + "Info Overlay Off": "关闭信息叠加", + "Info Overlay: Exposure": "信息叠加:曝光", + "Info Overlay: File": "信息叠加:文件", + "Insert at the cursor": "在光标处插入", + "Inspect Lightroom Catalog": "检查 Lightroom 目录", + "Inspecting Lightroom Catalog": "正在检查 Lightroom 目录", + "Invert Component": "反相组件", + "Join the ArtCraft community on Discord": "加入 Discord 上的 ArtCraft 社区", + "Label": "标签", + "Label Set": "标签集", + "Last GPU fallback": "最近一次 GPU 回退", + "Last commit": "最近一次提交", + "Last loupe render": "最近一次放大视图渲染", + "Lens Blur": "镜头模糊", + "Library saved": "图库已保存", + "Licence:": "许可证:", + "LightCraft could not start": "LightCraft 无法启动", + "Lightroom Classic Catalog": "Lightroom Classic 目录", + "Lightroom import cancelled": "已取消 Lightroom 导入", + "Lightroom inspection cancelled": "已取消 Lightroom 检查", + "Line delta": "行数差", + "Linear": "线性", + "Lines +/−": "行数 +/−", + "Lines added": "新增行数", + "Lines deleted": "删除行数", + "Lock": "锁定", + "Lock Crop Aspect": "锁定裁剪长宽比", + "Locked": "已锁定", + "Mask Adjustment": "蒙版调整", + "Memory budget": "内存预算", + "Merged": "已合并", + "Merged PRs": "已合并的 PR", + "Meta's terms, not LightCraft's. Downloading it means accepting them.": "适用的是 Meta 的条款,而非 LightCraft 的条款。下载即表示接受这些条款。", + "Missing metadata ({camera}, {title}…) leaves an empty gap; a name that comes out empty keeps the original name.": "缺少的元数据({camera}、{title}…)会留空;如果生成的名称为空,则保留原始名称。", + "Model": "型号", + "Models": "模型", + "Move Down": "下移", + "Move Preset to New Group": "将预设移动到新分组", + "Move Up": "上移", + "Move in Stack": "在堆叠中移动", + "Name (A–Z)": "名称(A–Z)", + "Name of the folder the original is in": "原始文件所在文件夹的名称", + "Named People": "已命名的人物", + "Nested group": "嵌套组", + "New time": "新时间", + "No contributor data was built into this copy.": "此版本未内置贡献者数据。", + "No matching commands": "没有匹配的命令", + "No model credits were built into this copy.": "此版本未内置模型致谢信息。", + "No named people in these photos": "这些照片中没有已命名的人物", + "No named people yet": "还没有已命名的人物", + "Not Now": "以后再说", + "Object Mask": "对象蒙版", + "Object and Describe masks use SAM 3, Meta's segmentation model. It isn't part of LightCraft, and everything else works without it.": "对象蒙版和描述蒙版使用 Meta 的分割模型 SAM 3。它不属于 LightCraft,其他所有功能无需它也能使用。", + "October 2026": "2026年10月", + "Only": "仅限", + "Original extension without the dot": "不含点的原始扩展名", + "Original file name without its extension": "不含扩展名的原始文件名", + "Panorama": "全景", + "Panorama Merge Preview": "全景合并预览", + "Panoramas": "全景", + "Paste Settings": "粘贴设置", + "Paste from Previous": "粘贴上一张照片的设置", + "People": "人物", + "Person": "人物", + "Photos per bracket (0 = auto)": "每组包围曝光的照片数(0 = 自动)", + "Pick Flag": "留用旗标", + "Pictures": "图片", + "Prepare AI Masks": "准备 AI 蒙版", + "Presets and profiles": "预设和配置文件", + "Press keys…": "请按键…", + "Preview only: editing the camera's embedded JPEG": "仅预览:正在编辑相机内嵌的 JPEG", + "Proof copy created": "已创建校样副本", + "Quit Anyway": "仍然退出", + "Quit with unsaved changes?": "有未保存的更改,确定退出吗?", + "RAW decoding": "RAW 解码", + "Radial": "径向", + "Random": "随机", + "Rate ★": "评为 ★", + "Rate ★ and advance": "评为 ★ 并前进", + "Rate ★★": "评为 ★★", + "Rate ★★ and advance": "评为 ★★ 并前进", + "Rate ★★★": "评为 ★★★", + "Rate ★★★ and advance": "评为 ★★★ 并前进", + "Rate ★★★★": "评为 ★★★★", + "Rate ★★★★ and advance": "评为 ★★★★ 并前进", + "Rate ★★★★★": "评为 ★★★★★", + "Rate ★★★★★ and advance": "评为 ★★★★★ 并前进", + "Rating cleared": "已清除星级", + "Raw": "RAW", + "Read the SAM License": "阅读 SAM 许可证", + "Reading Lightroom catalog…": "正在读取 Lightroom 目录…", + "Real name": "真实姓名", + "Recent": "最近使用", + "Recent Keywords": "最近使用的关键字", + "Red label": "红色标签", + "Refine": "细化", + "Refine AI Mask Detail": "细化 AI 蒙版细节", + "Refine Edges": "细化边缘", + "Refine Saturation": "细化饱和度", + "Refining the mask's detail…": "正在细化蒙版细节…", + "Reliability": "可靠性", + "Relink Photo": "重新链接照片", + "Reload Changed Files": "重新载入已更改的文件", + "Remove Face Box": "移除人脸框", + "Remove a filter above, or choose Clear all": "请移除上方的某个筛选条件,或选择“全部清除”", + "Remove from Target Album": "从目标相册中移除", + "Remove shortcut": "移除快捷键", + "Remove this face box (undo with Edit ▸ Undo)": "移除此人脸框(可通过“编辑 → 撤销”恢复)", + "Rename Component": "重命名组件", + "Reorder Masks": "重新排序蒙版", + "Reset All": "全部重置", + "Reset All Keyboard Shortcuts": "重置所有键盘快捷键", + "Reshuffle": "重新随机排序", + "File name": "文件名", "Fill Edges": "填充边缘", "Film": "胶片", "Filmstrip": "胶片窗格", "Filmstrip (/)": "胶片窗格 (/)", "Filter": "过滤器", "Filter Bar": "过滤器栏", - "Filter Keywords": "过滤关键字", "Filter Presets": "过滤器预设", "Filter bar": "过滤器栏", "Filter fields": "过滤字段", @@ -581,24 +650,16 @@ "Find Missing Photos": "查找缺失的照片", "Find Missing Photos…": "查找丢失的照片…", "Find Similar Photos": "查找相似照片", - "Find photos added to or missing from this folder on disk, and XMP sidecars changed by other apps": "查找磁盘上此文件夹中新增或缺失的照片,以及被其他应用更改的 XMP 附属文件", "Find tools and commands…": "查找工具和命令…", "Find…": "查找…", - "First": "最早", - "First commit": "首次提交", "Fit": "适合", "Fit (F)": "适合 (F)", "Fit on Screen": "适合屏幕", "Fit to width": "适合宽度", "Fit visible": "适合可见区域", - "Five stars": "五星", - "Fix the marked rules to save this album.": "请修正标记的规则以保存此相册。", - "Fixed keys": "固定按键", "Flag": "旗标", "Flag as Pick": "标记为留用", "Flag as Reject": "标记为排除", - "Flagged as Pick": "已标记为留用", - "Flagged as Reject": "已标记为排除", "Flash": "闪光灯", "Flip H": "水平翻转", "Flip Horizontal": "水平翻转", @@ -607,9 +668,7 @@ "Flip Vertical": "垂直翻转", "Flow": "流量", "Fluorescent": "荧光灯", - "Focal Length": "焦距", "Focal length": "焦距", - "Focus (assisted culling)": "对焦(辅助筛选)", "Folder": "文件夹", "Folder name (renamed on disk; its photos follow)": "文件夹名称 (在磁盘上重命名;照片随之更新)", "Folders": "文件夹", @@ -620,9 +679,6 @@ "Form fields panel": "表单字段面板", "Format": "格式", "Forward": "前进", - "Fr": "五", - "Frame time": "帧耗时", - "Frame update": "帧更新", "Free": "自由", "Fresh & Bright": "清新明亮", "Friday": "星期五", @@ -632,9 +688,7 @@ "Full size": "完整大小", "GPS Track Log": "GPS 轨迹日志", "GPU Rendering": "GPU 渲染", - "GPU rendering": "GPU 渲染", "General": "常规", - "Generated demo photo": "生成的演示照片", "Geometry": "几何", "Get Develop Settings": "获取编辑设置", "Global Hue": "全局色相", @@ -645,10 +699,8 @@ "Golden": "黄金比例", "Golden Glow": "金色光晕", "Golden Hour": "黄金时刻", - "Grab bag": "名单", "Grain": "颗粒", "Graphic": "图形", - "Graphics card": "显卡", "Green": "绿色", "Green Amount": "绿色量", "Green Hue": "绿色色相", @@ -658,7 +710,6 @@ "Green Primary": "绿色原色", "Green Saturation": "绿色饱和度", "Green channel": "绿色通道", - "Green label": "绿色标签", "Grid": "网格视图", "Grid Info": "网格信息", "Gritty Street": "粗粝街景", @@ -669,23 +720,15 @@ "Group by Year": "按年分组", "Group by date": "按日期分组", "Group into Stack": "合并为堆叠", - "Group name": "分组名称", "Guided": "引导式", "Guided Upright": "引导式透视校正", "Guided Upright Guides": "引导式垂直校正参考线", "HDR Merge": "HDR 合并", - "HDR Merge Preview": "HDR 合并预览", "HDR Panorama Merge": "HDR 全景合并", - "HDR Panorama Merge Preview": "HDR 全景合并预览", "HDR Panorama with Last Settings": "使用上次设置合并 HDR 全景", - "HDR Panoramas": "HDR 全景", "HDR Panorama…": "HDR 全景…", - "HDR panorama": "HDR 全景", - "HDR panoramas": "HDR 全景", "HDR with Last Settings": "使用上次设置合并 HDR", "HDR…": "HDR…", - "Has Edits": "有编辑", - "Has GPS": "有 GPS", "Heal": "修复", "Heal the small, soft dark spots sensor dust leaves on smooth areas": "修复传感器灰尘在平滑区域留下的小而柔和的暗斑", "Height": "高度", @@ -706,24 +749,14 @@ "Histogram": "直方图", "History": "历史记录", "History & Activity (Y)": "历史记录与活动(Y)", - "Home": "主文件夹", "Horizontal": "水平", "Horizontal text": "横排文字", - "Hours": "小时", - "How to install the model": "如何安装模型", "Hue": "色相", "Hue Range": "色相范围", - "I have read these terms and accept them for my own use": "我已阅读这些条款,并为个人使用表示接受", - "ISO Speed": "ISO 感光度", - "ISO speed": "ISO 感光度", "If file exists": "如果文件已存在", "Image": "图像", "Import": "导入", "Import Date": "导入日期", - "Import Keywords": "导入关键字", - "Import Keywords…": "导入关键字…", - "Import Lightroom Catalog": "导入 Lightroom 目录", - "Import Lightroom Catalog…": "导入 Lightroom 目录…", "Import Photos": "导入照片", "Import Photos…": "导入照片…", "Import Point Curve Presets": "导入点曲线预设", @@ -732,49 +765,23 @@ "Import Presets…": "导入预设…", "Import Profiles": "导入配置文件", "Import Profiles & Presets…": "导入配置文件与预设…", - "Import as new": "作为新照片导入", "Import from Device": "从设备导入", "Import from Folder…": "从文件夹导入…", - "Import new photos": "导入新照片", "Import photos with File → Import Photos… (Cmd+Shift+I), or drop them here": "使用“文件 → 导入照片…”(Cmd+Shift+I)导入照片,或将照片拖放到此处", - "Imported": "已导入", "Imported Photos": "已导入的照片", "Importing": "正在导入", - "Importing Lightroom Catalog": "正在导入 Lightroom 目录", "Importing scans the source for photos; it doesn't add the folder to Local (use Local → Browse Folder… to work in a folder without importing).": "导入会扫描源中的照片,但不会将该文件夹添加到“本地”(若想在不导入的情况下处理某个文件夹,请使用“本地 → 浏览文件夹…”)。", - "In Color": "彩色", - "In Photo Grid and Square Grid: Flag as Pick and advance": "在照片网格和正方形网格中:标记为留用并前进", - "In Photo Grid and Square Grid: expand or collapse the stack": "在照片网格和正方形网格中:展开或折叠堆叠", - "In Recently Deleted": "在“最近删除”中", - "In a Stack": "在堆叠中", "In library": "在图库中", "In memory — nothing is saved": "仅存于内存 — 不会保存任何内容", "In use": "使用中", "In-memory session: these settings last until LightCraft quits.": "内存会话:这些设置仅在 LightCraft 退出前有效。", - "Include on Export": "导出时包含", "Include subfolders": "包含子文件夹", "Increase Brush Feather": "增大画笔羽化", "Increase Brush Size": "增大画笔尺寸", - "Increase Rating": "提高星级", - "Index Faces": "索引人脸", "Info": "信息", "Info (I)": "信息(I)", - "Info Overlay Off": "关闭信息叠加", - "Info Overlay: Exposure": "信息叠加:曝光", - "Info Overlay: File": "信息叠加:文件", "Info overlay": "信息叠加显示", - "Insert at the cursor": "在光标处插入", - "Inspect Denoise Model": "查看降噪模型", - "Inspect Face Model File": "查看人脸模型文件", - "Inspect Lightroom Catalog": "检查 Lightroom 目录", "Inspect Photo": "检查照片", - "Inspecting Lightroom Catalog": "正在检查 Lightroom 目录", - "Install Denoise Model": "安装降噪模型", - "Install Face Model": "安装人脸模型", - "Install a denoise model to enable AI Denoise on this photo": "安装降噪模型后即可在此照片上启用 AI 降噪", - "Install from file…": "从文件安装…", - "Install the SAM 3 Model": "安装 SAM 3 模型", - "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "请自行安装 ONNX 模型,并在其旁边放置 denoise-model.json。此版本不提供模型下载。", "Instant ’70s": "70 年代即时成像", "Interface": "界面", "Interface language": "界面语言", @@ -782,86 +789,55 @@ "Intersect with": "相交于", "Into one folder": "合并到一个文件夹", "Invert": "反相", - "Invert Component": "反相组件", "Invert Mask": "反相蒙版", - "It tests this album, so testing it back would loop": "它引用了此相册,反过来引用会形成循环", - "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "该照片的降噪结果尚未生成,而且不会自动生成(设置、AI 降噪)。", "JPEG (Large)": "JPEG(大)", "JPEG (Small)": "JPEG(小)", "Join the ArtCraft Discord": "加入 ArtCraft Discord", "Join the ArtCraft Discord…": "加入 ArtCraft Discord…", - "Join the ArtCraft community on Discord": "加入 Discord 上的 ArtCraft 社区", "Keep in Local": "保留在本地", "Keep it on a drive with room: smart previews are about 1 MB per photo. The thumbnail cache stays in the library.": "请选择空间充足的驱动器:智能预览每张照片约 1 MB。缩略图缓存仍保留在图库中。", "Keyboard Shortcuts": "键盘快捷键", "Keyboard shortcuts": "键盘快捷键", "Keyword": "关键字", - "Keyword Count": "关键字数量", - "Keyword List": "关键字列表", - "Keyword Lists": "关键字列表", - "Keyword Name": "关键字名称", "Keyword Painter": "关键字画笔", "Keyword Set": "关键字集", "Keyword Sets": "关键字集", "Keyword Suggestions": "关键字建议", - "Keyword Tag Options": "关键字标记选项", "Keyword to paint": "要涂抹的关键字", "Keywords": "关键字", - "Keywords & People": "关键字与人物", "Keywords (K)": "关键字 (K)", - "Keywords (⌥1–⌥9)": "关键字 (⌥1–⌥9)", "Keywords you add appear here; ⌥1–⌥9 apply them.": "您添加的关键字会显示在此处;按 ⌥1–⌥9 即可应用。", - "Label": "标签", - "Label Set": "标签集", "Landscape": "横向", - "Landscape (wide)": "横向", "Language": "语言", + "Automatic renders the Detail view at the size it is shown, up to the photo's own pixels. Choose a size to cap it: smaller is faster.": "“自动”按详细视图的显示大小渲染(最高为照片自身像素)。选择一个大小可设上限:越小越快。", "Last": "上次", - "Last GPU fallback": "最近一次 GPU 回退", - "Last commit": "最近一次提交", - "Last loupe render": "最近一次放大视图渲染", "Last view": "上次视图", "Lateral Chromatic Aberration": "横向色差", "Layer": "图层", "Layers": "图层", "Leave them": "保留", "Lens": "镜头", - "Lens Blur": "镜头模糊", "Lens Corrections": "镜头校正", "Level": "水平", "Library": "图库", - "Library Folders": "图库文件夹", "Library Info": "图库信息", "Library Originals": "图库原始文件", "Library Preferences": "图库偏好设置", "Library State": "图库状态", - "Library saved": "图库已保存", - "Licence:": "许可证:", "Light": "亮度", "LightCraft Default": "LightCraft 默认", "LightCraft Help": "LightCraft 帮助", "LightCraft Preset": "LightCraft 预设", "LightCraft Presets": "LightCraft 预设", "LightCraft Website": "LightCraft 网站", - "LightCraft could not start": "LightCraft 无法启动", "LightCraft couldn't find where to keep your library.": "LightCraft 找不到保存图库的位置。", "LightCraft couldn't open the library at": "LightCraft 无法打开以下位置的图库:", - "LightCraft does not know this model, so it assumed:": "LightCraft 不认识此模型,因此按以下假设处理:", "LightCraft on GitHub": "GitHub 上的 LightCraft", "LightCraft website": "LightCraft 网站", "LightCraft — Second Window": "LightCraft — 第二窗口", - "Lightroom Classic Catalog": "Lightroom Classic 目录", - "Lightroom import cancelled": "已取消 Lightroom 导入", - "Lightroom inspection cancelled": "已取消 Lightroom 检查", "Lights": "高光", - "Limit": "上限", "Limit file size (KB, 0 = off)": "限制文件大小(KB,0 = 关闭)", - "Line delta": "行数差", - "Linear": "线性", "Linear Gradient": "线性渐变", - "Lines +/−": "行数 +/−", - "Lines added": "新增行数", - "Lines deleted": "删除行数", "List Albums": "列出相册", "List Develop Controls": "列出编辑控制项", "List History": "列出历史记录", @@ -874,9 +850,6 @@ "Locate Missing File…": "定位缺失的文件…", "Locate Photo": "定位照片", "Location": "位置", - "Lock": "锁定", - "Lock Crop Aspect": "锁定裁剪长宽比", - "Locked": "已锁定", "Long Edge": "长边", "Looking for photos…": "正在查找照片…", "Lossless": "无损", @@ -889,11 +862,8 @@ "Magenta Luminance": "品红色明度", "Magenta Saturation": "品红色饱和度", "Make Candidate the Select": "将候选设为选择", - "Make it now": "立即生成", - "Make the denoised picture of the photos I am looking at": "为我正在查看的照片生成降噪结果", "Manual": "手动", "Marquee zoom": "框选缩放", - "Mask Adjustment": "蒙版调整", "Masking": "蒙版", "Masking (M)": "蒙版 (M)", "Masks": "蒙版", @@ -906,21 +876,16 @@ "Megapixels": "百万像素", "Memory Budget": "内存预算", "Memory Usage": "内存用量", - "Memory budget": "内存预算", "Memory for caches": "缓存内存", "Menu": "菜单", "Merge": "合并", "Merge Keywords": "合并关键字", "Merge into…": "合并到…", - "Merged": "已合并", - "Merged PRs": "已合并的 PR", "Merging…": "正在合并…", - "Meta's terms, not LightCraft's. Downloading it means accepting them.": "适用的是 Meta 的条款,而非 LightCraft 的条款。下载即表示接受这些条款。", "Metadata": "元数据", "Metadata Preset": "元数据预设", "Metadata Presets": "元数据预设", "Metadata preset": "元数据预设", - "Metadata read": "已读取元数据", "Midpoint": "中点", "Midtones": "中间调", "Midtones Hue": "中间调色相", @@ -929,13 +894,8 @@ "Midtones split": "中间调分割点", "Minimize": "最小化", "Missing Photos": "缺失的照片", - "Missing metadata ({camera}, {title}…) leaves an empty gap; a name that comes out empty keeps the original name.": "缺少的元数据({camera}、{title}…)会留空;如果生成的名称为空,则保留原始名称。", - "Missing photos move to Recently Deleted and can be restored; no file on disk is touched.": "缺失的照片会移到“最近删除”,可以恢复;磁盘上的文件不会被改动。", "Misty Morning": "薄雾清晨", - "Mo": "一", "Mode": "模式", - "Model": "型号", - "Models": "模型", "Modified Date": "修改日期", "Monday": "星期一", "Mono Red Filter": "黑白红色滤镜", @@ -949,40 +909,28 @@ "More preset options": "更多预设选项", "Move": "移动", "Move Album": "移动相册", - "Move Down": "下移", "Move Down in Stack": "在堆叠中下移", "Move Folder": "移动文件夹", "Move Folder To…": "将文件夹移动到…", "Move Mask": "移动蒙版", "Move Preset to Group": "将预设移动到分组", - "Move Preset to New Group": "将预设移动到新分组", - "Move Up": "上移", "Move Up in Stack": "在堆叠中上移", - "Move in Stack": "在堆叠中移动", "Move the files (into the library's Originals/, or a folder you choose), removing them from the source": "移动文件 (移到图库的 Originals/,或你选择的文件夹),并从源位置移除", "Move them": "移动它们", "Move to": "移动到", "Move to Group": "移动到组", "Move: the files are moved to the folder below; each original (and its XMP sidecar) is removed from the source only after its copy is verified. Duplicates and files that fail stay where they are.": "移动:文件将移动到下面的文件夹;每个原始文件 (及其 XMP 附属文件) 仅在副本校验通过后才会从源位置移除。重复的文件和失败的文件保留在原处。", - "Moves every photo imported from this disk to Recently Deleted; no file is touched": "将从此磁盘导入的所有照片移到“最近删除”;不会改动任何文件", - "Moves the photos imported from this folder to Recently Deleted; no file is touched": "将从此文件夹导入的照片移到“最近删除”;不会改动任何文件", "Muted": "低饱和", "Muted Film": "低饱和胶片", "My Photos": "我的照片", "My Photos Panel": "我的照片面板", "Name": "名称", - "Name (A–Z)": "名称(A–Z)", - "Name Face": "命名人脸", - "Name Faces": "命名人脸", - "Name of the folder the original is in": "原始文件所在文件夹的名称", "Named": "已命名", - "Named People": "已命名的人物", "Names appear in the label menu, the filter bar and the Info panel, and are written to XMP. Empty = the colour's name.": "名称会显示在标签菜单、筛选栏和信息面板中,并写入 XMP。留空 = 颜色的名称。", "Navigator": "导航器", "Needs Work": "待处理", "Neon Dusk": "霓虹暮色", "Neon Night": "霓虹之夜", - "Nested group": "嵌套组", "Neutral": "中性", "Never": "从不", "New": "新建", @@ -997,40 +945,26 @@ "New album…": "新建相册…", "New blank PDF": "新建空白 PDF", "New name": "新名称", - "New time": "新时间", "New…": "新建…", "Next Photo": "下一张照片", - "Next up for AI Denoise…": "接下来的 AI 降噪任务…", "Night Blue": "夜色蓝调", "No": "否", "No Camera or Card Found": "未找到相机或存储卡", "No Date Groups": "不按日期分组", - "No Label": "无标签", "No Stars": "无星级", "No Tool": "无工具", - "No albums match": "没有匹配的相册", - "No albums yet": "还没有相册", "No automatic versions.": "没有自动版本。", "No camera": "无相机信息", - "No contributor data was built into this copy.": "此版本未内置贡献者数据。", "No date": "无日期", "No dust spots found": "未找到污点", "No edits yet.": "尚未编辑。", "No exposure information": "无曝光信息", "No filter presets yet": "暂无筛选器预设", - "No keywords are exported": "没有要导出的关键字", - "No keywords match": "没有匹配的关键字", - "No keywords yet": "还没有关键字", "No lens data embedded in this file (DNG lens opcodes only).": "此文件中未嵌入镜头数据 (仅支持 DNG 镜头操作码)。", "No masks yet. Choose a mask type above.": "尚无蒙版。请在上方选择蒙版类型。", - "No matching commands": "没有匹配的命令", "No matching photos": "没有匹配的照片", "No metadata found": "未找到元数据", - "No model credits were built into this copy.": "此版本未内置模型致谢信息。", - "No named people in these photos": "这些照片中没有已命名的人物", - "No named people yet": "还没有已命名的人物", "No new presets": "没有新预设", - "No photo has it; the keywords below it go too.": "没有照片使用它;其下的关键字也会一并删除。", "No photo selected": "未选择照片", "No photos": "没有照片", "No photos found": "未找到照片", @@ -1045,34 +979,21 @@ "Noise Reduction": "降噪", "Non-raw photos": "非 RAW 照片", "None": "无", - "Not Now": "以后再说", - "Not saved": "未保存", - "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "在你接受之前不会下载任何内容。模型仅保存在本机;LightCraft 绝不会上传或分享它。", "Nothing to compare": "没有可比较的照片", "Nothing to survey": "没有可筛选的照片", "Nudge Develop Value": "微调编辑数值", "OK": "确定", - "ONNX model": "ONNX 模型", "Object": "对象", - "Object Mask": "对象蒙版", - "Object and Describe masks use SAM 3, Meta's segmentation model. It isn't part of LightCraft, and everything else works without it.": "对象蒙版和描述蒙版使用 Meta 的分割模型 SAM 3。它不属于 LightCraft,其他所有功能无需它也能使用。", - "October 2026": "2026年10月", "Off": "关闭", "Offset X": "水平偏移", "Offset Y": "垂直偏移", - "Only": "仅限", - "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "只对 AI 降噪数量大于 0 的照片生成结果。导出时若缺少结果则会先生成。", "Opacity": "不透明度", "Opacity (%)": "不透明度(%)", "Open Library": "打开图库", "Open Library…": "打开图库…", - "Open Log Folder": "打开日志文件夹", "Open in": "打开方式", "Open in Detail": "在放大视图中打开", - "Open page": "打开页面", - "Open the model's page": "打开该模型的页面", "Open the place in OpenStreetMap": "在 OpenStreetMap 中打开该地点", - "Opens the model's own page in your browser, to read about it or get the file yourself.": "在浏览器中打开该模型的主页,以便查阅说明或自行获取文件。", "Open…": "打开…", "Optics": "镜头校正", "Optimize Library": "优化图库", @@ -1085,8 +1006,6 @@ "Orientation": "方向", "Original": "原始比例", "Original + Settings": "原始文件 + 设置", - "Original extension without the dot": "不含点的原始扩展名", - "Original file name without its extension": "不含扩展名的原始文件名", "Original files": "原始文件", "Original offline · editing the smart preview": "原图离线 · 正在编辑智能预览", "Original offline · no smart preview": "原图离线 · 无智能预览", @@ -1099,11 +1018,8 @@ "Paint only areas like the one under the brush": "仅绘制与画笔下相似的区域", "Paint over a distraction on the photo to remove it.": "在照片上要移除的干扰物上涂抹。", "Paint overlay": "蒙版叠加显示", - "Panorama": "全景", "Panorama Merge": "全景合并", - "Panorama Merge Preview": "全景合并预览", "Panorama with Last Settings": "使用上次设置合并全景", - "Panoramas": "全景", "Panorama…": "全景…", "Parametric curve": "参数曲线", "Paste": "粘贴", @@ -1111,19 +1027,12 @@ "Paste Metadata": "粘贴元数据", "Paste Selected Settings": "粘贴所选设置", "Paste Selected Settings…": "粘贴所选设置…", - "Paste Settings": "粘贴设置", "Paste Settings from Previous": "粘贴上一张照片的设置", - "Paste from Previous": "粘贴上一张照片的设置", "Paste pages": "粘贴页面", "Pastel Haze": "粉彩薄雾", - "Pending photo action cancelled": "已取消待处理的照片操作", - "Pending photo action cancelled because its library or photo changed": "待处理的照片操作已取消,因为其所属图库或照片发生了变化", - "People": "人物", - "People Count": "人物数量", "Percent": "百分比", "Percentage": "百分比", "Performance": "性能", - "Person": "人物", "Perspective": "透视", "Pet Eye": "宠物眼", "Pet Eye Catchlight": "宠物眼神光", @@ -1133,46 +1042,36 @@ "Photo Merge": "照片合并", "Photo ▸ Edit in External Editor (⇧⌘E) renders a 16-bit TIFF copy, stacks it with the original and opens it here (an app name on macOS, a program path elsewhere).": "“照片 » 在外部编辑器中编辑”(⇧⌘E)会渲染一份 16 位 TIFF 副本,与原图堆叠并在此打开(macOS 上填应用名称,其他平台填程序路径)。", "Photos": "照片", - "Photos per bracket (0 = auto)": "每组包围曝光的照片数(0 = 自动)", "Photos seen in Local but never added or changed leave the catalog when their folder hasn't been browsed for this long (checked when the library opens). Files stay on disk; browsing the folder shows them again.": "仅在“本地”中查看过、从未添加或更改的照片,若其文件夹在此时长内未被浏览,将移出目录(打开图库时检查)。文件保留在磁盘上;浏览该文件夹即可再次显示。", "Photos that arrive in this folder (tethering, a scanner, a sync app) are added as soon as they're complete.": "进入此文件夹的照片(联机拍摄、扫描仪、同步应用)在传输完成后即被添加。", "Pick": "留用", - "Pick Flag": "留用旗标", "Pick a colour on the photo with the eyedropper.": "用吸管在照片上取色。", - "Pick a date": "选择日期", "Pick the sharpest photo of each burst": "为每组连拍选取最清晰的照片", "Picked": "已留用", "Picks": "留用", - "Pictures": "图片", "Pixels": "像素", "Point Color": "点颜色", "Point Curve": "点曲线", "Point Curve Presets": "点曲线预设", "Point curve": "点曲线", "Portrait": "人像", - "Portrait (tall)": "纵向", "Position": "位置", "Preferences": "首选项", "Preferences…": "首选项…", - "Prepare AI Masks": "准备 AI 蒙版", "Preset": "预设", "Preset name": "预设名称", "Presets": "预设", "Presets & Profiles": "预设和配置文件", "Presets (Shift+P)": "预设 (Shift+P)", - "Presets and profiles": "预设和配置文件", "Presets ▾": "预设 ⌄", - "Press keys…": "请按键…", "Preview": "预览", "Preview Build Progress": "预览生成进度", "Preview only": "仅预览", - "Preview only: editing the camera's embedded JPEG": "仅预览:正在编辑相机内嵌的 JPEG", "Preview size": "预览大小", "Previews": "预览", "Previous Photo": "上一张照片", "Print": "打印", "Print…": "打印…", - "Processor": "处理器", "Profile": "配置文件", "Profile Browser": "配置文件浏览器", "Profile Distortion": "配置文件扭曲", @@ -1181,12 +1080,9 @@ "Profile:": "配置文件:", "Profiles": "配置文件", "Projection": "投影", - "Proof copy created": "已创建校样副本", "Properties": "属性", - "Public Domain": "公共领域", "Public domain": "公有领域", "Pupil Size": "瞳孔大小", - "Purge Unused Keywords": "清除未使用的关键字", "Purple": "紫色", "Purple Amount": "紫色量", "Purple Hue": "紫色色相", @@ -1194,7 +1090,6 @@ "Purple Hue Low": "紫色色相下限", "Purple Luminance": "紫色亮度", "Purple Saturation": "紫色饱和度", - "Put New Keywords Inside This Keyword": "将新关键字放入此关键字中", "Put the locations you removed from Local back (no files change)": "恢复你从本地移除的位置 (不更改文件)", "Quality": "品质", "Query Photos": "查询照片", @@ -1202,49 +1097,23 @@ "Quick Develop": "快速编辑", "Quiet Green": "静谧绿", "Quit": "退出", - "Quit Anyway": "仍然退出", "Quit LightCraft": "退出 LightCraft", - "Quit with unsaved changes?": "有未保存的更改,确定退出吗?", - "RAW decoding": "RAW 解码", - "Radial": "径向", "Radial Gradient": "径向渐变", "Radius": "半径", - "Random": "随机", "Range": "范围", - "Rate ★": "评为 ★", - "Rate ★ and advance": "评为 ★ 并前进", - "Rate ★★": "评为 ★★", - "Rate ★★ and advance": "评为 ★★ 并前进", - "Rate ★★★": "评为 ★★★", - "Rate ★★★ and advance": "评为 ★★★ 并前进", - "Rate ★★★★": "评为 ★★★★", - "Rate ★★★★ and advance": "评为 ★★★★ 并前进", - "Rate ★★★★★": "评为 ★★★★★", - "Rate ★★★★★ and advance": "评为 ★★★★★ 并前进", "Rating": "星级", - "Rating cleared": "已清除星级", "Ratings & flags": "星级和旗标", - "Raw": "RAW", "Raw default": "RAW 默认值", "Raw defaults": "RAW 默认值", "Raw files": "RAW 文件", "Raw photos": "RAW 照片", "Raw photos as DNG, with the edits embedded. Size, color and output options don't apply.": "将 RAW 照片导出为内嵌编辑设置的 DNG。大小、颜色和输出选项不适用。", "Read Metadata from File": "从文件读取元数据", - "Read metadata updates from XMP sidecars": "从 XMP 附属文件读取元数据更新", "Read mode": "阅读模式", - "Read the SAM License": "阅读 SAM 许可证", "Reading": "正在读取", - "Reading Lightroom catalog…": "正在读取 Lightroom 目录…", - "Reading a sidecar replaces the photo's rating, labels, metadata and edits with what it says.": "读取附属文件会用其内容替换照片的评级、标签、元数据和编辑。", "Reading folder…": "正在读取文件夹…", - "Real name": "真实姓名", - "Recent": "最近使用", - "Recent Keywords": "最近使用的关键字", - "Recent Keywords is built in: choose another name": "“最近使用的关键字”为内置集:请另选名称", "Recently Added": "最近添加", "Recently Deleted": "最近删除", - "Recognition models": "识别模型", "Red": "红", "Red Eye": "红眼", "Red Hue": "红色色相", @@ -1252,61 +1121,38 @@ "Red Primary": "红色原色", "Red Saturation": "红色饱和度", "Red channel": "红色通道", - "Red label": "红色标签", "Red/Cyan Fringe": "红/青边缘", "Redo": "重做", "Reference": "参考", "Reference View": "参考视图", "Reference the files where they are": "在原地引用文件", - "Refine": "细化", - "Refine AI Mask Detail": "细化 AI 蒙版细节", - "Refine Edges": "细化边缘", "Refine Mask Edges": "细化蒙版边缘", - "Refine Saturation": "细化饱和度", - "Refining the mask's detail…": "正在细化蒙版细节…", "Refresh Source": "刷新源", "Refresh Source (/)": "刷新源 (/)", "Reject": "排除", "Reject below focus": "排除对焦低于此值的照片", "Rejected": "已排除", "Rejects": "排除的照片", - "Reliability": "可靠性", - "Relink Photo": "重新链接照片", - "Relink photos whose file was renamed or moved": "重新链接文件被重命名或移动的照片", - "Relinked": "已重新链接", - "Reload Changed Files": "重新载入已更改的文件", "Reload from Disk": "从磁盘重新读取", "Remove": "移除", "Remove (H)": "移除 (H)", "Remove CA": "移除色差", "Remove Chromatic Aberration": "移除色差", - "Remove Denoise Model": "移除降噪模型", - "Remove Disk from Library": "从图库中移除磁盘", - "Remove Disk from Library…": "从图库中移除磁盘…", - "Remove Face Box": "移除人脸框", - "Remove Face Model": "移除人脸模型", - "Remove Folder from Library": "从图库中移除文件夹", - "Remove a filter above, or choose Clear all": "请移除上方的某个筛选条件,或选择“全部清除”", + "Camera local tone mapping": "相机局部色调映射", + "Apply the local tone mapping stored in the raw (Apple ProRAW), as the camera renders it": "按相机的渲染方式应用 raw 中保存的局部色调映射(Apple ProRAW)", "Remove from Album": "从相册中移除", "Remove from Favorites": "从收藏夹中移除", - "Remove from Library…": "从图库中移除…", "Remove from Local": "从本地移除", "Remove from Photo": "从照片中移除", "Remove from Selected Photos": "从所选照片中移除", "Remove from Stack": "从堆叠中移出", - "Remove from Target Album": "从目标相册中移除", "Remove from survey": "从筛选视图中移除", "Remove location info": "移除位置信息", - "Remove missing photos from the library": "从图库中移除缺失的照片", - "Remove shortcut": "移除快捷键", - "Remove this face box (undo with Edit ▸ Undo)": "移除此人脸框(可通过“编辑 → 撤销”恢复)", "Remove this filter": "移除此筛选器", "Remove this group": "删除此组", "Remove this rule": "删除此规则", - "Removed": "已移除", "Rename": "重命名", "Rename Album": "重命名相册", - "Rename Component": "重命名组件", "Rename Folder": "重命名文件夹", "Rename Folder…": "重命名文件夹…", "Rename Keyword": "重命名关键字", @@ -1321,12 +1167,8 @@ "Rename…": "重命名…", "Rendering": "正在渲染", "Rendering…": "正在渲染…", - "Reorder Album": "重排相册", - "Reorder Masks": "重新排序蒙版", "Reset": "重置", - "Reset All": "全部重置", "Reset All Curves": "重置所有曲线", - "Reset All Keyboard Shortcuts": "重置所有键盘快捷键", "Reset Before to Import State": "将修改前重置为导入时的状态", "Reset Control": "重置控件", "Reset Crop": "重置裁剪", @@ -1336,7 +1178,6 @@ "Reset Tone Curve": "重置色调曲线", "Reset all curves: point curves (all channels) and parametric (double-click a channel to reset only that one)": "重置所有曲线:点曲线(所有通道)和参数曲线(双击某个通道可单独重置该通道)", "Reset all edits (Cmd+Shift+R)": "重置所有编辑 (Cmd+Shift+R)", - "Reshuffle": "重新随机排序", "Resize Face Box": "调整人脸框大小", "Resize to": "调整大小为", "Resolution (ppi)": "分辨率 (ppi)", @@ -1363,7 +1204,6 @@ "Roundness": "圆度", "Rules": "规则", "SAM 3 download stopped: it resumes where it left off next time.": "SAM 3 下载已停止:下次将从中断处继续。", - "Sa": "六", "Same as raw default": "与 RAW 默认设置相同", "Sample Mask Color": "取样蒙版颜色", "Sample a colour on the photo": "在照片上取样颜色", @@ -1386,7 +1226,6 @@ "Save as Preset": "保存为预设", "Save as Set…": "存储为标签集…", "Save as Smart Album…": "保存为智能相册…", - "Save as a new set": "另存为新关键字集", "Save as set": "保存为集", "Save as…": "另存为…", "Save preset": "保存预设", @@ -1394,14 +1233,12 @@ "Scale": "缩放", "Scan failed": "扫描失败", "Scanning": "正在扫描", - "Scanning…": "正在扫描…", "Scores stay on the photos: filter or make smart albums with Focus and Best of Similar Shots.": "评分会保留在照片上:可使用“对焦”和“相似照片中的最佳”进行筛选或创建智能相册。", "Screen": "屏幕", "Scroll menu down": "向下滚动菜单", "Scroll menu up": "向上滚动菜单", "Search": "搜索", "Search Photos": "搜索照片", - "Search albums": "搜索相册", "Search commands or keys": "搜索命令或按键", "Search…": "搜索…", "Seasons": "四季", @@ -1415,14 +1252,12 @@ "Select a photo to edit": "选择要编辑的照片", "Select by": "选择方式", "Select by Flag, Rating or Label": "按旗标、星级或颜色标签选择", - "Select photos to give them keywords.": "选择照片以为其添加关键字。", "Select photos, then choose View → Survey (N)": "选择照片,然后选择 视图 → 筛选 (N)", "Select two photos, then choose View → Compare (Shift+C)": "选择两张照片,然后选择 视图 → 比较 (Shift+C)", "Selected": "已选择", "Selecting…": "正在选择…", "Selenium Tone": "硒调色", "Send Feedback…": "发送反馈…", - "Separated by commas": "用逗号分隔", "Sepia Tone": "棕褐色调", "Sequence number zero-padded to N digits (1–9)": "补零至 N 位的序号(1–9)", "Sequence number, counting from the start number": "从起始编号开始计数的序号", @@ -1450,7 +1285,6 @@ "Set date & time": "设置日期和时间", "Set name": "关键字集名称", "Set the mix from the photo's colours": "根据照片颜色设置混合", - "Set up AI Denoise…": "设置 AI 降噪…", "Settings": "设置", "Settings file problem": "设置文件有问题", "Settings new raw photos start from. Changing them doesn't touch photos already in the library.": "新 RAW 照片的初始设置。更改这些设置不会影响图库中已有的照片。", @@ -1481,7 +1315,6 @@ "Show Mask Overlay": "显示蒙版叠加", "Show Mask Pins": "显示蒙版图钉", "Show Original": "显示原图", - "Show Person": "显示人物", "Show Photo Counts": "显示照片数量", "Show Photos with Keyword": "显示包含该关键字的照片", "Show Pins": "显示图钉", @@ -1501,7 +1334,6 @@ "Show the template tags; click one to insert it": "显示模板标签;单击即可插入", "Show/Hide Mask": "显示/隐藏蒙版", "Show/hide My Photos panel": "显示/隐藏我的照片面板", - "Shutter Speed": "快门速度", "Shutter speed": "快门速度", "Side panels": "侧面板", "Sidecar names": "附属文件名", @@ -1509,8 +1341,6 @@ "Size (% of short edge)": "大小 (占短边 %)", "Size limit": "大小限制", "Skip": "跳过", - "Skipped: files already in the library elsewhere": "已跳过:已在图库其他位置的文件", - "Skipped: files that can't be read": "已跳过:无法读取的文件", "Sky": "天空", "Slideshow": "幻灯片播放", "Slideshow paused": "幻灯片播放已暂停", @@ -1528,20 +1358,14 @@ "Soft Pastel": "柔和粉彩", "Soft Proofing": "软打样", "Soft Skin": "柔和肤质", - "Some rules need fixing (Edit Smart Album…):": "有些规则需要修正(编辑智能相册…):", "Sort": "排序", - "Sort Albums A–Z": "相册按 A–Z 排序", - "Sort Albums by Name": "相册按名称排序", - "Sort Contents A–Z": "内容按 A–Z 排序", "Sort by": "排序方式", "Source": "来源", "Source code on GitHub": "GitHub 上的源代码", - "Speed": "速度", "Spherical": "球面", "Spiral": "螺旋", "Split Stack": "拆分堆叠", "Spring Fresh": "春日清新", - "Square": "正方形", "Square Grid": "正方形网格", "Square Grid (G toggles)": "正方形网格(按 G 切换)", "Square Grid: show file names and formats": "正方形网格:显示文件名和格式", @@ -1553,26 +1377,21 @@ "Start at": "起始编号", "Start number": "起始编号", "Starting": "正在开始", - "Starting…": "正在启动…", "Startup": "启动", "State / Province": "省 / 州", "Stop": "停止", "Stop Building Previews": "停止构建预览", "Stop Using as Target Album": "不再用作目标相册", - "Stopped": "已停止", - "Stopped by an error (see the log)": "因错误而停止(见日志)", "Stopping…": "正在停止…", "Straighten": "拉直", "Straighten Tool": "拉直工具", "Strong Contrast": "强烈对比度", "Style": "样式", - "Su": "日", "Subfolder": "子文件夹", "Subfolder name": "子文件夹名称", "Subject": "主题", "Subtract": "减去", "Subtract from this mask": "从此蒙版中减去", - "Suggest Names for Faces": "为人脸建议名称", "Suggestions": "建议", "Summer Haze": "夏日薄雾", "Sunday": "星期日", @@ -1585,11 +1404,6 @@ "Swap Compare Photos": "交换对比照片", "Switch light / dark theme": "切换浅色/深色主题", "Sync Settings": "同步设置", - "Synchronize": "同步", - "Synchronize Folder": "同步文件夹", - "Synchronize Folder…": "同步文件夹…", - "Synchronizing…": "正在同步…", - "Synonyms": "同义词", "System": "系统", "System Info": "系统信息", "System Info…": "系统信息…", @@ -1607,46 +1421,24 @@ "Temperature": "色温", "Template": "模板", "Temporary session — your library isn't open, and nothing you do here is saved.": "临时会话 — 图库未打开,此处所做的任何操作都不会保存。", - "Test Denoise Model": "测试降噪模型", - "Test Face Model": "测试人脸模型", - "Tested and works on this computer": "已在本机测试通过并可正常使用", "Text": "文字", "Text direction": "文字方向", "Texture": "纹理", - "Th": "四", - "That includes the photos in the folders inside it.": "其中文件夹里的照片也包括在内。", "That key can't be used as a shortcut": "该按键不能用作快捷键", "The SAM 3 model is installed: Object and Describe masks are ready.": "SAM 3 模型已安装:对象蒙版和描述蒙版可以使用了。", - "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "降噪模型已安装并正在使用:细节面板中的 AI 降噪数量滑块现已可用", "The download didn't work:": "下载失败:", - "The graphics card is set up when the next photo is made.": "将在生成下一张照片时初始化显卡。", - "The library is up to date with this folder.": "图库与此文件夹一致。", - "The model is kept on this computer only. LightCraft never uploads or shares it.": "模型仅保存在本机。LightCraft 绝不会上传或分享它。", "The number at the end of the original name (IMG_0042 → 0042)": "原始名称末尾的数字(IMG_0042 → 0042)", "The original extension is always added; {ext} only puts it inside the name as well.": "始终会添加原始扩展名;{ext} 只是额外把它放进名称中。", "The original files, unchanged, each with an XMP sidecar holding its edits.": "保留原始文件不变,每个文件附带一个保存其编辑内容的 XMP 附属文件。", "The photo has these settings": "此照片具有这些设置", - "The processor does the work.": "由处理器完成运算。", - "Their photos and the keywords below them come together under one keyword.": "它们的照片和下级关键字将合并到同一个关键字下。", "Theme": "主题", - "There is no model file to install": "没有可安装的模型文件", "They can be restored from Recently Deleted until it is emptied.": "在“最近删除”被清空之前,都可以从那里恢复。", - "They move to Recently Deleted and can be restored; no file on disk is touched.": "它们会移到“最近删除”,可以恢复;磁盘上的文件不会被改动。", "Thirds": "三分法", - "This Computer": "此电脑", - "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚无法下载模型。如需手动安装,请将 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的文件夹:放入后即可使用“对象”和“描述”。", - "This build cannot run denoise models.": "此版本无法运行降噪模型。", - "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "此版本没有存放降噪模型的位置,因此无法使用 AI 降噪。(桌面版可以。)", - "This file cannot be used yet": "此文件暂时无法使用", "This folder is not available (drive disconnected?). Smart previews are not built or used until it is back or you choose another folder.": "此文件夹不可用(驱动器已断开连接?)。在它恢复或您选择其他文件夹之前,不会构建或使用智能预览。", - "This folder isn't there: it was moved or renamed, or its disk isn't connected.": "此文件夹不存在:它已被移动或重命名,或其磁盘未连接。", - "This group is empty: add a rule or remove it.": "此组为空:请添加规则或将其删除。", - "This is the album you're editing": "这是正在编辑的相册", "This photo can't be opened": "无法打开此照片", "Threshold": "阈值", "Thumbnail cache": "缩略图缓存", "Thursday": "星期四", - "Tick the box to accept the model's terms first": "请先勾选以接受该模型的条款", "Time zone": "时区", "Tint": "色调", "Title": "标题", @@ -1663,13 +1455,9 @@ "Triangle": "三角形", "Try Again": "重试", "Try Saving Again": "再次尝试保存", - "Try again": "重试", - "Try the graphics card again": "重新尝试使用显卡", - "Tu": "二", "Tuesday": "星期二", "Tungsten": "钨丝灯", "Turn Off": "关闭", - "Turn off": "关闭", "Turn on Include subfolders, or pick another folder under Local": "开启“包括子文件夹”,或在“本地”下选择其他文件夹", "Type": "类型", "Uncheck All": "全部取消选中", @@ -1680,11 +1468,8 @@ "Unflag": "取消旗标", "Unflagged": "无旗标", "Ungroup Stack": "取消堆叠", - "Unknown": "未知", "Unknown Date": "未知日期", - "Unknown licence": "许可证未知", "Unknown tags stay as typed — check the preview for a {typo}.": "未知标签会按原样保留 — 请在预览中检查是否有{typo}。", - "Unnamed Faces": "未命名人脸", "Unrated": "未评级", "Unreadable": "无法读取", "Update": "更新", @@ -1697,7 +1482,6 @@ "Updated the edits saved in the external editor": "已更新在外部编辑器中保存的编辑", "Upright": "垂直校正", "Urban": "都市", - "Use": "使用", "Use Keyword Set": "使用关键字集", "Use Library Folder": "使用图库文件夹", "Use camera-specific defaults": "使用相机专属默认值", @@ -1713,8 +1497,6 @@ "Vertical": "垂直", "Vertical text": "竖排文字", "Vibrance": "鲜艳度", - "Video": "视频", - "Video Duration": "视频时长", "Videos": "视频", "View": "视图", "Viewing and sharing": "查看与分享", @@ -1728,15 +1510,12 @@ "Visualize range": "显示范围", "Vivid": "鲜艳", "Vivid Pop": "明快跳色", - "Wait for the import to finish": "请等待导入完成", - "Waiting to start…": "等待启动…", "Warm Film": "暖调胶片", "Warm Glow": "暖光", "Warm Print": "暖调印相", "Warm Table": "暖调餐桌", "Watched folder": "监视的文件夹", "Watermark": "水印", - "We": "三", "Wednesday": "星期三", "What's New": "新功能", "When rated or hovered": "评级或悬停时", @@ -1748,12 +1527,10 @@ "Width (% of photo)": "宽度(照片百分比)", "Width (px)": "宽度 (px)", "Width × Height": "宽度 × 高度", - "Will Export": "将导出", "Will be replaced": "将替换", "Will be restored": "将恢复", "Window": "窗口", "Winter Blue": "冬日蓝调", - "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "适用于采用标准(拜耳)传感器的相机 RAW 文件。其他文件仍使用常规降噪。", "XMP Sidecar Preferences": "XMP 附属文件偏好设置", "XMP sidecars": "XMP 附属文件", "Year": "年", @@ -1780,32 +1557,20 @@ "are empty": "为空", "aren't empty": "不为空", "blue": "蓝色", - "cached picture": "张已缓存的照片", - "cached pictures": "张已缓存的照片", - "can't be tested that way": "不能这样判断", - "can't match any rating from 0 to 5": "不匹配 0 到 5 的任何星级", - "can't test a folder; choose an album in it": "无法判断文件夹;请选择其中的相册", "comma, separated": "逗号分隔", - "commercial use allowed": "允许商用", - "commercial use subject to GPL conditions": "商用须遵循 GPL 条款", "contains": "包含", "copyrighted": "受版权保护", "day 01–31": "日 01–31", "days": "天", - "denoiseModel.accept": "denoiseModel.accept", "discarding smart previews": "正在丢弃智能预览", "doesn't contain": "不包含", "e.g. sky · the red car · car, road": "例如(英文):sky · the red car · car, road", "ends with": "结尾为", "false": "否", "green": "绿色", - "has the first date after the second": "第一个日期晚于第二个", "hour 00–23": "时 00–23", "hours": "小时", "image": "图像", - "in use": "正在使用", - "includes the folders inside it": "包含其中的文件夹", - "installed": "已安装", "is": "是", "is <": "小于", "is >": "大于", @@ -1815,45 +1580,23 @@ "is empty": "为空", "is in the last": "在最近", "is unknown": "未知", - "is yes or no": "只能是“是”或“否”", "is ≤": "小于等于", "is ≥": "大于等于", "isn't": "不是", - "isn't a field rules know": "不是已知的字段", "isn't empty": "不为空", "isn't in the last": "不在最近", - "isn't one of the choices": "不是可选项之一", "keep names — or e.g. {date}_{seq:3}": "保留名称 — 或如 {date}_{seq:3}", "latitude, longitude": "纬度, 经度", - "makes AI Denoise available": "以启用 AI 降噪", "memory only": "仅内存", "minute": "分", "month 01–12": "月 01–12", "months": "个月", - "names an album that no longer exists": "指定的相册已不存在", - "needs a date like 2026, 2026-08 or 2026-08-14": "需要类似 2026、2026-08 或 2026-08-14 的日期", - "needs a number": "需要一个数字", - "needs a number more than 0": "需要大于 0 的数字", - "needs a time like 1/250 or 2": "需要类似 1/250 或 2 的快门速度", - "needs an album": "需要选择相册", - "needs hours, days, weeks, months or years": "需要小时、天、周、月或年", - "needs something to look for (or use “are empty”)": "需要要查找的内容(或使用“为空”)", - "needs something to look for (or use “aren't empty”)": "需要要查找的内容(或使用“不为空”)", - "needs something to look for (or use “is empty”)": "需要要查找的内容(或使用“为空”)", - "needs something to look for (or use “isn't empty”)": "需要要查找的内容(或使用“不为空”)", - "needs text": "需要文本", - "needs two values": "需要两个值", - "non-commercial use only": "仅限非商用", "none": "无", "none (CPU rendering)": "无(CPU 渲染)", "none of": "均不符合", "not available here": "此处不可用", "of the following rules:": "以下规则:", "or": "或", - "photo has its denoised picture.": "张照片已有降噪结果。", - "photo waiting": "张照片等待中", - "photos have their denoised picture.": "张照片已有降噪结果。", - "photos waiting": "张照片等待中", "pick": "留用", "previews": "预览", "publicDomain": "公共领域", @@ -1861,17 +1604,10 @@ "raw": "RAW", "red": "红色", "reject": "排除", - "save goes through.": "才能完成保存。", "saved in": "保存在", "second": "秒", - "smart album value|No": "智能相册取值|否", - "smart album value|Yes": "智能相册取值|是", "smart previews": "智能预览", - "starts using it for AI Denoise": "开始用它进行 AI 降噪", "starts with": "开头为", - "terms unclear": "条款不明确", - "the graphics card": "显卡", - "the graphics card is not used": "未使用显卡", "the photos in view": "视图中的照片", "the selected photos": "所选照片", "this photo": "这张照片", @@ -1879,7 +1615,6 @@ "unknown": "未知", "video": "视频", "weeks": "周", - "would make this album include itself through other smart albums": "会通过其他智能相册让此相册包含自身", "year, 2 digits": "年,2 位", "year, 4 digits": "年,4 位", "years": "年", @@ -1892,12 +1627,292 @@ "≥ at least": "≥ 至少", "日本語": "日本語", "繁體中文(台灣)": "繁體中文(台灣)", + "In Recently Deleted": "在“最近删除”中", + "Remove Folder from Library": "从图库中移除文件夹", + "Remove from Library…": "从图库中移除…", + "This Computer": "此电脑", + "includes the folders inside it": "包含其中的文件夹", + "They move to Recently Deleted and can be restored; no file on disk is touched.": "它们会移到“最近删除”,可以恢复;磁盘上的文件不会被改动。", + "Moves the photos imported from this folder to Recently Deleted; no file is touched": "将从此文件夹导入的照片移到“最近删除”;不会改动任何文件", + "Remove Disk from Library…": "从图库中移除磁盘…", + "Moves every photo imported from this disk to Recently Deleted; no file is touched": "将从此磁盘导入的所有照片移到“最近删除”;不会改动任何文件", + "Collapse": "折叠", + "Expand": "展开", + "Remove Disk from Library": "从图库中移除磁盘", + "All the photos imported from this disk are included.": "包括从此磁盘导入的所有照片。", + "That includes the photos in the folders inside it.": "其中文件夹里的照片也包括在内。", + "How to install the model": "如何安装模型", + "Install the SAM 3 Model": "安装 SAM 3 模型", + "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚无法下载模型。如需手动安装,请将 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的文件夹:放入后即可使用“对象”和“描述”。", + "Open Log Folder": "打开日志文件夹", + "Back to this person": "返回此人", + "AI Denoise": "AI 降噪", + "AI Denoise Model": "AI 降噪模型", + "AI Denoise is off: press Use on a model.": "AI 降噪已关闭:请在某个模型上点击“使用”。", + "AI Denoise needs a model. Install one in Settings.": "AI 降噪需要模型。请在设置中安装一个。", + "Accept & Download": "接受并下载", + "Already installed. Remove it before installing a replacement.": "已安装。安装替代模型前请先将其移除。", + "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "自动模式会在显卡快于处理器时使用显卡:两者都会在首次生成照片时于本机计时比较。", + "Cache": "缓存", + "Checking and installing the model…": "正在检查并安装模型…", + "Checking…": "正在检查…", + "Clear cache": "清除缓存", + "Denoise Models": "降噪模型", + "Denoise Settings": "降噪设置", + "Denoise all photos with an Amount": "对所有设置了数量的照片进行降噪", + "Downloaded and checked. Installing…": "已下载并校验完成。正在安装…", + "Failed its last test": "上次检测未通过", + "Graphics card": "显卡", + "I have read these terms and accept them for my own use": "我已阅读这些条款,并为个人使用表示接受", + "Inspect Denoise Model": "查看降噪模型", + "Install Denoise Model": "安装降噪模型", + "Install from file…": "从文件安装…", + "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "请自行安装 ONNX 模型,并在其旁边放置 denoise-model.json。此版本不提供模型下载。", + "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "该照片的降噪结果尚未生成,而且不会自动生成(设置、AI 降噪)。", + "LightCraft does not know this model, so it assumed:": "LightCraft 不认识此模型,因此按以下假设处理:", + "Limit": "上限", + "Make it now": "立即生成", + "Make the denoised picture of the photos I am looking at": "为我正在查看的照片生成降噪结果", + "Next up for AI Denoise…": "接下来的 AI 降噪任务…", + "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "在你接受之前不会下载任何内容。模型仅保存在本机;LightCraft 绝不会上传或分享它。", + "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "只对 AI 降噪数量大于 0 的照片生成结果。导出时若缺少结果则会先生成。", + "Open page": "打开页面", + "Open the model's page": "打开该模型的页面", + "Opens the model's own page in your browser, to read about it or get the file yourself.": "在浏览器中打开该模型的主页,以便查阅说明或自行获取文件。", + "Processor": "处理器", + "Set up AI Denoise…": "设置 AI 降噪…", + "Speed": "速度", + "Starting…": "正在启动…", + "Tested and works on this computer": "已在本机测试通过并可正常使用", + "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "降噪模型已安装并正在使用:细节面板中的 AI 降噪数量滑块现已可用", + "The graphics card is set up when the next photo is made.": "将在生成下一张照片时初始化显卡。", + "The model is kept on this computer only. LightCraft never uploads or shares it.": "模型仅保存在本机。LightCraft 绝不会上传或分享它。", + "The processor does the work.": "由处理器完成运算。", + "There is no model file to install": "没有可安装的模型文件", + "This build cannot run denoise models.": "此版本无法运行降噪模型。", + "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "此版本没有存放降噪模型的位置,因此无法使用 AI 降噪。(桌面版可以。)", + "This file cannot be used yet": "此文件暂时无法使用", + "Tick the box to accept the model's terms first": "请先勾选以接受该模型的条款", + "Try again": "重试", + "Try the graphics card again": "重新尝试使用显卡", + "Turn off": "关闭", + "Unknown licence": "许可证未知", + "Use": "使用", + "Waiting to start…": "等待启动…", + "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "适用于采用标准(拜耳)传感器的相机 RAW 文件。其他文件仍使用常规降噪。", + "cached picture": "张已缓存的照片", + "cached pictures": "张已缓存的照片", + "commercial use allowed": "允许商用", + "denoiseModel.accept": "denoiseModel.accept", + "in use": "正在使用", + "installed": "已安装", + "makes AI Denoise available": "以启用 AI 降噪", + "non-commercial use only": "仅限非商用", + "photo has its denoised picture.": "张照片已有降噪结果。", + "photo waiting": "张照片等待中", + "photos have their denoised picture.": "张照片已有降噪结果。", + "photos waiting": "张照片等待中", + "starts using it for AI Denoise": "开始用它进行 AI 降噪", + "terms unclear": "条款不明确", + "the graphics card": "显卡", + "the graphics card is not used": "未使用显卡", + "Cancel Denoise": "取消降噪", + "Cancel Denoise Model Download": "取消降噪模型下载", + "Choose Denoise Model": "选择降噪模型", + "Clear Denoise Cache": "清除降噪缓存", + "Denoise Model Downloads": "降噪模型下载", + "Denoise Photos": "照片降噪", + "Denoise Pump": "降噪队列", + "Denoise Status": "降噪状态", + "Download Denoise Model": "下载降噪模型", + "Remove Denoise Model": "移除降噪模型", + "Test Denoise Model": "测试降噪模型", + "AI Denoise is enabled on the requesting photo": "已在发起请求的照片上启用 AI 降噪", + "After installation, AI Denoise will turn on for the requesting photo.": "安装完成后,将为发起请求的照片开启 AI 降噪。", + "Cancel pending action": "取消待处理操作", + "Install a denoise model to enable AI Denoise on this photo": "安装降噪模型后即可在此照片上启用 AI 降噪", + "Pending photo action cancelled": "已取消待处理的照片操作", + "Pending photo action cancelled because its library or photo changed": "待处理的照片操作已取消,因为其所属图库或照片发生了变化", + "ONNX model": "ONNX 模型", + "commercial use subject to GPL conditions": "商用须遵循 GPL 条款", + "Decrease Rating": "降低星级", + "Increase Rating": "提高星级", + "Click to type a value": "单击以输入数值", + "Angle": "角度", + "Video": "视频", + "Copyrighted": "受版权保护", + "Public Domain": "公共领域", + "Unknown": "未知", + "Landscape (wide)": "横向", + "Portrait (tall)": "纵向", + "Square": "正方形", + "In Color": "彩色", + "No Label": "无标签", + "Choose an album…": "选择相册…", + "No albums yet": "还没有相册", + "Fix the marked rules to save this album.": "请修正标记的规则以保存此相册。", + "Some rules need fixing (Edit Smart Album…):": "有些规则需要修正(编辑智能相册…):", + "isn't a field rules know": "不是已知的字段", + "can't be tested that way": "不能这样判断", + "is yes or no": "只能是“是”或“否”", + "needs a number": "需要一个数字", + "needs a time like 1/250 or 2": "需要类似 1/250 或 2 的快门速度", + "needs a date like 2026, 2026-08 or 2026-08-14": "需要类似 2026、2026-08 或 2026-08-14 的日期", + "has the first date after the second": "第一个日期晚于第二个", + "needs two values": "需要两个值", + "needs a number more than 0": "需要大于 0 的数字", + "needs hours, days, weeks, months or years": "需要小时、天、周、月或年", + "isn't one of the choices": "不是可选项之一", + "needs something to look for (or use “is empty”)": "需要要查找的内容(或使用“为空”)", + "needs something to look for (or use “isn't empty”)": "需要要查找的内容(或使用“不为空”)", + "needs text": "需要文本", + "can't match any rating from 0 to 5": "不匹配 0 到 5 的任何星级", + "needs an album": "需要选择相册", + "names an album that no longer exists": "指定的相册已不存在", + "can't test a folder; choose an album in it": "无法判断文件夹;请选择其中的相册", + "This group is empty: add a rule or remove it.": "此组为空:请添加规则或将其删除。", + "needs something to look for (or use “are empty”)": "需要要查找的内容(或使用“为空”)", + "needs something to look for (or use “aren't empty”)": "需要要查找的内容(或使用“不为空”)", + "Mo": "一", + "Tu": "二", + "We": "三", + "Th": "四", + "Fr": "五", + "Sa": "六", + "Su": "日", + "Pick a date": "选择日期", + "would make this album include itself through other smart albums": "会通过其他智能相册让此相册包含自身", + "Search albums": "搜索相册", + "No albums match": "没有匹配的相册", + "This is the album you're editing": "这是正在编辑的相册", + "It tests this album, so testing it back would loop": "它引用了此相册,反过来引用会形成循环", + "Copy Name": "副本名称", + "In a Stack": "在堆叠中", + "File Extension": "文件扩展名", + "Video Duration": "视频时长", + "Keyword Count": "关键字数量", + "People Count": "人物数量", + "Shutter Speed": "快门速度", + "Cropped": "已裁剪", + "Keywords & People": "关键字与人物", + "Description": "描述", + "Camera Info": "相机信息", + "Develop": "编辑", + "Keyword List": "关键字列表", + "Filter Keywords": "过滤关键字", + "No keywords yet": "还没有关键字", + "No keywords match": "没有匹配的关键字", + "Keyword Name": "关键字名称", + "Synonyms": "同义词", + "Separated by commas": "用逗号分隔", + "Keyword Tag Options": "关键字标记选项", + "Include on Export": "导出时包含", + "Export Containing Keywords": "导出上级关键字", + "Export Synonyms": "导出同义词", + "Create Keyword Tag": "创建关键字标记", + "Edit Keyword Tag": "编辑关键字标记", + "Create": "创建", + "No photo has it; the keywords below it go too.": "没有照片使用它;其下的关键字也会一并删除。", + "Edit Keyword Tag…": "编辑关键字标记…", + "Put New Keywords Inside This Keyword": "将新关键字放入此关键字中", + "Purge Unused Keywords": "清除未使用的关键字", + "Delete Keyword…": "删除关键字…", + "Drop here for the top level": "拖放到此处以移至顶层", + "Their photos and the keywords below them come together under one keyword.": "它们的照片和下级关键字将合并到同一个关键字下。", + "Select photos to give them keywords.": "选择照片以为其添加关键字。", + "Add to All Selected Photos": "添加到所有所选照片", + "Will Export": "将导出", + "No keywords are exported": "没有要导出的关键字", + "& Containing": "及上级", + "Edit Set…": "编辑关键字集…", + "Edit Keyword Set": "编辑关键字集", + "Keywords (⌥1–⌥9)": "关键字 (⌥1–⌥9)", + "A keyword set needs a name": "关键字集需要名称", + "Save as a new set": "另存为新关键字集", + "Recent Keywords is built in: choose another name": "“最近使用的关键字”为内置集:请另选名称", + "Import Keywords…": "导入关键字…", + "Export Keywords…": "导出关键字…", + "Import Keywords": "导入关键字", + "Export Keywords": "导出关键字", + "Keyword Lists": "关键字列表", + "Synchronize Folder…": "同步文件夹…", + "Synchronize Folder": "同步文件夹", + "Synchronize": "同步", + "Scanning…": "正在扫描…", + "Find photos added to or missing from this folder on disk, and XMP sidecars changed by other apps": "查找磁盘上此文件夹中新增或缺失的照片,以及被其他应用更改的 XMP 附属文件", + "Bring the library up to date with this folder and the folders inside it:": "让图库与此文件夹及其中的文件夹保持一致:", + "Import new photos": "导入新照片", + "Remove missing photos from the library": "从图库中移除缺失的照片", + "Read metadata updates from XMP sidecars": "从 XMP 附属文件读取元数据更新", + "The library is up to date with this folder.": "图库与此文件夹一致。", + "Missing photos move to Recently Deleted and can be restored; no file on disk is touched.": "缺失的照片会移到“最近删除”,可以恢复;磁盘上的文件不会被改动。", + "Reading a sidecar replaces the photo's rating, labels, metadata and edits with what it says.": "读取附属文件会用其内容替换照片的评级、标签、元数据和编辑。", + "Skipped: files already in the library elsewhere": "已跳过:已在图库其他位置的文件", + "Skipped: files that can't be read": "已跳过:无法读取的文件", + "This folder isn't there: it was moved or renamed, or its disk isn't connected.": "此文件夹不存在:它已被移动或重命名,或其磁盘未连接。", + "Relink photos whose file was renamed or moved": "重新链接文件被重命名或移动的照片", + "A folder is already being synchronized": "已有文件夹正在同步", + "Synchronizing…": "正在同步…", + "Relinked": "已重新链接", + "Removed": "已移除", + "Metadata read": "已读取元数据", + "Failed": "失败", + "Stopped": "已停止", + "Stopped by an error (see the log)": "因错误而停止(见日志)", + "Not saved": "未保存", + "A folder is being synchronized": "正在同步文件夹", + "Wait for the import to finish": "请等待导入完成", + "Show less": "收起", + "Quit while tasks are running?": "要在任务运行时退出吗?", + "Exporting contact sheet": "正在导出联系表", + "Reading folder": "正在读取文件夹", + "Scanning folder": "正在扫描文件夹", + "Building previews": "正在构建预览", + "Building smart previews": "正在构建智能预览", + "Discarding smart previews": "正在丢弃智能预览", + "Importing Lightroom catalog": "正在导入 Lightroom 目录", + "Reading Lightroom catalog": "正在读取 Lightroom 目录", + "Merging": "正在合并", + "Downloading model": "正在下载模型", + "Finding faces": "正在查找人脸", + "Synchronizing folder": "正在同步文件夹", + "Looking for changes": "正在查找更改", + "Add Face Model…": "添加人脸模型…", "Cancel Face Model Download": "取消人脸模型下载", "Cancel Pending Photo Action": "取消待处理的照片操作", "Choose Face Recognition Model": "选择人脸识别模型", "Choose a model below (Use) to start.": "请在下方选择模型(点击“使用”)以开始。", + "Detect Faces": "检测人脸", + "Download Face Model": "下载人脸模型", + "Drag Crop Handle": "拖动裁切手柄", "Evaluate Face Recognition": "评估人脸识别", + "Face Model Downloads": "人脸模型下载", + "Face Models": "人脸模型", + "Face Recognition On/Off": "人脸识别开关", + "Face detector": "人脸检测器", + "Face recognition": "人脸识别", + "Faces": "人脸", + "Faces of a Person": "某个人的人脸", + "Go back to listing them by name": "返回按名称列出", + "Index Faces": "索引人脸", "Index Faces in the Background": "在后台索引人脸", + "Inspect Face Model File": "查看人脸模型文件", + "Install Face Model": "安装人脸模型", + "Library Folders": "图库文件夹", + "Name Face": "命名人脸", + "Name Faces": "命名人脸", "Recognise faces (experimental)": "识别面部(实验功能)", - "Go back to listing them by name": "返回按名称列出" + "Recognition models": "识别模型", + "Remove Face Model": "移除人脸模型", + "Reorder Album": "重排相册", + "Show Person": "显示人物", + "Sort Albums A–Z": "相册按 A–Z 排序", + "Sort Albums by Name": "相册按名称排序", + "Sort Contents A–Z": "内容按 A–Z 排序", + "Suggest Names for Faces": "为人脸建议名称", + "Test Face Model": "测试人脸模型", + "Unnamed Faces": "未命名人脸", + "save goes through.": "才能完成保存。", + "smart album value|No": "智能相册取值|否", + "smart album value|Yes": "智能相册取值|是" } From ab438b05ca94113f6213b460515e9ca01f34e7d7 Mon Sep 17 00:00:00 2001 From: goujicheng Date: Sat, 10 Oct 2026 22:16:04 +0800 Subject: [PATCH 07/24] LR-IMP-PREVIEW-READ: a preview reads the raw's header, not the raw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A grid thumbnail and a loupe preview of an unedited raw show the camera's embedded JPEG, which sits near the start of the file — for a 24 MP Fuji RAF, 5.6 MB of a 56 MB file (measured: DSCF9492.RAF). The loader read the file whole and threw the sensor data away, so every preview paid for 50 MB nobody looked at. On a local disk that is merely wasteful; on a network share, where photographers keep libraries, it is the wait: the same preview took 10 s over a GVFS/SMB Wi-Fi share against 0.96 s after this change. - `lightcraft_raw::bounded_preview(bytes)` locates the preview from the header alone and reports its byte range; `embedded_preview_from_range` extracts it, byte for byte what `embedded_preview` returns for the whole file (the pointer is read from the RAF header's fixed u32s at 84/88). - `engine::files::preview_window` reads a 4 KiB prefix, then — because the container reader clamps a slice to the buffer it was given, so a short prefix reports a plausible but truncated 3948 bytes — re-reads exactly the extent the header declares, and takes the window out of the prefix rather than reading those bytes twice. `LIGHTCRAFT_PROFILE=1` prints what each preview cost. - Formats whose preview only the whole file reveals (CR3 boxes, TIFF pointer tags past the prefix, CRW/MRW/X3F scans), a lying pointer, a short read and a file that changes underneath us all fall back to the whole-file path: a damaged header never costs the user their preview. - Tests: the header range matches `embedded_preview` byte for byte, with a clamped prefix, a padded extent, a late pointer, a lying pointer and a non-JPEG window covered; on a synthetic RAF with a 200 KB tail the quick path and the whole-file path produce identical pixels and the read stays under half the file; `crates/engine/tests/preview_read.rs` (opt-in, `#[ignore]`) measures a real file on a share. No raw fixture is committed. - `docs/raw-preview-reads.md` records the mechanism and the numbers. Co-Authored-By: DeepSeek V3.2 --- README.md | 1 + crates/engine/src/files.rs | 181 +++++++++++++++++++++++++++- crates/engine/tests/preview_read.rs | 42 +++++++ crates/raw/src/lib.rs | 5 +- crates/raw/src/preview.rs | 163 +++++++++++++++++++++++++ docs/parity.md | 11 +- docs/raw-preview-reads.md | 62 ++++++++++ 7 files changed, 457 insertions(+), 8 deletions(-) create mode 100644 crates/engine/tests/preview_read.rs create mode 100644 docs/raw-preview-reads.md diff --git a/README.md b/README.md index 217092fb..45e74007 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,7 @@ LightCraft is young and moving fast. **Where we honestly stand** (details in the - **camera colour calibration:** Sony, Nikon, Panasonic, Fujifilm and Canon CR3 raws have guarded estimates from their camera JPEGs, with built-in ILCE-7CR, ILCE-7M4, X-H2S and X-T4 profiles; measured calibration is missing, and other raws or rejected fits retain a neutral matrix; - **compressed Olympus raws and unsupported CR3 variants:** these use embedded JPEG previews when present. Fujifilm lossless/lossy compressed RAF now decodes sensor data; [verification and existing-library reload instructions](docs/raf-compression.md); - **AI masks and denoise:** subject and sky selection are classical heuristics; + - **network libraries:** a preview reads the raw's header and its embedded JPEG only (56.5 MB → 5.9 MB, 10 s → 1 s on the test share), but the *developed* picture still needs the sensor data over the wire; see [docs/raw-preview-reads.md](docs/raw-preview-reads.md); - **on-screen HDR display, video and the Classic Print / Book / Map modules.** - **What's next:** see [where we're going](ROADMAP.md#where-were-going). diff --git a/crates/engine/src/files.rs b/crates/engine/src/files.rs index 31253f66..b539c9c2 100644 --- a/crates/engine/src/files.rs +++ b/crates/engine/src/files.rs @@ -605,17 +605,144 @@ fn fit_box(max_edge: usize) -> lightcraft_codecs::DecodeOptions { /// Filesystem-backed embedded-preview hook (native). /// -/// Reads the whole file to find the preview, so it holds the memory gate for twice the file's size (the file and the -/// decoded preview): background work (the face scan) waits for room, interactive work is counted and never waits. +/// The quick path reads the file's header and its embedded preview and nothing else (see +/// [`read_preview_window`]); a format whose preview the header cannot point at falls back to the +/// whole-file path, which is what every format used to cost. The memory gate still counts the whole +/// file, which is what the decode of one can hold. pub fn fs_preview_loader() -> PreviewLoader { Arc::new(|path: &str, max_edge: usize| { let len = std::fs::metadata(path).map(|m| usize::try_from(m.len()).unwrap_or(usize::MAX)).unwrap_or(0); let gate = crate::memory::work_gate(); let _permit = if crate::memory::is_background() { gate.acquire(len.saturating_mul(2)) } else { gate.acquire_urgent(len.saturating_mul(2)) }; + if let Some(image) = preview_window(path).ok().flatten().and_then(|(_, window, _)| quick_image_srgb(&window, max_edge)) { + return Some(image); + } embedded_preview_srgb(&std::fs::read(path).ok()?, max_edge) }) } +/// What [`preview_window`] read from a file, so a caller can see what a preview cost. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PreviewRead { + /// Bytes read. + pub read: u64, + /// The file's size. + pub total: u64, + /// Whether the preview came from a bounded read (`false`: the whole file was read, which is what + /// every preview cost before the header path existed). + pub header_path: bool, +} + +impl PreviewRead { + /// The share of the file that was read, `0.0`..=`1.0`. + pub fn fraction(self) -> f64 { + if self.total == 0 { + return 0.0; + } + (self.read as f64 / self.total as f64).clamp(0.0, 1.0) + } +} + +/// The file's header and its embedded preview, read without the rest of the file: `(header bytes, +/// preview bytes, what was read)`. `None` when the header points at no preview this build can find, +/// which is the caller's cue to read the file whole. +/// +/// Why it exists: a 56 MB Fuji RAF keeps its JPEG in the first 5.6 MB (measured: `DSCF9492.RAF`), +/// so reading the file whole to extract it costs ten times the bytes — and on a network share ten +/// times the wait (measured on that file: 2.5 s local against 25.6 s over SMB). `embedded_preview` +/// still needs the whole container, so the header comes back with the window; for a preview taken +/// out of a container that carries no preview colour space of its own (Nikon's maker note) the +/// whole-file fallback stays in charge. +pub fn preview_window(path: &str) -> Result, Vec, PreviewRead)>, String> { + use std::io::{Read, Seek, SeekFrom}; + + let mut file = std::fs::File::open(path).map_err(|e| format!("{path}: {e}"))?; + let total = file.metadata().map_err(|e| format!("{path}: {e}"))?.len(); + let mut prefix = read_prefix(&mut file, path, total)?; + + // What the header itself declares, before the container reader is asked: it clamps a slice to + // the buffer it was given, so a prefix that stops inside the declared preview yields a + // self-consistently short range that looks trustworthy (measured on a real 56 MB DSCF9492.RAF: + // a 4 KiB prefix reports 3948 bytes where the file holds 5.6 MB). Reading the declared extent + // first costs nothing — it lives in the first bytes of every format covered here. + if let Some(extent) = raf_preview_extent(&prefix) + && extent > prefix.len() as u64 + { + prefix = read_prefix_at(&mut file, path, extent.min(total))?; + } + let Some(range) = lightcraft_raw::bounded_preview(&prefix) else { return Ok(None) }; + // A preview at the file's end may have been padded past it: clamp rather than fail. + let end = range.offset.saturating_add(range.window_len).min(total); + let len = usize::try_from(end.saturating_sub(range.offset)).map_err(|_| format!("{path}: preview too large"))?; + // When the prefix already covers the range (the usual case: the prefix was grown to the declared + // extent), take the window out of it. Reading those bytes twice would double a 5.6 MB preview. + let prefix_len = prefix.len() as u64; + let from_prefix = usize::try_from(range.offset).ok().and_then(|start| prefix.get(start..start.checked_add(len)?)).map(<[u8]>::to_vec); + let (window, bytes_read) = match from_prefix { + Some(window) => (window, prefix_len), + None => { + let mut window = vec![0u8; len]; + file.seek(SeekFrom::Start(range.offset)).map_err(|e| format!("{path}: {e}"))?; + file.read_exact(&mut window).map_err(|e| format!("{path}: {e}"))?; + (window, prefix_len + len as u64) + } + }; + + // A lying pointer must not cost the user their preview: hand the caller the whole-file fallback. + if lightcraft_raw::embedded_preview_from_range(&window, range).is_none() { + return Ok(None); + } + let read = PreviewRead { read: bytes_read, total, header_path: true }; + // On a network share the bytes are the wait, so say what this preview actually cost. A window + // taken from the prefix cost nothing beyond the prefix itself. + if lightcraft_pipeline::profiling() { + eprintln!("[profile] preview {path}: read {} of {} bytes (header {}, window {})", read.read, total, prefix_len, window.len()); + } + Ok(Some((prefix, window, read))) +} + +/// The file's first [`lightcraft_raw::PREVIEW_PREFIX`] bytes (or the whole file when it is shorter), +/// which is what every format [`lightcraft_raw::bounded_preview`] covers keeps its pointer in. +fn read_prefix(file: &mut std::fs::File, path: &str, total: u64) -> Result, String> { + let len = lightcraft_raw::PREVIEW_PREFIX.min(usize::try_from(total).unwrap_or(usize::MAX)); + read_prefix_at(file, path, len as u64) +} + +/// The file's first `len` bytes, read from the start. +fn read_prefix_at(file: &mut std::fs::File, path: &str, len: u64) -> Result, String> { + use std::io::{Read, Seek, SeekFrom}; + let len = usize::try_from(len).map_err(|_| format!("{path}: file too large"))?; + let mut prefix = vec![0u8; len]; + file.seek(SeekFrom::Start(0)).map_err(|e| format!("{path}: {e}"))?; + file.read_exact(&mut prefix).map_err(|e| format!("{path}: {e}"))?; + Ok(prefix) +} + +/// The end of a Fuji RAF's preview block, from the two big-endian u32s every RAF header carries at +/// 84 (offset) and 88 (length) — the same fields `lightcraft_raw`'s RAF reader uses, read here +/// without the rest of the file so the prefix can be made big enough first. +fn raf_preview_extent(prefix: &[u8]) -> Option { + if !prefix.starts_with(b"FUJIFILMCCD-RAW") { + return None; + } + let u32_at = |at: usize| prefix.get(at..at + 4).map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]])).map(u64::from); + let (offset, len) = (u32_at(84)?, u32_at(88)?); + (offset > 0 && len > 0).then(|| offset.checked_add(len)).flatten() +} + +/// The display-ready preview of a raw whose embedded JPEG was already located: the counterpart of +/// [`embedded_preview_srgb`] for [`read_preview_window`], which does not re-read the container. +/// `None` when the bytes are not a decodable image. +fn quick_image_srgb(window: &[u8], max_edge: usize) -> Option { + let mut d = lightcraft_codecs::decode(window, fit_box(max_edge)).ok()?; + if d.image.width.max(d.image.height) > max_edge { + d.image = fit(&d.image, max_edge, max_edge, Filter::Box); + d.alpha = None; + } + // The window is the JPEG itself, so its own EXIF carries the orientation; `decode` reports it. + Some(d.to_srgb8().oriented(Orientation::from_exif(d.orientation))) +} + /// The filesystem-backed [`PairLoader`]: the file is read and decoded once, and developed as usual and from the photo's /// denoised picture. It holds two pictures at once, so it counts for more at the memory gate than a plain load. pub fn fs_pair_loader() -> PairLoader { @@ -689,6 +816,56 @@ mod tests { use super::*; use lightcraft_codecs::{ChromaSubsampling, EncodeImage, EncodeMeta, Samples, encode_jpeg}; + /// A RAF whose preview pointer is in its header, then a large raw tail the reader must not need. + fn raf_with_embedded_jpeg(jpeg: &[u8], tail: usize) -> Vec { + let head = 160; + let mut f = vec![0u8; head]; + f.get_mut(..16).expect("head").copy_from_slice(b"FUJIFILMCCD-RAW "); + f.get_mut(84..88).expect("jpeg offset").copy_from_slice(&(head as u32).to_be_bytes()); + f.get_mut(88..92).expect("jpeg length").copy_from_slice(&(jpeg.len() as u32).to_be_bytes()); + f.extend_from_slice(jpeg); + f.extend(std::iter::repeat_n(0x33u8, tail)); + f + } + + /// A raw's preview is read from its header and its JPEG only. Every preview used to cost a + /// whole-file read, which on a 56 MB RAF over a network share is ten times the bytes and ten + /// times the wait (measured: 2.5 s local against 25.6 s over SMB, DSCF9492.RAF). The pixels the + /// quick path produces must equal the whole-file path's, exactly. + #[test] + fn preview_loading_reads_only_the_header_and_the_jpeg() { + let (w, h) = (48u32, 32u32); + let rgb: Vec = (0..w * h * 3).map(|i| (i * 13 % 251) as u8).collect(); + let img = EncodeImage::new(w, h, 3, Samples::U8(&rgb)); + let jpeg = encode_jpeg(&img, 90, ChromaSubsampling::S420, &EncodeMeta::default()).unwrap(); + let file = raf_with_embedded_jpeg(&jpeg, 200_000); + let dir = crate::tests_xmp::temp_dir("preview-window"); + let path = dir.join("dscf0001.raf"); + std::fs::write(&path, &file).unwrap(); + let path = path.to_string_lossy().into_owned(); + let max_edge = 512; + + let (header, window, read) = preview_window(&path).expect("the file reads").expect("it has a preview"); + assert_eq!(header.len(), lightcraft_raw::PREVIEW_PREFIX, "the header prefix is bounded"); + assert!(read.header_path, "the header path is what answered"); + assert_eq!(read.total, file.len() as u64); + assert!(read.fraction() < 0.5, "the read is a fraction of the file: {read:?}"); + assert!(window.len() < file.len() / 2, "the raw tail must not be read: {} of {} bytes", window.len(), file.len()); + + // The quick path and the whole-file path agree byte for byte. + let quick = quick_image_srgb(&window, max_edge).expect("the window holds a decodable preview"); + let whole = embedded_preview_srgb(&file, max_edge).expect("the whole file decodes too"); + assert_eq!((quick.width, quick.height), (whole.width, whole.height), "same preview size"); + assert_eq!(quick.data, whole.data, "byte for byte"); + + // A file with no locatable preview declines (the caller then reads it whole), a missing file errors. + let plain = dir.join("plain.jpg"); + std::fs::write(&plain, &jpeg).unwrap(); + assert!(preview_window(&plain.to_string_lossy()).expect("a plain file reads").is_none()); + assert!(preview_window(&dir.join("nope.raf").to_string_lossy()).is_err()); + let _ = std::fs::remove_dir_all(&dir); + } + /// Issue #367: probes read headers, not pixels, and report what the decode-based probe did: /// oriented dimensions (EXIF orientation 6 swaps them), metadata, the error for a truncated /// file or one that isn't an image. diff --git a/crates/engine/tests/preview_read.rs b/crates/engine/tests/preview_read.rs new file mode 100644 index 00000000..11ffec44 --- /dev/null +++ b/crates/engine/tests/preview_read.rs @@ -0,0 +1,42 @@ +//! Opt-in test of the embedded-preview read path on a **real file the user has** — typically one on a +//! network share, which is where reading only the header instead of the whole raw matters: +//! +//! LC_PREVIEW_RAW="/run/user/1000/gvfs/smb-share:.../DSCF0001.RAF" \ +//! cargo test --release -p lightcraft-engine --test preview_read -- --ignored --nocapture +//! +//! It reports what the preview cost and fails when the read is not the small part of the file the +//! header path promises. Skipped without `LC_PREVIEW_RAW` (there is no committed raw fixture: media +//! is never committed, see AGENTS.md). +use std::time::Instant; + +#[test] +#[ignore = "needs LC_PREVIEW_RAW pointing at a real raw file"] +fn a_preview_reads_only_the_header_and_the_preview() { + let Ok(path) = std::env::var("LC_PREVIEW_RAW") else { + eprintln!("skipped: set LC_PREVIEW_RAW="); + return; + }; + let file_len = std::fs::metadata(&path).expect("the file exists").len(); + assert!(file_len > 0, "empty file"); + + let started = Instant::now(); + let window = + lightcraft_engine::files::preview_window(&path).expect("the file reads").expect("it carries an embedded preview the header can point at"); + let elapsed = started.elapsed(); + let (_, preview, read) = window; + + let frac = read.fraction(); + println!( + "preview of {path}\n read {} of {} bytes ({:.1} %) in {:.2} s{}", + read.read, + read.total, + frac * 100.0, + elapsed.as_secs_f64(), + if read.header_path { " [header path]" } else { " [whole file]" } + ); + + assert!(read.header_path, "the header path must answer for a raw whose pointer is in its header"); + assert_eq!(read.total, file_len, "the report covers the whole file"); + assert!(frac < 0.5, "a preview must not cost most of the file: read {} of {} bytes", read.read, read.total); + assert!(preview.starts_with(&[0xff, 0xd8]), "the preview bytes are a JPEG: {:?}", preview.get(..4)); +} diff --git a/crates/raw/src/lib.rs b/crates/raw/src/lib.rs index f0331d61..6fc491ac 100644 --- a/crates/raw/src/lib.rs +++ b/crates/raw/src/lib.rs @@ -52,7 +52,10 @@ pub use lightcraft_geom::Orientation; pub use lightcraft_meta::Metadata; pub use lightcraft_raster::Rgb32f; pub use opcodes::{Opcode, OpcodeLists}; -pub use preview::{PreviewColorSpace, embedded_preview, embedded_preview_color_space, embedded_preview_dynamic_range_optimized}; +pub use preview::{ + PREVIEW_PREFIX, PreviewColorSpace, PreviewRange, bounded_preview, embedded_preview, embedded_preview_color_space, + embedded_preview_dynamic_range_optimized, embedded_preview_from_range, +}; pub use semantic::{SemanticMask, semantic_masks}; use lightcraft_color::Xy; diff --git a/crates/raw/src/preview.rs b/crates/raw/src/preview.rs index d34fb4c6..cff924b2 100644 --- a/crates/raw/src/preview.rs +++ b/crates/raw/src/preview.rs @@ -167,6 +167,63 @@ pub fn embedded_preview(bytes: &[u8]) -> Option> { /// walking the marker segments and the entropy-coded data. `None` when it is not a complete, displayable /// (baseline or progressive) JPEG, so a stray `D8 FF` in other data is rejected. `*reach` is left at /// the furthest byte looked at (what the walk cost; see [`scan_for_jpeg`]). +/// Where a preview lives in a file, so a caller with a seekable file can read just that part of it. +/// +/// Why it exists: a 56 MB Fuji RAF keeps its JPEG in the first 5.6 MB (measured: DSCF9492.RAF at +/// 5.6 MB), so a grid thumbnail or a loupe preview has no reason to pull the remaining 50 MB — +/// which on a network share is the difference between a moment and half a minute. A caller that has +/// only bytes (`wasm`, tests) keeps using [`embedded_preview`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PreviewRange { + /// Byte offset of the preview in the file. + pub offset: u64, + /// Bytes to read at [`Self::offset`]: the container's declared preview extent, which may carry + /// padding past the preview's end-of-image marker. + pub window_len: u64, +} + +/// The preview's range in the file, when the file's header alone locates it. +/// +/// `bytes` is the file's first [`PREVIEW_PREFIX`] bytes (more is fine). `None` means "read the file +/// whole and call [`embedded_preview`]": the formats not covered here (a TIFF-based raw's pointer +/// tag can sit in an IFD the prefix cuts short; CR3 scans box by box) and files whose pointer is +/// damaged. +/// +/// The caller reads `[range.offset, range.offset + range.window_len)` and passes those bytes to +/// [`embedded_preview_from_range`], which returns exactly what [`embedded_preview`] returns for the +/// whole file. +/// +/// Pass a prefix that actually covers the pointer's range (read [`Self::offset`] + +/// [`Self::window_len`] bytes, or start with [`PREVIEW_PREFIX`] and grow when it is not enough): +/// [`crate::vendor::raf::header`] clamps a slice to the buffer it was given, so a prefix that stops +/// inside the declared extent yields a shorter `window_len` than the file really holds. +pub fn bounded_preview(bytes: &[u8]) -> Option { + if !bytes.starts_with(b"FUJIFILMCCD-RAW") { + return None; + } + let jpeg = crate::vendor::raf::header(bytes).ok()?.jpeg?; + let offset = (jpeg.as_ptr() as usize).checked_sub(bytes.as_ptr() as usize)?; + if offset.checked_add(jpeg.len())? > bytes.len() { + return None; + } + Some(PreviewRange { offset: offset as u64, window_len: jpeg.len() as u64 }) +} + +/// The preview inside `window`, which holds the file's bytes at `[range.offset, range.offset + +/// range.window_len)`: the counterpart of [`bounded_preview`]. Padding past the end-of-image marker +/// is dropped, exactly as [`embedded_preview`] drops it. +pub fn embedded_preview_from_range(window: &[u8], range: PreviewRange) -> Option> { + let preview = window.get(..usize::try_from(range.window_len).ok()?)?; + if !is_dct_jpeg(preview) { + return None; + } + Some(trim_eoi(preview).to_vec()) +} + +/// How many bytes of a raw file are enough for [`bounded_preview`]: every covered format keeps its +/// preview pointer in a fixed header, well inside this. +pub const PREVIEW_PREFIX: usize = 4096; + fn jpeg_extent(b: &[u8], soi: usize, reach: &mut usize) -> Option<(usize, u8)> { let mut i = soi + 2; let r = walk_jpeg(b, &mut i); @@ -322,6 +379,112 @@ mod tests { j } + /// A minimal RAF: the fixed header (JPEG offset/length at 84/88), the JPEG, then a raw tail the + /// caller must not have to read. `jpeg_len` is written as declared, so a test can claim more than + /// the JPEG holds the way a padded container does. + fn raf_fixture(jpeg: &[u8], gap: usize, preamble: usize, declared_len: Option) -> Vec { + let head = 160 + preamble; + let jpeg_off = head; + let mut f = vec![0u8; head]; + f[..16].copy_from_slice(b"FUJIFILMCCD-RAW "); + f[84..88].copy_from_slice(&(jpeg_off as u32).to_be_bytes()); + f[88..92].copy_from_slice(&(declared_len.unwrap_or(jpeg.len()) as u32).to_be_bytes()); + // records directory and raw block are absent (offset 0), which `header` reads as "none" + f.extend_from_slice(jpeg); + f.extend(std::iter::repeat_n(0x33u8, gap)); + f + } + + /// A RAF's preview pointer sits at a fixed offset, so a header-sized prefix locates it and the + /// caller reads just that range instead of the whole file — the whole point on a network share. + #[test] + fn bounded_preview_locates_a_rafs_jpeg_from_the_header_alone() { + let jpeg = fake_jpeg(5000); + let file = raf_fixture(&jpeg, 40_000, 0, None); + // A prefix that covers the pointer's range: header + JPEG, and nothing of the tail. + let prefix_len = 8192.min(file.len()); + let prefix = &file[..prefix_len]; + + let range = bounded_preview(prefix).expect("a RAF's preview is found from its header"); + assert_eq!(range.offset, 160); + assert_eq!(range.window_len, jpeg.len() as u64); + assert!(range.offset + range.window_len <= prefix_len as u64, "the prefix covers the whole range: {range:?} of {prefix_len}"); + assert!((range.offset + range.window_len) < file.len() as u64, "the range stops short of the tail: {range:?} of {}", file.len()); + + // What the caller reads, and what it holds afterwards: identical to the whole-file answer. + let window = prefix.get(range.offset as usize..(range.offset + range.window_len) as usize).expect("the range lies inside the prefix"); + let bounded = embedded_preview_from_range(window, range).expect("the range holds the preview"); + assert_eq!(Some(bounded.clone()), embedded_preview(&file)); + assert!(bounded.ends_with(&[0xff, 0xd9]), "the preview ends at its EOI marker"); + + // A prefix that stops inside the declared extent is clamped by the container reader. The + // caller then reads a short window; what comes back is the whole JPEG it holds (the EOI + // marker is inside), never bytes past the window. + let short = &file[..2000]; + let clipped = bounded_preview(short).expect("a pointer before the cut is still found"); + assert!(clipped.window_len < jpeg.len() as u64, "clamped: {clipped:?}"); + let window = short.get(clipped.offset as usize..).expect("inside"); + let preview = embedded_preview_from_range(window, clipped).expect("the bytes are a JPEG start"); + // No end-of-image marker inside the window: the bytes come back as they are, exactly as the + // whole-file path returns an unclosed JPEG. The decoder refuses it later; nothing is invented. + assert_eq!(preview, window, "an unclosed JPEG is handed on unchanged, never padded"); + assert!(preview.len() <= window.len(), "never past the window"); + } + + /// The preview pointer may sit past the prefix (a long maker-note record before it), and a + /// container may declare padding after the JPEG: both stay correct, and the padding is dropped. + #[test] + fn bounded_preview_handles_a_late_pointer_and_padding() { + let jpeg = fake_jpeg(300); + let late = raf_fixture(&jpeg, 0, 9000, None); + let range = bounded_preview(&late).expect("the whole file is always a valid prefix"); + assert_eq!(range.offset, (160 + 9000) as u64); + let padded = raf_fixture(&jpeg, 500, 0, Some(jpeg.len() + 500)); + let range = bounded_preview(&padded).expect("a padded extent is still located"); + assert_eq!(range.window_len, (jpeg.len() + 500) as u64); + let window = padded.get(range.offset as usize..(range.offset + range.window_len) as usize).expect("the window is inside the file"); + let preview = embedded_preview_from_range(window, range).expect("the padding is dropped"); + assert_eq!(preview, embedded_preview(&padded).expect("the whole-file answer agrees")); + assert!(preview.ends_with(&[0xff, 0xd9])); + } + + /// Without a usable header or pointer the caller must read the file whole: the answer is `None`, + /// never a wrong range. + #[test] + fn bounded_preview_declines_what_the_header_cannot_answer() { + let jpeg = fake_jpeg(64); + let file = raf_fixture(&jpeg, 0, 0, None); + assert_eq!(bounded_preview(&jpeg), None, "not a raw at all"); + assert_eq!(bounded_preview(b""), None); + if let Some(short) = file.get(..64) { + assert_eq!(bounded_preview(short), None, "a header cut short has not shown the pointer yet"); + } + // a pointer that claims more than the file holds: `header` clamps the slice to the buffer, + // so the range never runs past the end (no out-of-bounds read can follow from it) + let mut lying = raf_fixture(&jpeg, 4096, 0, None); + lying[88..92].copy_from_slice(&(u32::MAX / 2).to_be_bytes()); + let clamped = bounded_preview(&lying).expect("a pointer inside the file is still usable"); + assert!(clamped.offset + clamped.window_len <= lying.len() as u64, "clamped to the file: {clamped:?}"); + // a range the window cannot satisfy yields nothing + assert_eq!(embedded_preview_from_range(&jpeg, PreviewRange { offset: 0, window_len: u64::MAX }), None); + // a TIFF-based raw: its pointer tag lives in an IFD this prefix may cut short + let mut tiff = b"II*\0\x08\0\0\0".to_vec(); + tiff.extend_from_slice(&[0u8; 64]); + assert_eq!(bounded_preview(&tiff), None); + } + + /// A range that does not hold a JPEG yields nothing, rather than a truncated image handed on. + #[test] + fn a_range_that_does_not_hold_a_jpeg_yields_nothing() { + let jpeg = fake_jpeg(200); + let range = PreviewRange { offset: 0, window_len: jpeg.len() as u64 }; + assert_eq!(embedded_preview_from_range(&jpeg, range).as_deref(), Some(&jpeg[..])); + assert_eq!(embedded_preview_from_range(&jpeg[..10], range), None, "window shorter than the range claims"); + let not_jpeg = vec![0u8; jpeg.len()]; + assert_eq!(embedded_preview_from_range(¬_jpeg, range), None, "not a JPEG"); + assert_eq!(embedded_preview_from_range(&[], PreviewRange { offset: 0, window_len: 0 }), None, "empty window"); + } + #[test] fn picks_largest_dct_jpeg() { let small = fake_jpeg(10); diff --git a/docs/parity.md b/docs/parity.md index d0bdc35c..19e298b8 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -21,7 +21,7 @@ and prints the summary; `cargo xtask parity --write` refreshes the summary table | Section | ✅ | 🟡 | ⬜ | 🚫 | P0 done | P1 done | |---|---:|---:|---:|---:|---:|---:| -| A. Import (IMP) | 11 | 3 | 1 | 1 | 3/5 (60%) | 5/5 (100%) | +| A. Import (IMP) | 12 | 3 | 1 | 1 | 4/6 (67%) | 5/5 (100%) | | B. Library management (LIB) | 22 | 2 | 1 | 2 | 9/9 (100%) | 9/9 (100%) | | C. Views & navigation (VIEW) | 15 | 1 | 1 | 0 | 8/9 (89%) | 4/4 (100%) | | D. Search & filter (FILT) | 11 | 1 | 1 | 0 | 4/4 (100%) | 4/4 (100%) | @@ -42,15 +42,15 @@ and prints the summary; `cargo xtask parity --write` refreshes the summary table | S. Export (EXP) | 15 | 2 | 1 | 0 | 7/7 (100%) | 7/7 (100%) | | T. Share (SHARE) | 0 | 0 | 0 | 4 | — | — | | U. Map & location (MAP) | 0 | 1 | 1 | 0 | — | 0/1 (0%) | -| V. Preferences (PREF) | 7 | 1 | 2 | 2 | 1/1 (100%) | 5/5 (100%) | +| V. Preferences (PREF) | 6 | 2 | 2 | 2 | 1/1 (100%) | 4/5 (80%) | | W. Cloud & AI infrastructure (CLOUD / AI) | 1 | 0 | 1 | 2 | — | — | | X. Cross-cutting behaviours (BEHAV) | 18 | 6 | 1 | 1 | 8/8 (100%) | 6/9 (67%) | | Y. Menus | 82 | 0 | 4 | 8 | 47/47 (100%) | 23/23 (100%) | | Z. Keyboard shortcuts (desktop) | 75 | 3 | 3 | 1 | 49/52 (94%) | 23/24 (96%) | -| Lightroom Classic extras | 32 | 11 | 39 | 9 | — | 22/24 (92%) | -| **Total** | 393 | 39 | 84 | 36 | 192/200 (96%) | 142/152 (93%) | +| Lightroom Classic extras | 33 | 15 | 35 | 9 | — | 23/25 (92%) | +| **Total** | 398 | 49 | 73 | 36 | 193/201 (96%) | 143/155 (92%) | -Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **79.9%** of 516 in-scope rows — P0 98.0% of 200 · P1 95.7% of 152 · P2 42.9% of 163. +Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **81.2%** of 520 in-scope rows — P0 98.0% of 201 · P1 95.2% of 155 · P2 47.2% of 163. ## Top gaps @@ -148,6 +148,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins. | LR-IMP-SIDECAR-SPLIT | Separate XMP sidecar variants | P2 | ⬜ | `cmd:library.xmpPreferences` | sidecar naming option exists (stem/full), no split sidecars | | LR-IMP-FORMATS | Supported formats | P0 | 🟡 | `crates/codecs/src/lib.rs`, `crates/raw/src/lib.rs`, `crates/raw/src/jxl.rs` | JPEG, PNG, TIFF (1- to 64-bit integer, 16- to 64-bit float, incl. bilevel and CCITT fax), WebP, JXL, PSD, GIF, BMP; DNG (incl. lossy / Smart Preview DNG and DNG 1.7 JPEG XL tiles/strips: lossless ones decode bit-exactly on synthetic CFA and LinearRaw files, lossy ones are restored to the declared linear primaries without clipping or gamut mapping, checked on one real file with 108 lossy tiles, and any tile that fails to decode makes the file preview-only; DNG 1.7 row/column-interleaved rasters (`RowInterleaveFactor` / `ColumnInterleaveFactor`) are undone before the mosaic is read, so a file from Adobe's converter with a 2×2 interleave no longer decodes as four scaled-down copies in the wrong colours; a dimension the factor does not divide decodes with short trailing fields; verified on 4 real ILCE-6400 JPEG XL DNGs rendering bit-identical to their non-interleaved twins, factors above 2 rest on the plain reading of the tag; a single-chunk JPEG XL preview IFD is offered as the embedded preview; a DNG whose samples pile up at one value well below its stated `WhiteLevel`, as a camera app's 12-bit Bayer DNG that states 4095 and clips at 3567, has those samples raised to the white level on decode, so highlight reconstruction sees the clipped areas instead of white balance tinting them magenta, issue #548, `crates/raw/src/saturation.rs`), CR2 (colour-filter layout from the file's `CR2CFAPattern` tag, issue #85), ARW (pre-2017 bodies: white balance and black level from the encrypted `SR2SubIFD`, issue #148, its `WB_RGGBLevels` being the white balance the camera applied, issue #535; the DSLR-A900's packed 12-bit strip, two pixels per three bytes, read only when the strip is exactly 1.5 bytes per pixel, with its camera-JPEG window centred, checked on one CC0 file (A850 unmeasured); downsized Sony lossless YCbCr 4:2:0 / 4:2:2 tiles decoded to linear RGB, verified on private ILCE-7M4 3:2 & 4:3 examples including 16-bit full-resolution export; Sony DSC-R1 .SR2 decodes, its big-endian 16-bit words read in the order that fits the sample range, checked on the CC0 3221 sample; its crop and white balance live in the SR2SubIFD and are not read yet), NEF (uncompressed, lossless and lossy compressed 12/14-bit, `crates/raw/src/vendor/nefc.rs`), RAF (uncompressed, lossless/lossy compressed Bayer and X-Trans; `crates/raw/src/vendor/rafc.rs`, full sensor verification on 13 bodies, `docs/raf-compression.md`), RW2 / Leica RWL / Panasonic RAW in every raw format (compressed 4 and 6, the prefix-coded strips of format 8 from the GH6 on, packed 2/5/7, the 16-bit words of the 2005–2007 bodies; black levels, in-camera aspect crops and mapped-out defect pixels; 178 CC0 files from 118 bodies checked, `crates/raw/src/vendor/rw2.rs`), PEF, ORF (uncompressed; colour-filter layout from the file's Exif `CFAPattern`; 12.8 bit/px 16-byte blocks of ten 12-bit samples + a zero pad byte, E-300 / E-330 / E-500 verified on CC0 files, E-M5 Mark II / PEN-F high-res files decode smooth but CFA and black unverified), Hasselblad 3FR and FFF (a TIFF without a DNG version tag that describes its CFA IFD, levels, crop and white balance with DNG's tags; `RawFormat::CfaTiff`, read by `dng::decode_as`, RGGB when the file states no `CFAPattern`): uncompressed (X1D, X1D II, X2D, CFV 100C) and Hasselblad's lossless JPEG with scan selection value 8 (every FFF; H3D, H4D and CFV-50c 3FR), `ljpeg::decode_hasselblad`, its rule measured on 14 CC0 files with the ljpeg-explorer tool, Samsung SRW without compression (NX5, NX10, NX11 and the NX20-generation bodies NX20, NX200, NX210, NX1000 and NX1100 as 12-bit packed samples in either bit order, EX1 and WB2000 as 16-bit words; layout, camera-JPEG framing, levels and white balance from the maker note, `crates/raw/src/vendor/srw.rs`; median ΔE vs the camera JPEG 2.2–7.6 on 11 of the 12 files from 10 bodies checked, 10.7 (5.7 registered) on the backlit WB2000 sample). 12-bit NEF black level: maker note `0x003d` is in 14-bit units (D750, D780, D850, D7500, Z 50 checked). Lossy type 2 NEF curves of version `0x44 0x40` (D780, D6, Z 5, Z 6, Z 7, Z 7_2) span a quarter of the sample range, read from the table's own point spacing. CR3 lossless CRX Bayer and version 0x100/0x200 C-RAW decodes to sensor pixels with timed white balance, black/white levels and file crop (the shot's aspect ratio from the maker note's `AspectInfo`); full sensor samples match the reference on M50/R100/R8 (`crates/raw/tests/cr3_corpus.rs`, `docs/cr3.md`). Unsupported coding variants retain metadata and use the embedded JPEG when available; HEVC preview decoding remains unsupported. Missing: CR3 unverified CRX variants, compressed ORF, compressed Samsung SRW (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini: preview only with the reason), NEFs labelled compressed (34713) but stored uncompressed without a `0x0096` table (Z 6 packed 14-bit, D850 12-bit uncompressed), Nikon High Efficiency NEF (HE / HE★ from the Z 8, Z 9, Z 6III and Z f: compression 34713 with a JPEG XS codestream and no `0x0096` table; no public specification, so preview only with the reason "Nikon High Efficiency NEF", issue #193; Lossless compressed NEFs from the same bodies decode), NEF "lossy after split" (preview only: imported with `preview_only` = the decoder's reason, rendered as a rendered JPEG, flagged by a grid badge and loupe/Edit/Info notices, `previewOnly` in `catalog.query`; Reload clears it once the file decodes), TIFF-based raws whose raw data sits in a private block (Phase One / Leaf IIQ, Canon EOS-1D / 1Ds and Kodak DCS TIFFs: IFD0 is only a reduced RGB image next to a much larger declared picture) are recognised from that structure and import as `preview_only` with the reason, instead of opening as a thumbnail-sized ordinary image (`thumbnail_shell` in `crates/raw/src/lib.rs`), Recognised raws LightCraft cannot decode open as `preview_only` with any usable embedded JPEG, also Canon CRW, Minolta MRW and Sigma X3F (containers recognised by magic, preview found by a structural JPEG scan), Kodak KDC (IFD0 without `ImageWidth`), Leaf MOS and Epson ERF (scan), and any decode failure of a recognised raw, not only an unsupported variant; ones with no JPEG still fail with "without an embedded preview", AVIF decode (HEIC decodes only behind the non-default `heif` feature, which the release packages enable: `crates/heif`, heic-rs for the container and HEVC (bit-exact against libde265 on iPhone tiles) plus our own colour conversion, matching libheif within 1 code value on iPhone-style grids (VUI full range, Display P3 ICC, nclx, 10-bit, irot/imir/clap, gain map/depth ignored); 4:2:0/monochrome only, 4:2:2/4:4:4 Canon/Sony HIF refused; same policy as PhotoCraft), palette (indexed-colour) TIFF (refused by the `tiff` crate) | | LR-IMP-CAMERA-COVERAGE | Camera coverage, verified per model | P0 | 🟡 | `crates/raw/tests/corpus.rs`, `xtask/src/main.rs` | a supported container is not the same as every camera that writes it decoding correctly: ~55 CC0 corpus files plus 6 exact-sensor CR3 cases (plus ~60 NEFs checked by hand and 178 Panasonic / Leica RW2, RWL and RAW files from 118 bodies, checked against each file's own JPEG) against the >1,000 models Lightroom lists; per-model differences still surface (e.g. the CR2 colour-filter layout differed by model until it was read from the file's own tag, issue #85, checked on 35 CR2 bodies; CR2 black levels were read from border columns light already reaches, left of `SensorInfo`'s border, so CR2s rendered dark with crushed shadows (EOS 6D: 2591 instead of 2047), now measured from the masked columns only, checked on 8 corpus bodies; pre-2017 Sony ARWs opened green until their scrambled white balance / black level were read, issue #148, checked on 16 ARWs from 15 Sony bodies; their white balance now comes from the encrypted `SR2SubIFD`'s `WB_RGGBLevels`, the gains the camera applied, which 14 bodies (DSLR-A450 … A700, NEX-3/5/C3, SLT-A33/A35/A55, ILCE-6001, UMC-R10C) had no other source for and which the maker-note estimate read before missed by up to 100 % on preset shots, issue #535, checked against ExifTool on 100 decoded ARWs from 87 Sony bodies; the encrypted SR2 black level is now read from its directory entry (the DSLR-A450/A500/A550/A700 read 354–975 instead of 512), checked on 126 ARWs from 97 Sony bodies, and downsized lossless M/S ARWs use their measured black level 1024 instead of 512, checked on 12 files from 4 bodies, issue #535; pre-2017 Sony bodies shot in the camera's 16:9 mode (ILCE-7SM2, DSLR-A580) now get the 16:9 crop of their camera JPEG instead of the whole 3:2 frame with padding columns, issue #535; Canon PowerShot and EOS M CR2s whose `ColorData` is stored as bytes opened green until the bytes were read as 16-bit words, 25 files from 25 bodies checked). Compressed RAF sensor arrays verified on 13 Fujifilm bodies (`docs/raf-compression.md`). Sony ILCE-7CR (61 MP): lossless compressed L and M and compressed ARW2 samples of one scene in the corpus (black level, default crop, as-shot gains, lossless vs compressed mosaic agreement, fitted camera look), and all nine public codings (also uncompressed, small and APS-C crop) decode and fit. Grow the CC0 corpus per model and test each sample decodes with plausible colour. Nikon CropArea and untagged-preview colour-space regression tests; private Z 8 crop verified. | +| LR-IMP-PREVIEW-READ | Preview reads the header, not the raw | P0 | ✅ | `crates/raw/src/preview.rs` (`bounded_preview`, `embedded_preview_from_range`), `crates/engine/src/files.rs` (`preview_window`), `docs/raw-preview-reads.md` | a grid thumbnail and a loupe preview of an unedited raw show the camera's embedded JPEG, which sits near the start of the file (a 24 MP Fuji RAF keeps its 5.6 MB JPEG in a 56 MB file), so the preview loader reads the header and that JPEG instead of the whole file: 56.5 MB → 5.9 MB and ~10 s → 0.96 s measured on one real RAF over a GVFS/SMB Wi-Fi share (byte-identical preview pixels; `LIGHTCRAFT_PROFILE=1` reports the bytes); the RAF pointer is read from its fixed header fields, formats whose preview only the whole file reveals (CR3 boxes, TIFF pointer tags past the prefix, CRW/MRW/X3F scans) keep the whole-file path, and a lying pointer, a short read or a file that changes underneath us falls back to it rather than losing the preview | | LR-IMP-CULL-AT-IMPORT | Culling analysis at import | P2 | 🟡 | `cmd:photo.analyze` | run Assisted Culling on the imported photos (they're selected after an import); not automatic | | LR-IMP-MOVE | Move on import [Classic] | P1 | ✅ | `cmd:library.import` (`mode` move), `crates/engine/src/import_move.rs`, `crates/engine/src/tests_import_move.rs`, `crates/ui-egui/src/import.rs` | Transfer ▸ Move with Copy's destination, folders (day / month / one folder / custom template), rename template and example destination; XMP sidecars (both namings) move along; each source is removed only after its destination is written (hard link on the same volume, else copied, synced and compared byte for byte) and its catalog record is saved; failed, duplicate, unchecked files keep their sources, taken names get -1, -2…, never overwritten; files already in the destination/library are added in place; sources that can't be removed (read-only card) are kept and reported (`kept`). Undo removes the photos from the library but leaves the moved files at the destination. No Copy as DNG while moving | | LR-IMP-DNG-CONVERT | Convert to DNG on import [Classic] | P2 | ✅ | `cmd:library.import` (`dng`), `crates/ui-egui/src/import.rs` | copy imports: Raw files ▸ Copy as DNG (lossless; the card is untouched); the raw copy is removed only once its DNG is verified and on disk (issue #106) | diff --git a/docs/raw-preview-reads.md b/docs/raw-preview-reads.md new file mode 100644 index 00000000..51058255 --- /dev/null +++ b/docs/raw-preview-reads.md @@ -0,0 +1,62 @@ +# Reading a raw's preview without reading the raw + +A grid thumbnail and a loupe preview of an unedited raw both show the camera's embedded JPEG, not a +decode of the sensor data. That JPEG lives near the *start* of the file, but the rest of the file is +the sensor data — for a 24 MP Fuji RAF, **50 of 56 MB**. Until this change the preview loader called +`std::fs::read` on the whole file and threw the sensor data away, so every preview paid for bytes +nobody looked at. + +That is free on a local SSD and expensive on a network share, which is where photographers keep +libraries. Measured on one real file (`DSCF9492.RAF`, 56.5 MB) over a GVFS/SMB share on Wi-Fi: + +| | read | time | +|---|---|---| +| whole file | 59 277 632 B (100 %) | ~10 s | +| header + preview | **5 857 257 B (9.9 %)** | **0.96 s** | + +## How it works + +1. `read_prefix` reads the first `PREVIEW_PREFIX` (4 KiB); every format whose preview is locatable + from the header keeps its pointer well inside that. +2. For Fuji RAF the header's two big-endian `u32`s at **84** (offset) and **88** (length) are the + same fields `lightcraft_raw`'s RAF reader uses. When they declare more than the prefix holds, the + prefix is re-read once at exactly that extent: `lightcraft_raw::bounded_preview` reports the range + its buffer covers (`slice` clamps to `b.len()`), so a short prefix yields a *plausible but + truncated* range — a real 4 KiB prefix reports 3 948 bytes where the file holds 5.6 MB. +3. `embedded_preview_from_range` checks the window really starts with a JPEG SOI and trims it at the + last EOI marker, exactly as `embedded_preview` trims the whole-file answer. +4. When the prefix already covers the range — the usual case, since it was grown to it — the window + is a slice of the prefix. Reading it again would double a 5.6 MB preview. + +Formats whose preview pointer is not in the header (CR3 scans boxes; a TIFF pointer tag can sit in +an IFD the prefix cuts short; CRW/MRW/X3F are found by scanning) keep the whole-file path, so +nothing regresses: `preview_window` returns `None` and the loader reads the file as before. A pointer +that lies, a short read or a file that changes underneath us ends in the same fallback — the user's +preview is never lost to a damaged header. + +`LIGHTCRAFT_PROFILE=1` prints what each preview cost: + +``` +[profile] preview …/DSCF9492.RAF: read 5857257 of 59277632 bytes (header 5857257, window 5857109) +``` + +## Tests + +- `crates/raw/src/preview.rs`: the header-only range matches `embedded_preview` byte for byte; a + clamped prefix, a padded extent, a late pointer, a lying pointer and a non-JPEG window are all + covered. +- `crates/engine/src/files.rs`: on a synthetic RAF with a 200 KB tail, the quick path and the + whole-file path produce identical pixels, and the read stays under half the file. +- `crates/engine/tests/preview_read.rs` (opt-in, `#[ignore]`): the same measurement on a **real** + file, typically on a share: + + ```sh + LC_PREVIEW_RAW="/mnt/nas/…/DSCF0001.RAF" \ + cargo test --release -p lightcraft-engine --test preview_read -- --ignored --nocapture + ``` + + No raw fixture is committed (media never is, see `AGENTS.md`). + +A network share is still slower than a local disk — the preview itself is 5.6 MB — and the sensor +data still has to come over the wire for the *developed* picture, which is a separate path +(`fs_pair_loader`, the full decode). This change removes the preview's share of it, nothing more. From afbc0f97447d3180023b34ffca731d1997511e65 Mon Sep 17 00:00:00 2001 From: robmdunn <1900604+robmdunn@users.noreply.github.com> Date: Sat, 10 Oct 2026 11:42:59 -0400 Subject: [PATCH 08/24] LR-EXP-TYPE: JPEG XL export (#247) Export as JPEG XL from the dialog, CLI, MCP and control channel (format jxl): lossy with quality or lossless, 8/16-bit, effort fast/normal, progressive, EXIF/XMP boxes; HDR edits as 16-bit Rec. 2020 PQ. Encoder: jixel (pure Rust, BSD-3-Clause OR Apache-2.0, native only: it spawns its own threads), wrapped in catch_unwind. Lossy files declare an enum encoding (sRGB / P3 / Rec. 2020; Adobe RGB and ProPhoto written as Rec. 2020); lossless files embed the ICC profile. Decoder fix: jxl-oxide desaturated colours outside sRGB when rendering XYB images to wider primaries; lossy wide-gamut JXL now renders to linear sRGB primaries and is converted to Rec. 2020 here. jixel's slowest effort is left out: jxl-oxide misdecodes its (valid) files. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 8 ++ Cargo.toml | 1 + ROADMAP.md | 4 +- apps/lightcraft-cli/src/main.rs | 4 +- apps/lightcraft-web/src/store.rs | 1 + crates/codecs/Cargo.toml | 7 +- crates/codecs/README.md | 4 +- crates/codecs/src/encode.rs | 6 +- crates/codecs/src/jxl.rs | 14 +- crates/codecs/src/jxl_encode.rs | 188 +++++++++++++++++++++++++++ crates/codecs/src/lib.rs | 5 +- crates/codecs/tests/bench.rs | 9 ++ crates/codecs/tests/roundtrip.rs | 134 +++++++++++++++++++ crates/engine/Cargo.toml | 2 + crates/engine/src/export.rs | 155 +++++++++++++++++++--- crates/engine/src/tests_export.rs | 34 +++++ crates/mcp/src/headless.rs | 4 +- crates/mcp/src/tools.rs | 13 +- crates/ui-egui/src/panels/dialogs.rs | 24 +++- docs/mcp.md | 2 +- docs/parity.md | 12 +- docs/web.md | 2 + docs/whats-new.md | 9 ++ 23 files changed, 597 insertions(+), 45 deletions(-) create mode 100644 crates/codecs/src/jxl_encode.rs diff --git a/Cargo.lock b/Cargo.lock index 0ad4ab84..3b6bf118 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2612,6 +2612,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jixel" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "278a75e12caacec34be774d62ad05e45fc0b6a52d1f4e9f7fbc0209ecdf794f3" + [[package]] name = "jni" version = "0.22.4" @@ -3031,6 +3037,7 @@ dependencies = [ "bytemuck", "image", "image-webp", + "jixel", "jpeg-decoder", "jpeg-encoder", "jxl-oxide", @@ -3110,6 +3117,7 @@ version = "0.5.0" dependencies = [ "ab_glyph", "harfrust", + "jxl-oxide", "libc", "lightcraft-catalog", "lightcraft-codecs", diff --git a/Cargo.toml b/Cargo.toml index 58e5383b..aae0161b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ tiff = "0.11" image-webp = "0.2" moxcms = { version = "0.9", default-features = false, features = ["lut"] } jxl-oxide = { version = "0.12", default-features = false } +jixel = "0.3.4" # BSD-3-Clause OR Apache-2.0, pure-Rust JPEG XL encoder (export; port of libjxl-tiny) zune-jpegxl = { version = "0.5", default-features = false, features = ["std"] } # MIT/Apache-2.0/Zlib lossless JXL encoder (test fixtures only) ravif = { version = "0.13", default-features = false, features = ["threading"] } # no "asm": it needs nasm (non-Rust) # HDR AVIF (PQ): ravif fixes the sRGB transfer curve, so HDR files drive its rav1e + avif-serialize directly diff --git a/ROADMAP.md b/ROADMAP.md index 9bb4744b..ad0327ba 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -28,7 +28,7 @@ duplicate Local entries #22, black GPU exports on an Intel iGPU #78). | **AI & computational** | ~15–20% | Assisted culling (focus, bursts), auto tone, HDR/panorama merge; subject/sky/background masks as classical heuristics, or the photo's own segmentation mattes when it ships them (DNG semantic masks, e.g. iPhone ProRAW's sky matte); non-destructive Bayer RAW denoise with opt-in weights on pure-Rust CPU/GPU; **faces (MVP, [docs/faces.md](docs/faces.md))**: a pure-Rust detector (a 232 KB opt-in download), opt-in recognition models (one-click download or add a file; they run on the shared pure-Rust CPU interpreter; pinned SFace/AuraFace embeddings agree with an independent reference runtime on 40 public crops; one-thread medians 34–36 / 233–252 ms (AuraFace remains expensive)), a background scan that finds and embeds faces, name suggestions, a person's page with look-alike faces to confirm, an Unnamed faces section (look-alikes together, select a group and name it at once) | Real segmentation masks (subject, sky, people, objects, landscape, depth), denoise model policy / linear RGB / quality verification, super resolution, lens blur, generative remove, natural-language search; for faces: clusters with separators between groups, small faces in large photos (the detector looks at 640 px), thresholds calibrated on live faces, writing face regions back to XMP. **Blocked on a model strategy** (licensable weights or our own training; pure-Rust inference is feasible) | | **Workflow & library** | ~85% (single machine) | Robust catalog (journal + snapshots, background compaction, crash-tested), 85k-photo libraries stay responsive, Local browsing with automatic cleanup, XMP interop, keywords, smart albums, Move import | Opening an 85k library takes 1.7–4.7 s; no cloud sync (out of scope), no tablet companion (#74, roadmap), shared albums, publish services, tethering | | **Classic modules** | ~30% | Geotagging from GPX track logs, soft proofing (partial), slideshow (basic), paginated contact-sheet PDF export (A4/Letter, layout/captions, current edits, 150 dpi sRGB) | **Map view, Book, full Print module, Slideshow module, Web, publish services**: contact sheets work, custom print packages and printer colour management remain missing | -| **HDR & video** | ~25% | HDR editing (headroom limit, SDR rendition, Visualize HDR); gain map JPEG, PQ AVIF and float TIFF export | On-screen HDR display, HLG, HDR settings from crs XMP; video play/trim/edit/export | +| **HDR & video** | ~25% | HDR editing (headroom limit, SDR rendition, Visualize HDR); gain map JPEG, PQ AVIF / JPEG XL and float TIFF export | On-screen HDR display, HLG, HDR settings from crs XMP; video play/trim/edit/export | | **Platform & robustness** | ~70% | macOS native; Windows/Linux builds; web via WASM; no-panic lints workspace-wide, `unsafe` confined to `crates/sysmem`; failed saves are reported; GPU errors fall back to CPU | Windows installer UI unverified on Windows (PR #79); GPU path proven only on Apple + user reports; English, Simplified/Traditional Chinese, Japanese, Brazilian Portuguese, Spanish, German, Russian and Ukrainian UI (see docs/localization.md); remaining technical errors; Traditional Chinese uses the Simplified Chinese font until craft-fonts has a TC face; accessibility partial; headless UI tests time out under machine load | ### By kind of user @@ -96,7 +96,7 @@ yet guarantee original-pixel detail; visible-region rendering remains a gap (LR- | M7 | Detail | sharpening + masking preview, luminance/colour NR, Denoise, Raw Details, Super Resolution | 6–10 | 🚧 (sharpening, luminance/colour NR ✅; AI Denoise 🟡 (Bayer, optional weights; policy/fidelity open); Raw Details, Super Resolution ⬜) | | M8 | Heal / Remove | content-aware remove (PatchMatch), heal, clone, brush spots, visualize spots, red/pet eye | 6–10 | 🚧 (heal, clone, auto source, visualize spots, red/pet eye ✅; PatchMatch remove ⬜) | | M9 | Presets, profiles, versions, sync | preset browser + amount, create/import presets, profile browser, versions, history, copy/paste/sync settings | 5–8 | ✅ | -| M10 | Export & share | export dialog (JPEG/PNG/TIFF/DNG/AVIF/JXL/original), sizing, sharpening, metadata, watermark, naming, batch jobs, XMP sidecars, HDR export | 6–10 | 🚧 (all formats incl. DNG/original, sizing, presets, background jobs, HDR gain map JPEG / PQ AVIF ✅; JXL encode ⬜) | +| M10 | Export & share | export dialog (JPEG/PNG/TIFF/DNG/AVIF/JXL/original), sizing, sharpening, metadata, watermark, naming, batch jobs, XMP sidecars, HDR export | 6–10 | 🚧 (all formats incl. DNG/original, sizing, presets, background jobs, HDR gain map JPEG / PQ AVIF ✅; JPEG XL export ✅ (native only)) | | M11 | RAW II | CR3, RAF (X-Trans), ORF, RW2, PEF, SRW, 3FR, IIQ + long tail; camera calibration DB; HEIC/AVIF/JXL import | 20–35 | 🚧 (CR3 lossless CRX and C-RAW, RAF uncompressed and lossless/lossy compressed, RW2 (every raw format), PEF, ORF uncompressed ✅; **camera colour calibration** 🚧 (guarded ARW, NEF, RW2 and RAF preview fitting; measured database still missing), CR3, compressed ORF ⬜) | | M12 | AI & smart features | subject/sky/background/people/object masks, semantic search, faces/People (permissively licensed models, pure-Rust inference) | 20–40 | ⬜ | | M13 | Merge | HDR merge (deghost), panorama (projections, boundary warp, fill edges), HDR panorama | 10–15 | ✅ | diff --git a/apps/lightcraft-cli/src/main.rs b/apps/lightcraft-cli/src/main.rs index 592a534a..1bd1eec3 100644 --- a/apps/lightcraft-cli/src/main.rs +++ b/apps/lightcraft-cli/src/main.rs @@ -51,13 +51,13 @@ USAGE: --library DIR headless: open (or create) a persistent LightCraft library; edits are saved --compact list only the helper tools (every command stays reachable via run_command) lightcraft-cli render -o [OPTIONS] - Develop one file and export it (.jpg, .png, .tif, .webp, .avif or .dng by extension) with the + Develop one file and export it (.jpg, .png, .tif, .webp, .avif, .jxl or .dng by extension) with the same encoder as the app's Export dialog. Options: --set CONTROL=VALUE set a develop slider, repeatable (e.g. --set light.exposure=0.5) --settings FILE merge a partial develop-settings JSON file --preset ID apply a preset (see `commands`/presets.list) --size N long edge in pixels (default: full size, cropped) - --quality Q JPEG/AVIF quality 1..100 (default 92) + --quality Q JPEG/AVIF/JPEG XL quality 1..100 (default 92) --opt KEY=VALUE any export option of `app.export`, repeatable (VALUE is JSON or a string), e.g. --opt colorSpace=displayP3 --opt bitDepth=16 --opt percent=50 --opt shortEdge=1080 --opt ppi=300 diff --git a/apps/lightcraft-web/src/store.rs b/apps/lightcraft-web/src/store.rs index 9d3e2199..729e7940 100644 --- a/apps/lightcraft-web/src/store.rs +++ b/apps/lightcraft-web/src/store.rs @@ -180,6 +180,7 @@ pub fn mime_for(name: &str) -> &'static str { Some("tif" | "tiff") => "image/tiff", Some("webp") => "image/webp", Some("avif") => "image/avif", + Some("jxl") => "image/jxl", _ => "application/octet-stream", } } diff --git a/crates/codecs/Cargo.toml b/crates/codecs/Cargo.toml index 4c93a89a..166fbf4d 100644 --- a/crates/codecs/Cargo.toml +++ b/crates/codecs/Cargo.toml @@ -32,11 +32,14 @@ jxl-oxide = { workspace = true, optional = true } ravif = { workspace = true, optional = true } rav1e = { workspace = true, optional = true } avif-serialize = { workspace = true, optional = true } +# spawns threads of its own (std::thread), which wasm32 has not got +jixel = { workspace = true, optional = true } [features] default = ["parallel", "jxl", "avif"] parallel = ["dep:rayon", "lightcraft-raster/parallel", "jpeg-decoder/rayon"] -jxl = ["dep:jxl-oxide"] +## JPEG XL decode (jxl-oxide) and, on native targets, encode (jixel); both pure Rust. +jxl = ["dep:jxl-oxide", "dep:jixel"] # HEIC/HEIF decode via the optional lightcraft-heif crate (heic-rs, pure Rust). Off by default so # distributors choose it at build time (HEVC patents are a distributor's call, same policy as # PhotoCraft); `cargo xtask ci` clippies and tests it (`--features heif`), releases don't ship it yet. @@ -45,6 +48,8 @@ heif = ["dep:lightcraft-heif"] avif = ["dep:ravif", "dep:rav1e", "dep:avif-serialize"] [dev-dependencies] +# JPEG XL tests read encoded samples without our colour conversion +jxl-oxide = { workspace = true } proptest = { workspace = true } # Lossless synthetic HEIC files for the `heif` feature's tests. lightcraft-heif = { workspace = true, features = ["testdata"] } diff --git a/crates/codecs/README.md b/crates/codecs/README.md index e1998c49..4f9566b5 100644 --- a/crates/codecs/README.md +++ b/crates/codecs/README.md @@ -23,6 +23,7 @@ encode_png(&EncodeImage::new(w, h, 4, Samples::U16(&px)), &meta)?; encode_tiff(&EncodeImage::new(w, h, 3, Samples::F32(&px)), TiffCompression::Deflate, &meta)?; encode_webp_lossless(&img, &meta)?; encode_avif(&img, 70, 6, &meta)?; // native + feature `avif` +encode_jxl(&img, &JxlOptions { quality: 90, ..Default::default() }, &meta)?; // native + feature `jxl` // HDR gain map JPEG (ISO 21496-1): SDR + HDR renditions → base + gain map, and back let (map, gm) = gainmap::compute(&sdr_linear, &hdr_linear, w, h, luma, &GainMapOptions::default())?; @@ -51,7 +52,7 @@ CRW, MRW, X3F) are detected so the engine can route them to `lightcraft-raw`; `d | WebP | lossy + lossless, alpha, first frame of animations | lossless only | ICCP/EXIF/XMP chunks | | GIF / BMP | yes (first GIF frame) | — | via `image` | | PSD / PSB | merged composite: 8/16/32-bit gray, RGB, CMYK, indexed, duotone (as gray); raw/RLE | — | ICC (1039), EXIF (1058), XMP (1060); ZIP-compressed composite, Lab and 1-bit unsupported | -| JPEG XL | yes (jxl-oxide, feature `jxl`, default on) | — | enum colour → rendered straight to linear Rec.2020; ICC → our ICC path; orientation applied by the decoder (reported as 1) | +| JPEG XL | yes (jxl-oxide, feature `jxl`, default on) | yes (jixel, `encode_jxl`, feature `jxl`, native only): lossy VarDCT / lossless Modular, 8/16-bit, effort (fast / normal), progressive; HDR as 16-bit Rec. 2020 PQ | decode: enum colour → rendered straight to linear Rec.2020 (XYB via linear sRGB primaries); ICC → our ICC path; orientation applied by the decoder (reported as 1). Encode: lossy files declare an enum encoding (sRGB, Display P3, Rec. 2020 BT.709 curve, Rec. 2020 PQ), lossless ones embed the ICC profile; EXIF / XMP in `Exif` / `xml ` boxes | | AVIF | **no** | yes (ravif/rav1e, native only, feature `avif`) | 8/10-bit sRGB, EXIF; no ICC in the muxer. HDR: `encode_avif_pq` writes 10-bit BT.2020 PQ (SDR white at 203 cd/m², CICP `nclx` + `clli`) | | HEIC/HEIF | yes with feature `heif` (lightcraft-heif: heic-rs + libheif-matching colour; 4:2:0/mono, 8–10-bit, grids, irot/imir/clap, ICC/nclx/VUI, EXIF/XMP, thumbnail item) | — | 4:2:2/4:4:4 (Canon/Sony HIF) unsupported | | Gain map JPEG (HDR) | gain map found and decoded (`gainmap::read_jpeg`): ISO 21496-1 metadata, else Adobe `hdrgm` XMP | yes (`gainmap::encode_jpeg`) | CIPA DC-007 MPF index (gain map typed `0x050000`), ISO 21496-1 APP2 on both images, `hdrgm` + Container XMP for Android/Chrome, Apple `HDRGainMap`/`HDRToneMap` XMP; gain maps are never used as thumbnails | @@ -87,6 +88,7 @@ All pure Rust, verified with `cargo info` / `cargo tree`: | image (gif, bmp only) | MIT OR Apache-2.0 | GIF/BMP | | moxcms | BSD-3-Clause OR Apache-2.0 | ICC parse/write, CMS | | jxl-oxide | MIT OR Apache-2.0 | JPEG XL decode | +| jixel | BSD-3-Clause OR Apache-2.0 | JPEG XL encode (native only) | | ravif (+ rav1e, avif-serialize) | BSD-3-Clause / BSD-2-Clause | AVIF encode (native only; heavy to compile) | Deliberately **not** used: anything AGPL from imazen (heic, zenjpeg, jxl-encoder, rav1d-safe), diff --git a/crates/codecs/src/encode.rs b/crates/codecs/src/encode.rs index c002abd9..cd1b7427 100644 --- a/crates/codecs/src/encode.rs +++ b/crates/codecs/src/encode.rs @@ -1,6 +1,6 @@ //! Encoders: JPEG (`jpeg-encoder`), PNG (`png`), TIFF (`tiff`), lossless WebP (`image-webp`) and, -//! on native targets with the `avif` feature, AVIF (`ravif`/`rav1e`). Metadata (ICC/EXIF/XMP) is -//! embedded where the format allows. +//! on native targets with the `avif` feature, AVIF (`ravif`/`rav1e`); JPEG XL is in `jxl_encode`. +//! Metadata (ICC/EXIF/XMP) is embedded where the format allows. use crate::{Error, Result}; use lightcraft_raster::Rgba8; @@ -32,7 +32,7 @@ impl<'a> EncodeImage<'a> { EncodeImage { width, height, channels, samples } } - fn validate(&self) -> Result<()> { + pub(crate) fn validate(&self) -> Result<()> { if !(1..=4).contains(&self.channels) { return Err(Error::Encode(format!("unsupported channel count {}", self.channels))); } diff --git a/crates/codecs/src/jxl.rs b/crates/codecs/src/jxl.rs index 00a25fab..26e144ae 100644 --- a/crates/codecs/src/jxl.rs +++ b/crates/codecs/src/jxl.rs @@ -1,5 +1,7 @@ //! JPEG XL decode via `jxl-oxide` (pure Rust). Enum-encoded images are rendered straight into linear //! Rec.2020; ICC-tagged ones are rendered in their own space and interpreted with our ICC path. +//! XYB (lossy) images are rendered to linear sRGB primaries and converted to Rec.2020 here: +//! jxl-oxide 0.12 desaturates colours outside sRGB when it renders XYB to wider primaries. //! jxl-oxide applies the codestream orientation itself, so `orientation` is reported as 1. use crate::convert::{Buf, Meta, Model, Raw, check_size, finish}; @@ -24,11 +26,13 @@ pub(crate) fn decode(bytes: &[u8], opts: &DecodeOptions) -> Result { let gray = header.metadata.grayscale(); let icc = img.original_icc().map(|v| v.to_vec()); let mut meta = Meta { orientation: Some(1), ..Default::default() }; + // linear sRGB primaries, unclamped, then our own matrix to Rec.2020 (see the module docs) + let via_srgb = icc.is_none() && !gray && header.metadata.xyb_encoded; if icc.is_none() { img.request_color_encoding(EnumColourEncoding { colour_space: if gray { ColourSpace::Grey } else { ColourSpace::Rgb }, white_point: WhitePoint::D65, - primaries: Primaries::Bt2100, + primaries: if via_srgb { Primaries::Srgb } else { Primaries::Bt2100 }, tf: TransferFunction::Linear, rendering_intent: RenderingIntent::Relative, }); @@ -55,11 +59,19 @@ pub(crate) fn decode(bytes: &[u8], opts: &DecodeOptions) -> Result { let keep = n_color + has_alpha as usize; let src = fb.buf(); let mut v = Vec::with_capacity(fw * fh * keep); + let to_2020 = lightcraft_color::SRGB.to_space(&lightcraft_color::REC2020).to_f32(); for px in src.chunks_exact(ch) { + let start = v.len(); v.extend_from_slice(&px[..keep.min(ch)]); if keep > ch { v.push(1.0); } + if via_srgb && let Some(rgb) = v.get_mut(start..start + 3) { + let [r, g, b] = [rgb[0], rgb[1], rgb[2]]; + for (o, m) in rgb.iter_mut().zip(&to_2020) { + *o = m[0] * r + m[1] * g + m[2] * b; + } + } } let raw = Raw { width: fw, diff --git a/crates/codecs/src/jxl_encode.rs b/crates/codecs/src/jxl_encode.rs new file mode 100644 index 00000000..e76c5b14 --- /dev/null +++ b/crates/codecs/src/jxl_encode.rs @@ -0,0 +1,188 @@ +//! JPEG XL encode via `jixel` (pure Rust, BSD-3-Clause OR Apache-2.0; native targets with the `jxl` +//! feature: it spawns threads of its own). Lossy files are VarDCT in XYB, tagged with an enum colour +//! encoding the decoder renders back to; lossless files are Modular and keep their samples as they +//! are, tagged with the ICC profile when one is given. EXIF and XMP go in `Exif` / `xml ` boxes. + +use crate::encode::{EncodeImage, EncodeMeta, HDR_REFERENCE_WHITE_NITS, Samples, pq_encode}; +use crate::{Error, Result}; +use serde::{Deserialize, Serialize}; + +/// Encoder effort: time spent searching for a smaller file at the same quality. jixel's slowest +/// search (about 20 % smaller again, five times slower) is not offered: its files are valid, but +/// jxl-oxide, our decoder, renders blocks of them wrongly, so LightCraft could not read back its own +/// exports. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum JxlEffort { + /// Plain 8×8 transforms only: quickest, largest (about a third larger than `Normal`). + Fast, + #[default] + Normal, +} + +impl JxlEffort { + pub fn parse(s: &str) -> Option { + Some(match s.to_ascii_lowercase().as_str() { + "fast" => Self::Fast, + "normal" => Self::Normal, + _ => return None, + }) + } +} + +/// The colour encoding a JPEG XL file declares. Integer samples are display-encoded with its curve. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum JxlColour { + #[default] + Srgb, + /// P3 primaries, D65, sRGB curve. + DisplayP3, + /// Rec. 2020 primaries with the BT.709 curve. + Rec2020, + /// HDR: Rec. 2020 primaries with the PQ curve (SMPTE ST 2084). Float samples are linear light + /// with SDR white at 1.0 (written with SDR white at [`HDR_REFERENCE_WHITE_NITS`]); 16-bit + /// samples are already PQ-encoded. + Rec2020Pq, +} + +/// JPEG XL encoder settings. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct JxlOptions { + /// 1..=100 on a libjpeg-like scale (90 ≈ visually lossless). Ignored when `lossless`. + pub quality: u8, + /// Mathematically lossless (Modular) instead of lossy (VarDCT). + pub lossless: bool, + pub effort: JxlEffort, + /// Lossy: two passes, a coarse image first; lossless: the Squeeze transform (a low-resolution + /// preview that refines to the exact image). + pub progressive: bool, + pub colour: JxlColour, +} + +impl Default for JxlOptions { + fn default() -> Self { + Self { quality: 90, lossless: false, effort: JxlEffort::Normal, progressive: false, colour: JxlColour::Srgb } + } +} + +/// Encode JPEG XL from 8- or 16-bit samples (float samples only with [`JxlColour::Rec2020Pq`]). +/// Gray is widened to RGB; alpha is kept only when some pixel is not opaque. The ICC profile in +/// `meta` is embedded in lossless files (lossy ones carry the enum encoding of `o.colour`); `ppi` +/// has no place in the format. Returns [`Error::Encode`] on wasm32 or without the `jxl` feature. +pub fn encode_jxl(img: &EncodeImage, o: &JxlOptions, meta: &EncodeMeta) -> Result> { + img.validate()?; + #[cfg(all(feature = "jxl", not(target_arch = "wasm32")))] + { + let pixels = Pixels::from(img, o.colour)?; + // jixel asserts internally; a panic there is an export error, not a crash + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run(&pixels, img.width as usize, img.height as usize, o, meta))) + .unwrap_or_else(|_| Err(Error::Encode("JPEG XL encoder panicked".into()))) + } + #[cfg(not(all(feature = "jxl", not(target_arch = "wasm32"))))] + { + let _ = (o, meta); + Err(Error::Encode("JPEG XL encoding is not available in this build".into())) + } +} + +/// Interleaved RGB or RGBA samples for jixel. +#[cfg(all(feature = "jxl", not(target_arch = "wasm32")))] +enum Pixels { + U8 { data: Vec, alpha: bool }, + U16 { data: Vec, alpha: bool }, +} + +#[cfg(all(feature = "jxl", not(target_arch = "wasm32")))] +impl Pixels { + fn from(img: &EncodeImage, colour: JxlColour) -> Result { + let n = (img.width as usize).checked_mul(img.height as usize).ok_or_else(|| Error::Encode("image too large".into()))?; + let ch = img.channels as usize; + // keep alpha only when it says something + fn rgba(s: &[T], ch: usize, n: usize, opaque: impl Fn(T) -> bool) -> (Vec, bool) { + let px = s.chunks_exact(ch).take(n); + let alpha = (ch == 2 || ch == 4) && s.chunks_exact(ch).take(n).any(|c| c.last().is_some_and(|a| !opaque(*a))); + let out_ch = if alpha { 4 } else { 3 }; + let mut v = Vec::with_capacity(n * out_ch); + for c in px { + match c { + [g] => v.extend_from_slice(&[*g, *g, *g]), + [g, a] => v.extend_from_slice(&[*g, *g, *g, *a][..out_ch]), + [r, g, b] => v.extend_from_slice(&[*r, *g, *b]), + [r, g, b, a, ..] => v.extend_from_slice(&[*r, *g, *b, *a][..out_ch]), + [] => {} + } + } + (v, alpha) + } + Ok(match img.samples { + Samples::U8(s) => { + let (data, alpha) = rgba(s, ch, n, |a| a == u8::MAX); + Pixels::U8 { data, alpha } + } + Samples::U16(s) => { + let (data, alpha) = rgba(s, ch, n, |a| a == u16::MAX); + Pixels::U16 { data, alpha } + } + Samples::F32(s) if colour == JxlColour::Rec2020Pq => { + let pq = |v: f32| (pq_encode(if v.is_finite() { v.max(0.0) } else { 0.0 } * HDR_REFERENCE_WHITE_NITS) * 65535.0).round() as u16; + let alpha = |a: f32| (if a.is_finite() { a.clamp(0.0, 1.0) } else { 1.0 } * 65535.0).round() as u16; + let s: Vec = s + .chunks_exact(ch) + .take(n) + .flat_map(|c| match c { + [g] => [pq(*g), pq(*g), pq(*g), u16::MAX], + [g, a] => [pq(*g), pq(*g), pq(*g), alpha(*a)], + [r, g, b] => [pq(*r), pq(*g), pq(*b), u16::MAX], + [r, g, b, a, ..] => [pq(*r), pq(*g), pq(*b), alpha(*a)], + [] => [0; 4], + }) + .collect(); + let (data, alpha) = rgba(&s, 4, n, |a| a == u16::MAX); + Pixels::U16 { data, alpha } + } + Samples::F32(_) => return Err(Error::Encode("JPEG XL float samples are written as HDR (Rec. 2020 PQ) only".into())), + }) + } +} + +#[cfg(all(feature = "jxl", not(target_arch = "wasm32")))] +fn run(px: &Pixels, w: usize, h: usize, o: &JxlOptions, meta: &EncodeMeta) -> Result> { + use jixel::{ColorEncoding, EncodeConfig, Primaries, Speed, TransferFunction}; + let colour = match o.colour { + JxlColour::Srgb => ColorEncoding::srgb(), + JxlColour::DisplayP3 => ColorEncoding::display_p3(), + JxlColour::Rec2020 => ColorEncoding { primaries: Primaries::Bt2020, transfer: TransferFunction::Bt709, ..ColorEncoding::srgb() }, + JxlColour::Rec2020Pq => ColorEncoding::bt2020_pq(), + }; + let mut c = EncodeConfig::default() + .with_quality(f32::from(o.quality.clamp(1, 100))) + .with_color_encoding(colour) + .with_lossless(o.lossless) + .with_progressive(o.progressive) + .with_speed(match o.effort { + JxlEffort::Fast => Speed::Fastest, + JxlEffort::Normal => Speed::Fast, + }); + if o.colour == JxlColour::Rec2020Pq { + // PQ code values are absolute: 1.0 = 10 000 cd/m² + c = c.with_intensity_target(10_000.0); + } + if o.lossless + && let Some(icc) = meta.icc + { + c = c.with_icc_profile(icc.to_vec()); + } + if let Some(exif) = meta.exif { + c = c.with_exif(exif.to_vec()); + } + if let Some(xmp) = meta.xmp { + c = c.with_xmp(xmp.as_bytes().to_vec()); + } + let r = match px { + Pixels::U8 { data, alpha: false } => jixel::encode_image(data, w, h, &c), + Pixels::U8 { data, alpha: true } => jixel::encode_image_with_alpha(data, w, h, &c), + Pixels::U16 { data, alpha: false } => jixel::encode_image_16bit(data, w, h, &c), + Pixels::U16 { data, alpha: true } => jixel::encode_image_with_alpha_16bit(data, w, h, &c), + }; + r.map_err(|e| Error::Encode(format!("JPEG XL: {e}"))) +} diff --git a/crates/codecs/src/lib.rs b/crates/codecs/src/lib.rs index 65798718..c7e8bbb5 100644 --- a/crates/codecs/src/lib.rs +++ b/crates/codecs/src/lib.rs @@ -9,7 +9,8 @@ //! - [`read_header`] reads a file's stored dimensions and orientation from its headers, without //! decoding pixels (import probes). //! - [`decode_thumbnail`] is the fast path for grid thumbnails (EXIF thumbnail or DCT-scaled decode). -//! - [`encode`] writes JPEG, PNG, TIFF, lossless WebP and (native, feature `avif`) AVIF, embedding +//! - [`encode`] writes JPEG, PNG, TIFF, lossless WebP and, on native targets, AVIF (feature `avif`) and +//! JPEG XL ([`encode_jxl`], feature `jxl`), embedding //! ICC/EXIF/XMP. [`icc::write_matrix_trc`] builds profiles for export. //! //! No decoder panics on malformed input (property-tested). @@ -27,6 +28,7 @@ mod jpeg; pub mod jpeg_par; #[cfg(feature = "jxl")] mod jxl; +mod jxl_encode; mod other; mod png_codec; mod psd; @@ -39,6 +41,7 @@ pub use encode::{ ChromaSubsampling, EncodeImage, EncodeMeta, HDR_REFERENCE_WHITE_NITS, Samples, TiffCompression, encode_avif, encode_avif_pq, encode_jpeg, encode_png, encode_tiff, encode_webp_lossless, }; +pub use jxl_encode::{JxlColour, JxlEffort, JxlOptions, encode_jxl}; pub use sniff::{Format, sniff}; pub use space::{NamedSpace, SourceSpace, SpaceOrigin, Trc}; diff --git a/crates/codecs/tests/bench.rs b/crates/codecs/tests/bench.rs index 8bae6901..4c6f03fa 100644 --- a/crates/codecs/tests/bench.rs +++ b/crates/codecs/tests/bench.rs @@ -66,6 +66,15 @@ fn bench_24mp_jpeg() { let rgba = d.to_srgb8(); time("encode PNG 8-bit", mp, 1, || encode_png(&EncodeImage::rgba8(&rgba), &EncodeMeta::default()).unwrap()); time("encode TIFF 8-bit deflate", mp, 1, || encode_tiff(&EncodeImage::rgba8(&rgba), TiffCompression::Deflate, &EncodeMeta::default()).unwrap()); + #[cfg(feature = "jxl")] + for (label, o) in [ + ("encode JPEG XL q90 effort fast", JxlOptions { effort: JxlEffort::Fast, ..Default::default() }), + ("encode JPEG XL q90 effort normal", JxlOptions::default()), + ("encode JPEG XL lossless", JxlOptions { lossless: true, ..Default::default() }), + ] { + let b = time(label, mp, 1, || encode_jxl(&img, &o, &EncodeMeta::default()).unwrap()); + println!(" size {:.1} MB", b.len() as f64 / 1e6); + } } fn exif_with_thumbnail(jpeg: &[u8]) -> Vec { diff --git a/crates/codecs/tests/roundtrip.rs b/crates/codecs/tests/roundtrip.rs index a700ddf6..e62978c5 100644 --- a/crates/codecs/tests/roundtrip.rs +++ b/crates/codecs/tests/roundtrip.rs @@ -635,3 +635,137 @@ fn print_resolution_is_written() { let j = encode_jpeg(&img, 90, ChromaSubsampling::S444, &EncodeMeta::default()).unwrap(); assert_eq!(&j[13..18], &[0, 0, 1, 0, 1]); } + +/// Largest per-channel difference between two linear images. +fn max_diff(a: &lightcraft_raster::Rgb32f, b: &lightcraft_raster::Rgb32f) -> f32 { + a.data.iter().zip(&b.data).flat_map(|(p, q)| (0..3).map(move |c| (p[c] - q[c]).abs())).fold(0.0, f32::max) +} + +#[test] +#[cfg(feature = "jxl")] +fn jxl_lossless_exact_with_metadata() { + let img = gradient(97, 61); + let icc = write_named(NamedSpace::Srgb); + let exif = minimal_exif(1); + let xmp = r#""#; + let meta = EncodeMeta { icc: Some(&icc), exif: Some(&exif), xmp: Some(xmp), ..Default::default() }; + let o = JxlOptions { lossless: true, ..Default::default() }; + let b = encode_jxl(&EncodeImage::rgba8(&img), &o, &meta).unwrap(); + assert_eq!(sniff(&b), Some(Format::Jxl)); + let d = decode(&b, DecodeOptions::default()).unwrap(); + assert_eq!((d.width, d.height), (97, 61)); + assert!(!d.has_alpha, "opaque alpha is dropped"); + assert_eq!(d.icc.as_deref(), Some(&icc[..])); + assert_eq!(d.exif.as_deref(), Some(&exif[..])); + assert_eq!(d.xmp.as_deref(), Some(xmp)); + assert_eq!(d.to_srgb8().data, img.data, "lossless round trip is exact"); + + // 16-bit lossless keeps every code value + let px: Vec = (0..40 * 30 * 3).map(|i| (i as u32 * 2741 % 65536) as u16).collect(); + let b = encode_jxl(&EncodeImage::new(40, 30, 3, Samples::U16(&px)), &o, &meta).unwrap(); + let d = decode(&b, DecodeOptions::default()).unwrap(); + assert_eq!(d.bit_depth, 16); + let png = encode_png(&EncodeImage::new(40, 30, 3, Samples::U16(&px)), &meta).unwrap(); + let want = decode(&png, DecodeOptions::default()).unwrap(); + assert!(max_diff(&d.image, &want.image) < 1e-5, "16-bit lossless differs by {}", max_diff(&d.image, &want.image)); +} + +#[test] +#[cfg(feature = "jxl")] +fn jxl_lossy_quality_and_effort() { + let img = smooth(160, 120); + let opaque = Rgba8 { data: img.data.iter().map(|p| [p[0], p[1], p[2], 255]).collect(), ..img.clone() }; + let enc = |o: JxlOptions| encode_jxl(&EncodeImage::rgba8(&opaque), &o, &EncodeMeta::default()).unwrap(); + let hi = enc(JxlOptions { quality: 95, ..Default::default() }); + let lo = enc(JxlOptions { quality: 40, ..Default::default() }); + assert!(lo.len() < hi.len(), "q40 {} ≥ q95 {}", lo.len(), hi.len()); + for (q, b) in [(95, &hi), (40, &lo)] { + let d = decode(b, DecodeOptions::default()).unwrap(); + let p = psnr8(&opaque, &d.to_srgb8()); + assert!(p > if q == 95 { 38.0 } else { 28.0 }, "q{q} psnr {p}"); + } + for effort in [JxlEffort::Fast, JxlEffort::Normal] { + for progressive in [false, true] { + let b = enc(JxlOptions { effort, progressive, ..Default::default() }); + let p = psnr8(&opaque, &decode(&b, DecodeOptions::default()).unwrap().to_srgb8()); + assert!(p > 34.0, "{effort:?} progressive={progressive} psnr {p}"); + } + } +} + +#[test] +#[cfg(feature = "jxl")] +fn jxl_lossy_wide_gamut_matches_lossless_icc() { + // the same display-encoded samples, lossy with the enum encoding and lossless with the ICC + // profile, decode to the same colours in the working space + let img = smooth(96, 64); + let opaque = Rgba8 { data: img.data.iter().map(|p| [p[0], p[1], p[2], 255]).collect(), ..img.clone() }; + for (colour, named) in [(JxlColour::DisplayP3, NamedSpace::DisplayP3), (JxlColour::Rec2020, NamedSpace::Rec2020)] { + let icc = write_named(named); + let meta = EncodeMeta { icc: Some(&icc), ..Default::default() }; + let e = EncodeImage::rgba8(&opaque); + let lossy = encode_jxl(&e, &JxlOptions { quality: 98, colour, ..Default::default() }, &meta).unwrap(); + let exact = encode_jxl(&e, &JxlOptions { lossless: true, colour, ..Default::default() }, &meta).unwrap(); + let a = decode(&lossy, DecodeOptions::default()).unwrap(); + assert!(a.icc.is_none(), "lossy files carry the enum encoding"); + let b = decode(&exact, DecodeOptions::default()).unwrap(); + let d = max_diff(&a.to_working(), &b.to_working()); + assert!(d < 0.03, "{colour:?}: lossy differs from lossless by {d}"); + } +} + +#[test] +#[cfg(feature = "jxl")] +fn jxl_alpha_and_gray() { + let mut img = gradient(32, 24); + img.data[5][3] = 0; + let b = encode_jxl(&EncodeImage::rgba8(&img), &JxlOptions { lossless: true, ..Default::default() }, &EncodeMeta::default()).unwrap(); + let d = decode(&b, DecodeOptions::default()).unwrap(); + assert!(d.has_alpha); + assert_eq!(d.alpha.as_ref().map(|a| a.data[5]), Some(0.0)); + let g: Vec = (0..32 * 24).map(|i| (i % 256) as u8).collect(); + let b = encode_jxl(&EncodeImage::new(32, 24, 1, Samples::U8(&g)), &JxlOptions { lossless: true, ..Default::default() }, &EncodeMeta::default()) + .unwrap(); + let d = decode(&b, DecodeOptions::default()).unwrap(); + assert_eq!(d.to_srgb8().data.iter().map(|p| p[0]).collect::>(), g); +} + +#[test] +#[cfg(feature = "jxl")] +fn jxl_hdr_pq() { + // linear Rec. 2020, SDR white = 1.0: the left half at SDR white, the right half four times brighter + let (w, h) = (64usize, 32usize); + let px: Vec = (0..w * h).flat_map(|i| [if i % w < w / 2 { 1.0 } else { 4.0 }; 3]).collect(); + for lossless in [false, true] { + let o = JxlOptions { lossless, quality: 95, colour: JxlColour::Rec2020Pq, ..Default::default() }; + let b = encode_jxl(&EncodeImage::new(w as u32, h as u32, 3, Samples::F32(&px)), &o, &EncodeMeta::default()).unwrap(); + // the file's own samples: PQ code values for 203 and 812 cd/m² + let img = jxl_oxide::JxlImage::builder().read(std::io::Cursor::new(&b)).unwrap(); + let m = &img.image_header().metadata; + assert_eq!(m.tone_mapping.intensity_target, 10_000.0); + assert!(format!("{:?}", m.colour_encoding).contains("Pq"), "{:?}", m.colour_encoding); + let r = img.render_frame(0).unwrap(); + let fb = r.image_all_channels(); + let at = |x: usize| fb.buf()[((h / 2) * w + x) * fb.channels() + 1]; + for (x, nits) in [(w / 4, HDR_REFERENCE_WHITE_NITS), (w * 3 / 4, 4.0 * HDR_REFERENCE_WHITE_NITS)] { + let (got, want) = (at(x), lightcraft_codecs::encode::pq_encode(nits)); + assert!((got - want).abs() < 0.005, "lossless={lossless}: {nits} cd/m² written as {got}, want {want}"); + } + } + // float samples are HDR only + let sdr = encode_jxl(&EncodeImage::new(w as u32, h as u32, 3, Samples::F32(&px)), &JxlOptions::default(), &EncodeMeta::default()); + assert!(matches!(sdr, Err(Error::Encode(_)))); +} + +#[test] +#[cfg(feature = "jxl")] +fn jxl_bad_input_is_an_error() { + let px = [0u8; 12]; + assert!(encode_jxl(&EncodeImage::new(4, 4, 3, Samples::U8(&px)), &JxlOptions::default(), &EncodeMeta::default()).is_err()); + assert!(encode_jxl(&EncodeImage::new(0, 4, 3, Samples::U8(&px)), &JxlOptions::default(), &EncodeMeta::default()).is_err()); + // a 1×1 image and quality 1 still encode + let one = [10u8, 20, 30]; + let b = + encode_jxl(&EncodeImage::new(1, 1, 3, Samples::U8(&one)), &JxlOptions { quality: 1, ..Default::default() }, &EncodeMeta::default()).unwrap(); + assert_eq!(decode(&b, DecodeOptions::default()).unwrap().width, 1); +} diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 4036a5c9..1ef48821 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -49,6 +49,8 @@ denoise = ["dep:lightcraft-denoise", "lightcraft-denoise/runtime", "lightcraft-g rawnind-model = ["denoise", "lightcraft-denoise/rawnind-model", "lightcraft-denoise-core/rawnind-model"] [dev-dependencies] +# HDR JPEG XL tests read the file's colour encoding +jxl-oxide = { workspace = true } # Lossless synthetic HEIC files (the import test; decoded when the codecs' `heif` feature is on). lightcraft-heif = { workspace = true, features = ["testdata"] } libc = { workspace = true } diff --git a/crates/engine/src/export.rs b/crates/engine/src/export.rs index 6a9e9147..f0115d12 100644 --- a/crates/engine/src/export.rs +++ b/crates/engine/src/export.rs @@ -1,11 +1,11 @@ -//! Export: encode a rendered image to JPEG / PNG / TIFF / WebP / AVIF (or copy the original / write a +//! Export: encode a rendered image to JPEG / PNG / TIFF / WebP / AVIF / JPEG XL (or copy the original / write a //! DNG, each with the edits in XMP) in sRGB, Display P3, Adobe RGB //! (1998) compatible, ProPhoto RGB or Rec. 2020 with an embedded ICC profile we generate from the //! published primaries and curves, optional output sharpening and a JPEG file-size limit. Pure (bytes in, bytes out) so the desktop //! app, CLI, MCP and the web build share it; writing the file is the caller's job. -pub use lightcraft_codecs::TiffCompression; -use lightcraft_codecs::{ChromaSubsampling, EncodeImage, EncodeMeta, NamedSpace, Samples, encode, icc}; +use lightcraft_codecs::{ChromaSubsampling, EncodeImage, EncodeMeta, JxlColour, JxlOptions, NamedSpace, Samples, encode, icc}; +pub use lightcraft_codecs::{JxlEffort, TiffCompression}; use lightcraft_meta::{DateTime, Gps, Metadata}; pub use lightcraft_pipeline::{DeepImage, DeepSamples, OutputDepth, OutputSpace}; use lightcraft_raster::Rgba8; @@ -20,6 +20,8 @@ pub enum ExportFormat { Tiff, Webp, Avif, + /// JPEG XL: lossy (VarDCT) or lossless, 8/16-bit, HDR as Rec. 2020 PQ. + Jxl, /// The original file, unchanged, with an XMP sidecar holding the edits. Original, /// A DNG of the raw data (raw photos only), the edits embedded as XMP. @@ -34,6 +36,7 @@ impl ExportFormat { "tiff" | "tif" => Self::Tiff, "webp" => Self::Webp, "avif" => Self::Avif, + "jxl" | "jpegxl" | "jpeg-xl" | "jpeg_xl" => Self::Jxl, "original" => Self::Original, "dng" => Self::Dng, _ => return None, @@ -46,6 +49,7 @@ impl ExportFormat { Self::Tiff => "tif", Self::Webp => "webp", Self::Avif => "avif", + Self::Jxl => "jxl", // the original keeps its own extension (see `ExportOptions::file_name_for`) Self::Original => "", Self::Dng => "dng", @@ -564,7 +568,7 @@ pub enum MetadataPolicy { #[serde(default, rename_all = "camelCase")] pub struct ExportOptions { pub format: ExportFormat, - /// 1–100 (JPEG, AVIF). + /// 1–100 (JPEG, AVIF, lossy JPEG XL). pub quality: u8, /// Output size (`None` = full size). pub resize: Option, @@ -595,16 +599,24 @@ pub struct ExportOptions { pub remove_location: bool, /// Text watermark (none when absent or the text is empty). pub watermark: Option, - /// Output colour space (AVIF is always sRGB: its muxer cannot embed a profile). + /// Output colour space (AVIF is always sRGB: its muxer cannot embed a profile; lossy JPEG XL + /// writes Adobe RGB and ProPhoto as Rec. 2020, see [`ExportOptions::effective_space`]). pub color_space: OutputSpace, - /// Bits per channel: 8, 16 (PNG, TIFF), 32 (TIFF: float, linear) or 10 (AVIF); `None` = the + /// Bits per channel: 8, 16 (PNG, TIFF, JPEG XL), 32 (TIFF: float, linear) or 10 (AVIF); `None` = the /// format's default (TIFF 16, everything else 8). See [`ExportOptions::effective_depth`]. pub bit_depth: Option, /// HDR output for photos edited in HDR: JPEG as an ISO 21496-1 gain map JPEG (the SDR /// rendition plus a gain map, so the file looks right everywhere; quality as set, `limit_kb` - /// not applied), AVIF as 10-bit Rec. 2020 PQ, 32-bit float TIFF with the highlights above SDR - /// white kept. Other formats, and photos without an HDR edit, are SDR. + /// not applied), AVIF as 10-bit Rec. 2020 PQ, JPEG XL as 16-bit Rec. 2020 PQ, 32-bit float + /// TIFF with the highlights above SDR white kept. Other formats, and photos without an HDR + /// edit, are SDR. pub hdr: bool, + /// JPEG XL: mathematically lossless instead of lossy (`quality` is then unused). + pub jxl_lossless: bool, + /// JPEG XL: encoder effort (time spent for a smaller file). + pub jxl_effort: JxlEffort, + /// JPEG XL: progressive file (a coarse image first, refined as it loads). + pub jxl_progressive: bool, } impl Default for ExportOptions { @@ -629,6 +641,9 @@ impl Default for ExportOptions { color_space: OutputSpace::Srgb, bit_depth: None, hdr: false, + jxl_lossless: false, + jxl_effort: JxlEffort::Normal, + jxl_progressive: false, } } } @@ -665,6 +680,9 @@ pub const OPTION_PARAMS: &[&str] = &[ "colorSpace", "bitDepth", "hdr", + "jxlLossless", + "jxlEffort", + "jxlProgressive", ]; /// The keys of a `watermark` object ([`Watermark`], camelCase). @@ -766,7 +784,7 @@ impl ExportOptions { match k.as_str() { "format" => { if ExportFormat::parse(string(cmd, k, v)?).is_none() { - return Err(bad(format!("`{k}` must be one of jpeg|png|tiff|webp|avif|dng|original"))); + return Err(bad(format!("`{k}` must be one of jpeg|png|tiff|webp|avif|jxl|dng|original"))); } } "quality" => { @@ -796,7 +814,7 @@ impl ExportOptions { } serde_json::from_value::(v.clone()).map_err(|e| bad(format!("`{k}`: {e}")))?; } - "dontEnlarge" | "removeLocation" | "background" | "hdr" => { + "dontEnlarge" | "removeLocation" | "background" | "hdr" | "jxlLossless" | "jxlProgressive" => { boolean(k, v)?; } "naming" | "subfolder" | "path" | "dir" | "preset" => { @@ -806,6 +824,7 @@ impl ExportOptions { "sharpenAmount" => one_of(k, v, &["low", "standard", "high"])?, "conflict" => one_of(k, v, &["unique", "overwrite", "skip"])?, "tiffCompression" => one_of(k, v, &["none", "lzw", "zip", "deflate"])?, + "jxlEffort" => one_of(k, v, &["fast", "normal"])?, "dngCompression" => { serde_json::from_value::(v.clone()) .map_err(|_| bad(format!("`{k}` must be one of lossless|deflate|uncompressed")))?; @@ -930,6 +949,9 @@ impl ExportOptions { color_space: s("colorSpace").and_then(OutputSpace::parse).unwrap_or(d.color_space), bit_depth: u("bitDepth").filter(|b| matches!(b, 8 | 10 | 16 | 32)).map(|b| b as u8), hdr: p.get("hdr").and_then(Value::as_bool).unwrap_or(d.hdr), + jxl_lossless: p.get("jxlLossless").and_then(Value::as_bool).unwrap_or(d.jxl_lossless), + jxl_effort: s("jxlEffort").and_then(JxlEffort::parse).unwrap_or(d.jxl_effort), + jxl_progressive: p.get("jxlProgressive").and_then(Value::as_bool).unwrap_or(d.jxl_progressive), } } @@ -1004,6 +1026,9 @@ impl ExportOptions { (ExportFormat::Avif, _) if self.hdr => OutputDepth::F32Hdr, (ExportFormat::Avif, Some(10 | 16 | 32)) => OutputDepth::U16, (ExportFormat::Avif, _) => OutputDepth::U8, + (ExportFormat::Jxl, _) if self.hdr => OutputDepth::F32Hdr, + (ExportFormat::Jxl, Some(10 | 16 | 32)) => OutputDepth::U16, + (ExportFormat::Jxl, _) => OutputDepth::U8, } } @@ -1014,24 +1039,46 @@ impl ExportOptions { ExportFormat::Png => &[(8, "8-bit"), (16, "16-bit")], ExportFormat::Tiff => &[(16, "16-bit"), (8, "8-bit"), (32, "32-bit float")], ExportFormat::Avif => &[(8, "8-bit"), (10, "10-bit")], + ExportFormat::Jxl => &[(8, "8-bit"), (16, "16-bit")], } } - /// Whether these options write HDR files (for photos edited in HDR): JPEG (gain map), AVIF - /// (PQ) or 32-bit float TIFF with [`ExportOptions::hdr`]. + /// Whether these options write HDR files (for photos edited in HDR): JPEG (gain map), AVIF or + /// JPEG XL (PQ) or 32-bit float TIFF with [`ExportOptions::hdr`]. pub fn hdr_output(&self) -> bool { - self.hdr && matches!((self.format, self.bit_depth), (ExportFormat::Jpeg | ExportFormat::Avif, _) | (ExportFormat::Tiff, Some(32))) + self.hdr + && matches!( + (self.format, self.bit_depth), + (ExportFormat::Jpeg | ExportFormat::Avif | ExportFormat::Jxl, _) | (ExportFormat::Tiff, Some(32)) + ) } - /// The colour space the file is actually written in (AVIF: sRGB, or Rec. 2020 for HDR). + /// The colour space the file is actually written in (AVIF: sRGB; AVIF and JPEG XL HDR: Rec. + /// 2020; lossy JPEG XL: Adobe RGB and ProPhoto as Rec. 2020, the widest space its encoder can + /// declare without a profile). pub fn effective_space(&self) -> OutputSpace { match self.format { - ExportFormat::Avif if self.hdr => OutputSpace::Rec2020, + ExportFormat::Avif | ExportFormat::Jxl if self.hdr => OutputSpace::Rec2020, ExportFormat::Avif => OutputSpace::Srgb, + ExportFormat::Jxl if !self.jxl_lossless && matches!(self.color_space, OutputSpace::AdobeRgb | OutputSpace::ProPhoto) => { + OutputSpace::Rec2020 + } _ => self.color_space, } } + /// The JPEG XL encoder settings for these options, writing samples in `space`. + fn jxl(&self, space: OutputSpace, hdr: bool) -> JxlOptions { + let colour = match space { + _ if hdr => JxlColour::Rec2020Pq, + OutputSpace::DisplayP3 => JxlColour::DisplayP3, + OutputSpace::Rec2020 => JxlColour::Rec2020, + // lossless files carry the ICC profile; lossy ones never get here (`effective_space`) + OutputSpace::Srgb | OutputSpace::AdobeRgb | OutputSpace::ProPhoto => JxlColour::Srgb, + }; + JxlOptions { quality: self.quality, lossless: self.jxl_lossless, effort: self.jxl_effort, progressive: self.jxl_progressive, colour } + } + /// Output file name for photo `p` at 1-based position `seq` in a batch (the original's /// extension is kept for [`ExportFormat::Original`]). pub fn file_name_for(&self, p: &lightcraft_catalog::Photo, seq: usize) -> String { @@ -1178,6 +1225,7 @@ pub fn encode_with_metadata(img: &Rgba8, o: &ExportOptions, meta: Option<&Metada ExportFormat::Tiff => encode::encode_tiff(&e, o.tiff_compression, &meta), ExportFormat::Webp => encode::encode_webp_lossless(&e, &meta), ExportFormat::Avif => encode::encode_avif(&e, o.quality, 8, &meta), + ExportFormat::Jxl => lightcraft_codecs::encode_jxl(&e, &o.jxl(space, false), &meta), f @ (ExportFormat::Original | ExportFormat::Dng) => return Err(format!("{f:?} export does not encode pixels")), }; r.map_err(|e| e.to_string()) @@ -1289,6 +1337,10 @@ pub fn encode_deep(img: &DeepImage, o: &ExportOptions, meta: Option<&Metadata>) lightcraft_codecs::encode_avif_pq(w, h, v, o.quality, 8, &meta) } (ExportFormat::Avif, _) => encode::encode_avif(&e, o.quality, 8, &meta), + (ExportFormat::Jxl, DeepSamples::F32(_)) if img.space == OutputSpace::Rec2020 => { + lightcraft_codecs::encode_jxl(&e, &o.jxl(img.space, true), &meta) + } + (ExportFormat::Jxl, DeepSamples::U16(_)) => lightcraft_codecs::encode_jxl(&e, &o.jxl(img.space, false), &meta), (f, _) => return Err(format!("{f:?} export is 8-bit only")), }; r.map_err(|e| e.to_string()) @@ -2418,6 +2470,75 @@ mod tests { } } + #[test] + fn jxl_options() { + assert_eq!(ExportFormat::parse("JPEG-XL"), Some(ExportFormat::Jxl)); + let o = ExportOptions::from_params(&json!({"format": "jxl", "jxlLossless": true, "jxlEffort": "fast", "jxlProgressive": true})).unwrap(); + assert_eq!((o.format, o.jxl_lossless, o.jxl_effort, o.jxl_progressive), (ExportFormat::Jxl, true, JxlEffort::Fast, true)); + assert_eq!(ExportOptions::from_json(&o.to_json()), o, "saved presets keep the JPEG XL settings"); + assert!(ExportOptions::from_params(&json!({"jxlEffort": "best"})).is_err()); + assert!(ExportOptions::from_params(&json!({"jxlLossless": "yes"})).is_err()); + let space = |lossless, color_space, hdr| { + ExportOptions { format: ExportFormat::Jxl, jxl_lossless: lossless, color_space, hdr, ..Default::default() }.effective_space() + }; + assert_eq!(space(false, OutputSpace::DisplayP3, false), OutputSpace::DisplayP3); + assert_eq!(space(false, OutputSpace::ProPhoto, false), OutputSpace::Rec2020, "lossy has no ProPhoto enum"); + assert_eq!(space(true, OutputSpace::ProPhoto, false), OutputSpace::ProPhoto, "lossless keeps the profile"); + assert_eq!(space(true, OutputSpace::Srgb, true), OutputSpace::Rec2020); + let o = ExportOptions { format: ExportFormat::Jxl, hdr: true, ..Default::default() }; + assert!(o.hdr_output()); + assert_eq!(o.effective_depth(), OutputDepth::F32Hdr); + assert_eq!(ExportFormat::Jxl.extension(), "jxl"); + } + + #[test] + fn jxl_every_space_round_trips() { + // wide-gamut green stays outside sRGB in P3, Rec. 2020 and (lossless) ProPhoto files + for (lossless, space) in [ + (false, OutputSpace::Srgb), + (false, OutputSpace::DisplayP3), + (false, OutputSpace::Rec2020), + (true, OutputSpace::DisplayP3), + (true, OutputSpace::AdobeRgb), + (true, OutputSpace::ProPhoto), + ] { + let o = ExportOptions { format: ExportFormat::Jxl, color_space: space, jxl_lossless: lossless, quality: 95, ..Default::default() }; + let png = ExportOptions { format: ExportFormat::Png, color_space: space, ..Default::default() }; + let img = p3_green(space); + let bytes = encode_image(&img, &o).unwrap(); + assert_eq!(lightcraft_codecs::sniff(&bytes), Some(lightcraft_codecs::Format::Jxl)); + let (got, _) = decoded_in_srgb(&bytes); + let (want, _) = decoded_in_srgb(&encode_image(&img, &png).unwrap()); + for c in 0..3 { + assert!((got[c] - want[c]).abs() < 0.01, "{space:?} lossless={lossless}: {got:?} vs PNG {want:?}"); + } + if space != OutputSpace::Srgb { + assert!(got[0] < -0.03 || got[2] < -0.03, "{space:?}: outside sRGB after the round trip: {got:?}"); + } + } + } + + #[test] + fn jxl_sixteen_bit() { + let o = ExportOptions { + format: ExportFormat::Jxl, + bit_depth: Some(16), + jxl_lossless: true, + color_space: OutputSpace::DisplayP3, + ..Default::default() + }; + assert_eq!(o.effective_depth(), OutputDepth::U16); + let (levels8, _) = distinct_levels( + &encode_rendered(&ramp(OutputDepth::U8, OutputSpace::DisplayP3), &ExportOptions { bit_depth: Some(8), ..o.clone() }, None).unwrap(), + ); + let (levels, d) = distinct_levels(&encode_rendered(&ramp(OutputDepth::U16, OutputSpace::DisplayP3), &o, None).unwrap()); + assert_eq!(d.bit_depth, 16); + assert!(levels > 10 * levels8, "{levels} vs {levels8}"); + // lossy 16-bit encodes too + let lossy = ExportOptions { jxl_lossless: false, ..o }; + assert!(encode_rendered(&ramp(OutputDepth::U16, OutputSpace::DisplayP3), &lossy, None).is_ok()); + } + #[test] fn bit_depth_options_per_format() { let d = |format, bit_depth| ExportOptions { format, bit_depth, ..Default::default() }.effective_depth(); @@ -2428,7 +2549,9 @@ mod tests { assert_eq!(d(ExportFormat::Avif, Some(10)), OutputDepth::U16); assert_eq!(ExportOptions::from_json(&serde_json::json!({"bitDepth": 16})).bit_depth, Some(16)); assert_eq!(ExportOptions::from_json(&serde_json::json!({"bitDepth": 12})).bit_depth, None); - for f in [ExportFormat::Jpeg, ExportFormat::Png, ExportFormat::Tiff, ExportFormat::Webp, ExportFormat::Avif] { + assert_eq!(d(ExportFormat::Jxl, Some(16)), OutputDepth::U16); + assert_eq!(d(ExportFormat::Jxl, None), OutputDepth::U8); + for f in [ExportFormat::Jpeg, ExportFormat::Png, ExportFormat::Tiff, ExportFormat::Webp, ExportFormat::Avif, ExportFormat::Jxl] { let first = ExportOptions::bit_depths(f)[0].0; let def = d(f, None); assert_eq!(d(f, Some(first)), def, "{f:?}: the first choice is the default"); diff --git a/crates/engine/src/tests_export.rs b/crates/engine/src/tests_export.rs index b8636c01..e5d95fcf 100644 --- a/crates/engine/src/tests_export.rs +++ b/crates/engine/src/tests_export.rs @@ -440,3 +440,37 @@ fn hdr_avif_export_is_pq_rec2020() { // a photo without an HDR edit exports as an ordinary (sRGB) AVIF assert!(nclx(&sdr.bytes).is_none_or(|c| c[3] != 16)); } + +#[test] +fn hdr_jxl_export_is_pq_rec2020() { + let mut s = Session::with_demo(); + let id = sunset(&mut s); + let o = ExportOptions::from_json(&json!({"longEdge": 160, "format": "jxl", "hdr": true, "quality": 90})); + assert!(o.hdr_output()); + let sdr = export_photo(&mut s, id, &o, 1).unwrap(); + s.execute("develop.hdr", &json!({"enabled": true, "maxEv": 2})).unwrap(); + let hdr = export_photo(&mut s, id, &o, 1).unwrap(); + let encoding = |b: &[u8]| { + let img = jxl_oxide::JxlImage::builder().read(std::io::Cursor::new(b)).unwrap(); + let m = &img.image_header().metadata; + (format!("{:?}", m.colour_encoding), m.bit_depth.bits_per_sample()) + }; + let (c, bits) = encoding(&hdr.bytes); + assert!(c.contains("Bt2100") && c.contains("Pq"), "{c}"); + assert_eq!(bits, 16); + // a photo without an HDR edit exports as an ordinary SDR file + let (c, _) = encoding(&sdr.bytes); + assert!(!c.contains("Pq"), "{c}"); +} + +#[test] +fn jxl_export_writes_metadata() { + let mut s = Session::with_demo(); + let id = sunset(&mut s); + let o = ExportOptions::from_json(&json!({"longEdge": 120, "format": "jxl", "quality": 80})); + let e = export_photo(&mut s, id, &o, 1).unwrap(); + let d = lightcraft_codecs::decode(&e.bytes, Default::default()).unwrap(); + assert_eq!(d.width.max(d.height), 120); + assert!(d.xmp.as_deref().is_some_and(|x| x.contains("Golden horizon")), "title in the XMP box"); + assert!(d.exif.is_some(), "EXIF box"); +} diff --git a/crates/mcp/src/headless.rs b/crates/mcp/src/headless.rs index acbecb60..18849c5e 100644 --- a/crates/mcp/src/headless.rs +++ b/crates/mcp/src/headless.rs @@ -131,7 +131,7 @@ impl Backend for Headless { "engine.commands" => { let mut v: Vec = self.session.commands().into_iter().map(|c| serde_json::to_value(c).unwrap_or_default()).collect(); v.push(json!({"id": "app.export", "label": "Export Now", "menu": [], "shortcut": null, - "params": "{path?: output file (.jpg/.png/.tif/.webp/.avif/.dng) | dir?, ids?, preset?, format?: jpeg|png|tiff|webp|avif|dng|original, longEdge?|shortEdge?|width?|height?|megapixels?|percent? (default longEdge 3000; longEdge 0 = full size), dontEnlarge?, ppi?, quality?: 1..100, limitKb?, colorSpace?: srgb|displayP3|adobeRgb|proPhoto|rec2020, bitDepth?: 8|10|16|32, hdr? (HDR output for HDR edits: gain map JPEG, float TIFF), sharpen?: none|screen|matte|glossy, sharpenAmount?: low|standard|high, metadata?: all|allExceptCamera|copyright|none, removeLocation?, naming?, startNumber?, subfolder?, conflict?: unique|overwrite|skip, tiffCompression?: none|lzw|zip, dngCompression?: lossless|deflate|uncompressed, watermark?: text | {text?, vertical? (upright columns, right to left), size? (text height, 0.005..0.5 of the short edge; default 0.035), opacity? (0..1; 0.7), anchor?: topLeft|top|topRight|left|center|right|bottomLeft|bottom|bottomRight, inset? (margin, 0..0.4 of the short edge; 0.025), color? [r,g,b] sRGB, shadow?, image? (graphic drawn instead of the text), imageWidth? (0.01..1 of the photo's width; 0.2)}} — an unknown parameter, an out-of-range watermark size or a value of the wrong kind is an error, not a default", + "params": "{path?: output file (.jpg/.png/.tif/.webp/.avif/.jxl/.dng) | dir?, ids?, preset?, format?: jpeg|png|tiff|webp|avif|jxl|dng|original, longEdge?|shortEdge?|width?|height?|megapixels?|percent? (default longEdge 3000; longEdge 0 = full size), dontEnlarge?, ppi?, quality?: 1..100, jxlLossless?, jxlEffort?: fast|normal, jxlProgressive?, limitKb?, colorSpace?: srgb|displayP3|adobeRgb|proPhoto|rec2020, bitDepth?: 8|10|16|32, hdr? (HDR output for HDR edits: gain map JPEG, PQ AVIF / JPEG XL, float TIFF), sharpen?: none|screen|matte|glossy, sharpenAmount?: low|standard|high, metadata?: all|allExceptCamera|copyright|none, removeLocation?, naming?, startNumber?, subfolder?, conflict?: unique|overwrite|skip, tiffCompression?: none|lzw|zip, dngCompression?: lossless|deflate|uncompressed, watermark?: text | {text?, vertical? (upright columns, right to left), size? (text height, 0.005..0.5 of the short edge; default 0.035), opacity? (0..1; 0.7), anchor?: topLeft|top|topRight|left|center|right|bottomLeft|bottom|bottomRight, inset? (margin, 0..0.4 of the short edge; 0.025), color? [r,g,b] sRGB, shadow?, image? (graphic drawn instead of the text), imageWidth? (0.01..1 of the photo's width; 0.2)}} — an unknown parameter, an out-of-range watermark size or a value of the wrong kind is an error, not a default", "enabled": self.session.active().is_some()})); Ok(Value::Array(v)) } @@ -183,7 +183,7 @@ pub fn expand_paths(paths: &[String]) -> Vec { } /// Encode by extension: `.png` (default), `.jpg`/`.jpeg` (quality), `.tif`/`.tiff`, `.webp` (lossless), -/// `.avif` — via the shared export encoder (embeds an sRGB profile). +/// `.avif`, `.jxl` — via the shared export encoder (embeds an sRGB profile). pub fn encode_image(ext: &str, img: &Rgba8, quality: u8) -> Result, String> { use lightcraft_engine::export::{ExportFormat, ExportOptions}; let format = ExportFormat::parse(ext).unwrap_or(ExportFormat::Png); diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index dd52da40..5dadba5e 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -239,7 +239,7 @@ pub fn helper_tools(has_ui: bool) -> Vec { "conflict": {"type": "string", "enum": ["unique", "overwrite", "skip"]}, "tiffCompression": {"type": "string", "enum": ["none", "lzw", "zip"]}, "dngCompression": {"type": "string", "enum": ["lossless", "deflate", "uncompressed"]}, - "format": {"type": "string", "enum": ["jpeg", "png", "tiff", "webp", "avif", "original", "dng"], "description": "original = the file as is + an XMP sidecar with the edits; dng = raw photos as DNG with the edits embedded"}, + "format": {"type": "string", "enum": ["jpeg", "png", "tiff", "webp", "avif", "jxl", "original", "dng"], "description": "original = the file as is + an XMP sidecar with the edits; dng = raw photos as DNG with the edits embedded"}, "longEdge": {"type": "integer", "description": "Output long edge in px. When no size param is given: 3000; 0 = full size (cropped, native resolution)"}, "shortEdge": {"type": "integer", "description": "Output short edge in px"}, "width": {"type": "integer", "description": "Output width in px (with height: fit inside width × height, either orientation)"}, @@ -248,7 +248,10 @@ pub fn helper_tools(has_ui: bool) -> Vec { "percent": {"type": "number", "description": "Output size as a percentage of full size"}, "dontEnlarge": {"type": "boolean", "description": "Never upscale past the photo's own size (default true)"}, "ppi": {"type": "integer", "description": "Print resolution written into the file (default 240)"}, - "quality": {"type": "integer", "description": "JPEG/AVIF quality 1..100"}, + "quality": {"type": "integer", "description": "JPEG/AVIF/lossy JPEG XL quality 1..100"}, + "jxlLossless": {"type": "boolean", "description": "JPEG XL: mathematically lossless (quality unused); default false"}, + "jxlEffort": {"type": "string", "enum": ["fast", "normal"], "description": "JPEG XL encoder effort: normal (default) makes files about a quarter smaller than fast, in about twice the time"}, + "jxlProgressive": {"type": "boolean", "description": "JPEG XL: progressive file, a coarse image first (default false)"}, "limitKb": {"type": "integer", "description": "JPEG: largest quality that fits this many KB"}, "sharpen": {"type": "string", "enum": ["none", "screen", "matte", "glossy"]}, "sharpenAmount": {"type": "string", "enum": ["low", "standard", "high"]}, @@ -256,9 +259,9 @@ pub fn helper_tools(has_ui: bool) -> Vec { "startNumber": {"type": "integer", "description": "First {seq} value (default 1)"}, "metadata": {"type": "string", "enum": ["all", "allExceptCamera", "copyright", "none"]}, "removeLocation": {"type": "boolean"}, - "colorSpace": {"type": "string", "enum": ["srgb", "displayP3", "adobeRgb", "proPhoto", "rec2020"], "description": "Output colour space (default sRGB; AVIF is always sRGB). adobeRgb = Adobe RGB (1998) compatible; the embedded ICC profile is generated from the published primaries"}, - "bitDepth": {"type": "integer", "enum": [8, 10, 16, 32], "description": "Bits per channel: PNG 8|16 (default 8), TIFF 8|16|32 (default 16; 32 = linear float with a linear profile), AVIF 8|10; JPEG/WebP are 8-bit"}, - "hdr": {"type": "boolean", "description": "HDR output for photos edited in HDR (develop.hdr): JPEG as an ISO 21496-1 gain map JPEG, AVIF as 10-bit Rec. 2020 PQ, 32-bit TIFF with highlights above 1; other formats and SDR edits export as usual"}, + "colorSpace": {"type": "string", "enum": ["srgb", "displayP3", "adobeRgb", "proPhoto", "rec2020"], "description": "Output colour space (default sRGB; AVIF is always sRGB; lossy JPEG XL writes adobeRgb/proPhoto as rec2020). adobeRgb = Adobe RGB (1998) compatible; the embedded ICC profile is generated from the published primaries"}, + "bitDepth": {"type": "integer", "enum": [8, 10, 16, 32], "description": "Bits per channel: PNG 8|16 (default 8), TIFF 8|16|32 (default 16; 32 = linear float with a linear profile), AVIF 8|10, JPEG XL 8|16; JPEG/WebP are 8-bit"}, + "hdr": {"type": "boolean", "description": "HDR output for photos edited in HDR (develop.hdr): JPEG as an ISO 21496-1 gain map JPEG, AVIF as 10-bit and JPEG XL as 16-bit Rec. 2020 PQ, 32-bit TIFF with highlights above 1; other formats and SDR edits export as usual"}, "watermark": {"description": "Text, or {text, vertical (boolean; defaults to false: upright columns right to left), size (text height as a fraction of the short edge, 0.005..0.5; default 0.035), opacity (0..1; default 0.7), anchor (topLeft|top|topRight|left|center|right|bottomLeft|bottom|bottomRight), inset (margin as a fraction of the short edge, 0..0.4; default 0.025), color [r,g,b] (sRGB 0..255), shadow (boolean), image (path of a graphic drawn instead of the text), imageWidth (fraction of the photo's width, 0.01..1; default 0.2)}. Unknown keys and out-of-range sizes are errors"} }), &[], diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index aa287c95..82caa242 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -531,7 +531,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { ui, "Format", "exportFormat", - &[(F::Jpeg, "JPEG"), (F::Png, "PNG"), (F::Tiff, "TIFF"), (F::Webp, "WebP"), (F::Avif, "AVIF"), (F::Dng, "DNG"), (F::Original, "Original files")], + &[(F::Jpeg, "JPEG"), (F::Png, "PNG"), (F::Tiff, "TIFF"), (F::Webp, "WebP"), (F::Avif, "AVIF"), (F::Jxl, "JPEG XL"), (F::Dng, "DNG"), (F::Original, "Original files")], &mut opts.format, ); if !opts.format.is_rendered() { @@ -553,8 +553,8 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { choices(ui, "Bit depth", "exportBitDepth", depths, &mut bd); opts.bit_depth = Some(bd); } - if matches!(opts.format, F::Jpeg | F::Tiff | F::Avif) { - let tip = "Photos edited in HDR: JPEG with an HDR gain map (looks right everywhere, brighter highlights on HDR displays), AVIF as 10-bit Rec. 2020 PQ; TIFF needs 32-bit float"; + if matches!(opts.format, F::Jpeg | F::Tiff | F::Avif | F::Jxl) { + let tip = "Photos edited in HDR: JPEG with an HDR gain map (looks right everywhere, brighter highlights on HDR displays), AVIF as 10-bit and JPEG XL as 16-bit Rec. 2020 PQ; TIFF needs 32-bit float"; ui.checkbox(&mut opts.hdr, "HDR output").on_hover_text(tip); if opts.hdr && !opts.hdr_output() { ui.label(egui::RichText::new("HDR TIFF needs 32-bit float.").color(t.text_dim)); @@ -563,6 +563,8 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { if !rendered { } else if opts.format == F::Avif && opts.hdr { ui.label(egui::RichText::new("Color space: Rec. 2020 PQ (HDR AVIF)").color(t.text_dim)); + } else if opts.format == F::Jxl && opts.hdr { + ui.label(egui::RichText::new("Color space: Rec. 2020 PQ (HDR JPEG XL)").color(t.text_dim)); } else if opts.format == F::Avif { ui.label(egui::RichText::new(crate::i18n::tr("Color space: sRGB (AVIF)")).color(t.text_dim)); } else { @@ -580,13 +582,27 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { ], &mut opts.color_space, ); + if opts.effective_space() != opts.color_space { + let note = "Lossy JPEG XL writes Adobe RGB and ProPhoto as Rec. 2020; choose Lossless to keep them."; + ui.label(egui::RichText::new(crate::i18n::tr(note)).color(t.text_dim)); + } } - if matches!(opts.format, F::Jpeg | F::Avif) { + if opts.format == F::Jxl { + let tip = "Mathematically lossless: every pixel is kept exactly (larger files)"; + ui.checkbox(&mut opts.jxl_lossless, crate::i18n::tr("Lossless")).on_hover_text(crate::i18n::tr(tip)); + } + if matches!(opts.format, F::Jpeg | F::Avif) || (opts.format == F::Jxl && !opts.jxl_lossless) { let mut q = opts.quality as f64; if num(ui, &QUALITY, &mut q) { opts.quality = q as u8; } } + if opts.format == F::Jxl { + use lightcraft_engine::export::JxlEffort as E; + choices(ui, "Effort", "exportJxlEffort", &[(E::Fast, "Fast"), (E::Normal, "Normal")], &mut opts.jxl_effort); + let tip = "Shows a coarse version first and refines it as the file loads"; + ui.checkbox(&mut opts.jxl_progressive, crate::i18n::tr("Progressive")).on_hover_text(crate::i18n::tr(tip)); + } if opts.format == F::Jpeg { let mut k = *limit_kb as f64; if num(ui, &LIMIT_KB, &mut k) { diff --git a/docs/mcp.md b/docs/mcp.md index 81cbc4bd..a32f23d7 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -168,7 +168,7 @@ per-request `_meta` receive `resultType: complete` and list/read cache hints; re | `apply_preset {preset, amount?, ids?}` | Apply a preset (ids from `cmd_presets_list`) | | `crop {id?, rect?, angle?, reset?}` | Normalized crop rect `[x0,y0,x1,y1]` and straighten angle; at least one of `rect`, `angle`, `reset: true` | | `render_photo {id?, size?, format?, path?}` | Render with current settings → **image content** (PNG, or JPEG with `format: "jpeg"`), long edge `size` (default 1024) | -| `export {path \| dir, id? \| ids?, format?, longEdge? \| shortEdge? \| width?/height? \| megapixels? \| percent?, dontEnlarge?, ppi?, quality?, colorSpace?, bitDepth?, …}` | Full-quality render to `.png` / `.jpg` / `.tif` / `.webp` / `.avif`; `format: "original"` copies the file + an XMP sidecar with the edits, `format: "dng"` writes raw photos as DNG with the edits embedded. No size param = 3000 px long edge; `longEdge: 0` = full size (cropped, native resolution); `width` + `height` fit either orientation; `dontEnlarge` defaults to true | +| `export {path \| dir, id? \| ids?, format?, longEdge? \| shortEdge? \| width?/height? \| megapixels? \| percent?, dontEnlarge?, ppi?, quality?, colorSpace?, bitDepth?, …}` | Full-quality render to `.png` / `.jpg` / `.tif` / `.webp` / `.avif` / `.jxl` (`jxlLossless`, `jxlEffort`, `jxlProgressive`); `format: "original"` copies the file + an XMP sidecar with the edits, `format: "dng"` writes raw photos as DNG with the edits embedded. No size param = 3000 px long edge; `longEdge: 0` = full size (cropped, native resolution); `width` + `height` fit either orientation; `dontEnlarge` defaults to true | Tools taking `id` make that photo active first; without it they act on the active photo. diff --git a/docs/parity.md b/docs/parity.md index d0bdc35c..71835a49 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -146,7 +146,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins. | LR-IMP-PROFILES | Import profiles & presets | P1 | ✅ | `cmd:file.importPresets`, `cmd:preset.import`, `cmd:profile.import`, `crates/engine/src/preset_import.rs`, `crates/engine/src/preset_luminar.rs` | presets: .lcpreset, XMP, classic .lrtemplate, photos carrying edits ("DNG presets"), Luminar looks (.lmp, .mplumpack collections; sliders with a counterpart), .zip bundles, folders (folder → group), drag & drop; masks carried over; unmapped settings reported (fields that only describe the preset or photo, e.g. `Cluster`, `SortName`, `SupportsAmount2`, the as-shot white, empty Point Color slots, are not); profiles: .cube 3D LUTs; Adobe profile formats deliberately unsupported | | LR-IMP-LOCAL | Work on files in place | P0 | ✅ | `cmd:library.browse`, `cmd:photo.addToLibrary`, `cmd:library.import` (mode add), `cmd:local.addRoot`, `cmd:local.hide`, `cmd:local.restoreHidden`, `crates/engine/src/cmd/browse.rs`, `crates/ui-egui/src/panels/left.rs` (`local_section`), `crates/ui-egui/src/panels/grid.rs` (`folder_header`) | Local: browse Pictures / Desktop / Downloads / Home or any folder without adding it (breadcrumb, Include subfolders, Add N to My Photos); browsed photos stay out of All Photos, albums and counts; folders picked with Browse Folder… (or Keep in Local / Add to Local on a folder row) stay listed across restarts, and browsing a folder inside a listed one highlights it inside that tree (opened down to it, siblings reachable) instead of adding a root; edits go to XMP sidecars; importing promotes them; right-click a top-level location → Remove from Local hides the shortcut only (nothing on disk or in the catalog changes), saved with the UI state, and “Show N hidden locations” (or Browse Folder… on it) brings it back; a folder is one location however its path is spelled (`/` or `\`, trailing separator, `.`/`..`, drive-letter case; `folder_key` in `crates/catalog/src/query.rs`); browsed photos nobody changed are forgotten once their folder has not been browsed for 30 days (Settings → Performance → Local folders, 0 = never; checked when the library opens, or `cmd:library.forgetLocal` with `dryRun`; files and sidecars stay, browsing again brings them back; see `crates/catalog/src/local.rs` for what counts as a change) | | LR-IMP-SIDECAR-SPLIT | Separate XMP sidecar variants | P2 | ⬜ | `cmd:library.xmpPreferences` | sidecar naming option exists (stem/full), no split sidecars | -| LR-IMP-FORMATS | Supported formats | P0 | 🟡 | `crates/codecs/src/lib.rs`, `crates/raw/src/lib.rs`, `crates/raw/src/jxl.rs` | JPEG, PNG, TIFF (1- to 64-bit integer, 16- to 64-bit float, incl. bilevel and CCITT fax), WebP, JXL, PSD, GIF, BMP; DNG (incl. lossy / Smart Preview DNG and DNG 1.7 JPEG XL tiles/strips: lossless ones decode bit-exactly on synthetic CFA and LinearRaw files, lossy ones are restored to the declared linear primaries without clipping or gamut mapping, checked on one real file with 108 lossy tiles, and any tile that fails to decode makes the file preview-only; DNG 1.7 row/column-interleaved rasters (`RowInterleaveFactor` / `ColumnInterleaveFactor`) are undone before the mosaic is read, so a file from Adobe's converter with a 2×2 interleave no longer decodes as four scaled-down copies in the wrong colours; a dimension the factor does not divide decodes with short trailing fields; verified on 4 real ILCE-6400 JPEG XL DNGs rendering bit-identical to their non-interleaved twins, factors above 2 rest on the plain reading of the tag; a single-chunk JPEG XL preview IFD is offered as the embedded preview; a DNG whose samples pile up at one value well below its stated `WhiteLevel`, as a camera app's 12-bit Bayer DNG that states 4095 and clips at 3567, has those samples raised to the white level on decode, so highlight reconstruction sees the clipped areas instead of white balance tinting them magenta, issue #548, `crates/raw/src/saturation.rs`), CR2 (colour-filter layout from the file's `CR2CFAPattern` tag, issue #85), ARW (pre-2017 bodies: white balance and black level from the encrypted `SR2SubIFD`, issue #148, its `WB_RGGBLevels` being the white balance the camera applied, issue #535; the DSLR-A900's packed 12-bit strip, two pixels per three bytes, read only when the strip is exactly 1.5 bytes per pixel, with its camera-JPEG window centred, checked on one CC0 file (A850 unmeasured); downsized Sony lossless YCbCr 4:2:0 / 4:2:2 tiles decoded to linear RGB, verified on private ILCE-7M4 3:2 & 4:3 examples including 16-bit full-resolution export; Sony DSC-R1 .SR2 decodes, its big-endian 16-bit words read in the order that fits the sample range, checked on the CC0 3221 sample; its crop and white balance live in the SR2SubIFD and are not read yet), NEF (uncompressed, lossless and lossy compressed 12/14-bit, `crates/raw/src/vendor/nefc.rs`), RAF (uncompressed, lossless/lossy compressed Bayer and X-Trans; `crates/raw/src/vendor/rafc.rs`, full sensor verification on 13 bodies, `docs/raf-compression.md`), RW2 / Leica RWL / Panasonic RAW in every raw format (compressed 4 and 6, the prefix-coded strips of format 8 from the GH6 on, packed 2/5/7, the 16-bit words of the 2005–2007 bodies; black levels, in-camera aspect crops and mapped-out defect pixels; 178 CC0 files from 118 bodies checked, `crates/raw/src/vendor/rw2.rs`), PEF, ORF (uncompressed; colour-filter layout from the file's Exif `CFAPattern`; 12.8 bit/px 16-byte blocks of ten 12-bit samples + a zero pad byte, E-300 / E-330 / E-500 verified on CC0 files, E-M5 Mark II / PEN-F high-res files decode smooth but CFA and black unverified), Hasselblad 3FR and FFF (a TIFF without a DNG version tag that describes its CFA IFD, levels, crop and white balance with DNG's tags; `RawFormat::CfaTiff`, read by `dng::decode_as`, RGGB when the file states no `CFAPattern`): uncompressed (X1D, X1D II, X2D, CFV 100C) and Hasselblad's lossless JPEG with scan selection value 8 (every FFF; H3D, H4D and CFV-50c 3FR), `ljpeg::decode_hasselblad`, its rule measured on 14 CC0 files with the ljpeg-explorer tool, Samsung SRW without compression (NX5, NX10, NX11 and the NX20-generation bodies NX20, NX200, NX210, NX1000 and NX1100 as 12-bit packed samples in either bit order, EX1 and WB2000 as 16-bit words; layout, camera-JPEG framing, levels and white balance from the maker note, `crates/raw/src/vendor/srw.rs`; median ΔE vs the camera JPEG 2.2–7.6 on 11 of the 12 files from 10 bodies checked, 10.7 (5.7 registered) on the backlit WB2000 sample). 12-bit NEF black level: maker note `0x003d` is in 14-bit units (D750, D780, D850, D7500, Z 50 checked). Lossy type 2 NEF curves of version `0x44 0x40` (D780, D6, Z 5, Z 6, Z 7, Z 7_2) span a quarter of the sample range, read from the table's own point spacing. CR3 lossless CRX Bayer and version 0x100/0x200 C-RAW decodes to sensor pixels with timed white balance, black/white levels and file crop (the shot's aspect ratio from the maker note's `AspectInfo`); full sensor samples match the reference on M50/R100/R8 (`crates/raw/tests/cr3_corpus.rs`, `docs/cr3.md`). Unsupported coding variants retain metadata and use the embedded JPEG when available; HEVC preview decoding remains unsupported. Missing: CR3 unverified CRX variants, compressed ORF, compressed Samsung SRW (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini: preview only with the reason), NEFs labelled compressed (34713) but stored uncompressed without a `0x0096` table (Z 6 packed 14-bit, D850 12-bit uncompressed), Nikon High Efficiency NEF (HE / HE★ from the Z 8, Z 9, Z 6III and Z f: compression 34713 with a JPEG XS codestream and no `0x0096` table; no public specification, so preview only with the reason "Nikon High Efficiency NEF", issue #193; Lossless compressed NEFs from the same bodies decode), NEF "lossy after split" (preview only: imported with `preview_only` = the decoder's reason, rendered as a rendered JPEG, flagged by a grid badge and loupe/Edit/Info notices, `previewOnly` in `catalog.query`; Reload clears it once the file decodes), TIFF-based raws whose raw data sits in a private block (Phase One / Leaf IIQ, Canon EOS-1D / 1Ds and Kodak DCS TIFFs: IFD0 is only a reduced RGB image next to a much larger declared picture) are recognised from that structure and import as `preview_only` with the reason, instead of opening as a thumbnail-sized ordinary image (`thumbnail_shell` in `crates/raw/src/lib.rs`), Recognised raws LightCraft cannot decode open as `preview_only` with any usable embedded JPEG, also Canon CRW, Minolta MRW and Sigma X3F (containers recognised by magic, preview found by a structural JPEG scan), Kodak KDC (IFD0 without `ImageWidth`), Leaf MOS and Epson ERF (scan), and any decode failure of a recognised raw, not only an unsupported variant; ones with no JPEG still fail with "without an embedded preview", AVIF decode (HEIC decodes only behind the non-default `heif` feature, which the release packages enable: `crates/heif`, heic-rs for the container and HEVC (bit-exact against libde265 on iPhone tiles) plus our own colour conversion, matching libheif within 1 code value on iPhone-style grids (VUI full range, Display P3 ICC, nclx, 10-bit, irot/imir/clap, gain map/depth ignored); 4:2:0/monochrome only, 4:2:2/4:4:4 Canon/Sony HIF refused; same policy as PhotoCraft), palette (indexed-colour) TIFF (refused by the `tiff` crate) | +| LR-IMP-FORMATS | Supported formats | P0 | 🟡 | `crates/codecs/src/lib.rs`, `crates/raw/src/lib.rs`, `crates/raw/src/jxl.rs` | JPEG, PNG, TIFF (1- to 64-bit integer, 16- to 64-bit float, incl. bilevel and CCITT fax), WebP, JXL, PSD, GIF, BMP (lossy wide-gamut JXL decodes to the right colours: jxl-oxide desaturated XYB rendered to P3/Rec. 2020, worked around in `crates/codecs/src/jxl.rs`); DNG (incl. lossy / Smart Preview DNG and DNG 1.7 JPEG XL tiles/strips: lossless ones decode bit-exactly on synthetic CFA and LinearRaw files, lossy ones are restored to the declared linear primaries without clipping or gamut mapping, checked on one real file with 108 lossy tiles, and any tile that fails to decode makes the file preview-only; DNG 1.7 row/column-interleaved rasters (`RowInterleaveFactor` / `ColumnInterleaveFactor`) are undone before the mosaic is read, so a file from Adobe's converter with a 2×2 interleave no longer decodes as four scaled-down copies in the wrong colours; a dimension the factor does not divide decodes with short trailing fields; verified on 4 real ILCE-6400 JPEG XL DNGs rendering bit-identical to their non-interleaved twins, factors above 2 rest on the plain reading of the tag; a single-chunk JPEG XL preview IFD is offered as the embedded preview; a DNG whose samples pile up at one value well below its stated `WhiteLevel`, as a camera app's 12-bit Bayer DNG that states 4095 and clips at 3567, has those samples raised to the white level on decode, so highlight reconstruction sees the clipped areas instead of white balance tinting them magenta, issue #548, `crates/raw/src/saturation.rs`), CR2 (colour-filter layout from the file's `CR2CFAPattern` tag, issue #85), ARW (pre-2017 bodies: white balance and black level from the encrypted `SR2SubIFD`, issue #148, its `WB_RGGBLevels` being the white balance the camera applied, issue #535; the DSLR-A900's packed 12-bit strip, two pixels per three bytes, read only when the strip is exactly 1.5 bytes per pixel, with its camera-JPEG window centred, checked on one CC0 file (A850 unmeasured); downsized Sony lossless YCbCr 4:2:0 / 4:2:2 tiles decoded to linear RGB, verified on private ILCE-7M4 3:2 & 4:3 examples including 16-bit full-resolution export; Sony DSC-R1 .SR2 decodes, its big-endian 16-bit words read in the order that fits the sample range, checked on the CC0 3221 sample; its crop and white balance live in the SR2SubIFD and are not read yet), NEF (uncompressed, lossless and lossy compressed 12/14-bit, `crates/raw/src/vendor/nefc.rs`), RAF (uncompressed, lossless/lossy compressed Bayer and X-Trans; `crates/raw/src/vendor/rafc.rs`, full sensor verification on 13 bodies, `docs/raf-compression.md`), RW2 / Leica RWL / Panasonic RAW in every raw format (compressed 4 and 6, the prefix-coded strips of format 8 from the GH6 on, packed 2/5/7, the 16-bit words of the 2005–2007 bodies; black levels, in-camera aspect crops and mapped-out defect pixels; 178 CC0 files from 118 bodies checked, `crates/raw/src/vendor/rw2.rs`), PEF, ORF (uncompressed; colour-filter layout from the file's Exif `CFAPattern`; 12.8 bit/px 16-byte blocks of ten 12-bit samples + a zero pad byte, E-300 / E-330 / E-500 verified on CC0 files, E-M5 Mark II / PEN-F high-res files decode smooth but CFA and black unverified), Hasselblad 3FR and FFF (a TIFF without a DNG version tag that describes its CFA IFD, levels, crop and white balance with DNG's tags; `RawFormat::CfaTiff`, read by `dng::decode_as`, RGGB when the file states no `CFAPattern`): uncompressed (X1D, X1D II, X2D, CFV 100C) and Hasselblad's lossless JPEG with scan selection value 8 (every FFF; H3D, H4D and CFV-50c 3FR), `ljpeg::decode_hasselblad`, its rule measured on 14 CC0 files with the ljpeg-explorer tool, Samsung SRW without compression (NX5, NX10, NX11 and the NX20-generation bodies NX20, NX200, NX210, NX1000 and NX1100 as 12-bit packed samples in either bit order, EX1 and WB2000 as 16-bit words; layout, camera-JPEG framing, levels and white balance from the maker note, `crates/raw/src/vendor/srw.rs`; median ΔE vs the camera JPEG 2.2–7.6 on 11 of the 12 files from 10 bodies checked, 10.7 (5.7 registered) on the backlit WB2000 sample). 12-bit NEF black level: maker note `0x003d` is in 14-bit units (D750, D780, D850, D7500, Z 50 checked). Lossy type 2 NEF curves of version `0x44 0x40` (D780, D6, Z 5, Z 6, Z 7, Z 7_2) span a quarter of the sample range, read from the table's own point spacing. CR3 lossless CRX Bayer and version 0x100/0x200 C-RAW decodes to sensor pixels with timed white balance, black/white levels and file crop (the shot's aspect ratio from the maker note's `AspectInfo`); full sensor samples match the reference on M50/R100/R8 (`crates/raw/tests/cr3_corpus.rs`, `docs/cr3.md`). Unsupported coding variants retain metadata and use the embedded JPEG when available; HEVC preview decoding remains unsupported. Missing: CR3 unverified CRX variants, compressed ORF, compressed Samsung SRW (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini: preview only with the reason), NEFs labelled compressed (34713) but stored uncompressed without a `0x0096` table (Z 6 packed 14-bit, D850 12-bit uncompressed), Nikon High Efficiency NEF (HE / HE★ from the Z 8, Z 9, Z 6III and Z f: compression 34713 with a JPEG XS codestream and no `0x0096` table; no public specification, so preview only with the reason "Nikon High Efficiency NEF", issue #193; Lossless compressed NEFs from the same bodies decode), NEF "lossy after split" (preview only: imported with `preview_only` = the decoder's reason, rendered as a rendered JPEG, flagged by a grid badge and loupe/Edit/Info notices, `previewOnly` in `catalog.query`; Reload clears it once the file decodes), TIFF-based raws whose raw data sits in a private block (Phase One / Leaf IIQ, Canon EOS-1D / 1Ds and Kodak DCS TIFFs: IFD0 is only a reduced RGB image next to a much larger declared picture) are recognised from that structure and import as `preview_only` with the reason, instead of opening as a thumbnail-sized ordinary image (`thumbnail_shell` in `crates/raw/src/lib.rs`), Recognised raws LightCraft cannot decode open as `preview_only` with any usable embedded JPEG, also Canon CRW, Minolta MRW and Sigma X3F (containers recognised by magic, preview found by a structural JPEG scan), Kodak KDC (IFD0 without `ImageWidth`), Leaf MOS and Epson ERF (scan), and any decode failure of a recognised raw, not only an unsupported variant; ones with no JPEG still fail with "without an embedded preview", AVIF decode (HEIC decodes only behind the non-default `heif` feature, which the release packages enable: `crates/heif`, heic-rs for the container and HEVC (bit-exact against libde265 on iPhone tiles) plus our own colour conversion, matching libheif within 1 code value on iPhone-style grids (VUI full range, Display P3 ICC, nclx, 10-bit, irot/imir/clap, gain map/depth ignored); 4:2:0/monochrome only, 4:2:2/4:4:4 Canon/Sony HIF refused; same policy as PhotoCraft), palette (indexed-colour) TIFF (refused by the `tiff` crate) | | LR-IMP-CAMERA-COVERAGE | Camera coverage, verified per model | P0 | 🟡 | `crates/raw/tests/corpus.rs`, `xtask/src/main.rs` | a supported container is not the same as every camera that writes it decoding correctly: ~55 CC0 corpus files plus 6 exact-sensor CR3 cases (plus ~60 NEFs checked by hand and 178 Panasonic / Leica RW2, RWL and RAW files from 118 bodies, checked against each file's own JPEG) against the >1,000 models Lightroom lists; per-model differences still surface (e.g. the CR2 colour-filter layout differed by model until it was read from the file's own tag, issue #85, checked on 35 CR2 bodies; CR2 black levels were read from border columns light already reaches, left of `SensorInfo`'s border, so CR2s rendered dark with crushed shadows (EOS 6D: 2591 instead of 2047), now measured from the masked columns only, checked on 8 corpus bodies; pre-2017 Sony ARWs opened green until their scrambled white balance / black level were read, issue #148, checked on 16 ARWs from 15 Sony bodies; their white balance now comes from the encrypted `SR2SubIFD`'s `WB_RGGBLevels`, the gains the camera applied, which 14 bodies (DSLR-A450 … A700, NEX-3/5/C3, SLT-A33/A35/A55, ILCE-6001, UMC-R10C) had no other source for and which the maker-note estimate read before missed by up to 100 % on preset shots, issue #535, checked against ExifTool on 100 decoded ARWs from 87 Sony bodies; the encrypted SR2 black level is now read from its directory entry (the DSLR-A450/A500/A550/A700 read 354–975 instead of 512), checked on 126 ARWs from 97 Sony bodies, and downsized lossless M/S ARWs use their measured black level 1024 instead of 512, checked on 12 files from 4 bodies, issue #535; pre-2017 Sony bodies shot in the camera's 16:9 mode (ILCE-7SM2, DSLR-A580) now get the 16:9 crop of their camera JPEG instead of the whole 3:2 frame with padding columns, issue #535; Canon PowerShot and EOS M CR2s whose `ColorData` is stored as bytes opened green until the bytes were read as 16-bit words, 25 files from 25 bodies checked). Compressed RAF sensor arrays verified on 13 Fujifilm bodies (`docs/raf-compression.md`). Sony ILCE-7CR (61 MP): lossless compressed L and M and compressed ARW2 samples of one scene in the corpus (black level, default crop, as-shot gains, lossless vs compressed mosaic agreement, fitted camera look), and all nine public codings (also uncompressed, small and APS-C crop) decode and fit. Grow the CC0 corpus per model and test each sample decodes with plausible colour. Nikon CropArea and untagged-preview colour-space regression tests; private Z 8 crop verified. | | LR-IMP-CULL-AT-IMPORT | Culling analysis at import | P2 | 🟡 | `cmd:photo.analyze` | run Assisted Culling on the imported photos (they're selected after an import); not automatic | | LR-IMP-MOVE | Move on import [Classic] | P1 | ✅ | `cmd:library.import` (`mode` move), `crates/engine/src/import_move.rs`, `crates/engine/src/tests_import_move.rs`, `crates/ui-egui/src/import.rs` | Transfer ▸ Move with Copy's destination, folders (day / month / one folder / custom template), rename template and example destination; XMP sidecars (both namings) move along; each source is removed only after its destination is written (hard link on the same volume, else copied, synced and compared byte for byte) and its catalog record is saved; failed, duplicate, unchecked files keep their sources, taken names get -1, -2…, never overwritten; files already in the destination/library are added in place; sources that can't be removed (read-only card) are kept and reported (`kept`). Undo removes the photos from the library but leaves the moved files at the destination. No Copy as DNG while moving | @@ -430,7 +430,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins. | LR-HDR-SDRPREVIEW | SDR preview of HDR | P2 | 🟡 | `ctl:hdr.sdr*` | SDR rendition sliders (brightness, contrast, highlights, shadows, whites, clarity) used by every SDR view and export; no separate SDR-preview toggle until HDR display lands | | LR-HDR-VISUALIZE | Visualize HDR range | P2 | ✅ | `cmd:view.visualizeHdr`, `crates/pipeline/src/visualize.rs` (`hdr_range`) | grey below SDR white, four colour bands by stops above it | | LR-HDR-LIMIT | HDR headroom limit | P2 | ✅ | `ctl:hdr.maxEv`, `cmd:develop.hdr` | 0 to 5 stops | -| LR-HDR-EXPORT | HDR export | P2 | 🟡 | `cmd:app.export`, `crates/codecs/src/gainmap.rs`, `crates/engine/src/export.rs` (`encode_gain_map_jpeg`) | `hdr` export option: ISO 21496-1 gain map JPEG (plus Adobe hdrgm and Apple XMP), 10-bit Rec. 2020 PQ AVIF, 32-bit float TIFF; no HLG yet; no HEIC (no permissive HEVC encoder) | +| LR-HDR-EXPORT | HDR export | P2 | 🟡 | `cmd:app.export`, `crates/codecs/src/gainmap.rs`, `crates/engine/src/export.rs` (`encode_gain_map_jpeg`) | `hdr` export option: ISO 21496-1 gain map JPEG (plus Adobe hdrgm and Apple XMP), 10-bit Rec. 2020 PQ AVIF, 16-bit Rec. 2020 PQ JPEG XL, 32-bit float TIFF; no HLG yet; no HEIC (no permissive HEVC encoder) | ## R. Video (VID) @@ -448,13 +448,13 @@ under the UI command that wraps it (a no-op error); the UI command now wins. | Id | Feature | Tier | Status | Evidence | Missing / notes | |---|---|---|---|---|---| | LR-EXP-DIALOG | Export dialog | P0 | ✅ | `cmd:dialog.export`, `cmd:app.export` (`preset`), `cmd:export.presets`, `cmd:export.savePreset`, `cmd:export.deletePreset`, `crates/ui-egui/src/panels/dialogs.rs`, `crates/engine/src/export.rs` | batch export; built-in presets (JPEG Small 2048 px / Large full size, Original + Settings, DNG) and saved user presets (library prefs) load into the dialog; deleting a user preset is command-only | -| LR-EXP-TYPE | File types | P0 | ✅ | `cmd:app.export` (`format`), `crates/engine/src/export.rs` (`ExportFormat`), `crates/engine/src/tests_export.rs` | JPEG, PNG, TIFF, WebP, AVIF, DNG (raw photos: lossless re-encode with the edits in the embedded XMP), Original (+ XMP sidecar). No JXL encoder; non-raw → DNG not supported | +| LR-EXP-TYPE | File types | P0 | ✅ | `cmd:app.export` (`format`), `crates/engine/src/export.rs` (`ExportFormat`), `crates/engine/src/tests_export.rs` | JPEG, PNG, TIFF, WebP, AVIF, JPEG XL (issue #247: `jixel`, native only; lossy with quality or lossless, 8/16-bit, effort fast/normal (jixel's slowest search left out: jxl-oxide misdecodes its files), progressive, EXIF/XMP boxes; lossy files in Adobe RGB / ProPhoto are written as Rec. 2020, lossless keep any space via ICC; `crates/codecs/src/jxl_encode.rs`), DNG (raw photos: lossless re-encode with the edits in the embedded XMP), Original (+ XMP sidecar). Non-raw → DNG not supported; no JPEG XL export in the web build | | LR-EXP-DIM | Output size | P0 | ✅ | `cmd:app.export` (`longEdge`, `shortEdge`, `width`, `height`, `megapixels`, `percent`, `dontEnlarge`, `ppi`), `crates/engine/src/export.rs` (`Resize`), `crates/ui-egui/src/panels/dialogs.rs` | full size = the cropped native size (no longer upscaled); W × H fits either orientation; ppi written to JFIF / pHYs / TIFF tags | | LR-EXP-QUALITY | JPEG quality | P0 | ✅ | `cmd:app.export` (`quality`, `limitKb`) | | -| LR-EXP-BITDEPTH | Bit depth | P1 | ✅ | `cmd:app.export` (`bitDepth`), `crates/pipeline/src/output.rs` (`OutputDepth`), `crates/engine/src/export.rs` | 16-bit PNG/TIFF rendered at 16 bits (TIFF defaults to 16), 32-bit float linear TIFF with a linear profile, 10-bit AVIF; high-bit-depth renders run on the CPU | +| LR-EXP-BITDEPTH | Bit depth | P1 | ✅ | `cmd:app.export` (`bitDepth`), `crates/pipeline/src/output.rs` (`OutputDepth`), `crates/engine/src/export.rs` | 16-bit PNG/TIFF/JPEG XL rendered at 16 bits (TIFF defaults to 16), 32-bit float linear TIFF with a linear profile, 10-bit AVIF; high-bit-depth renders run on the CPU | | LR-EXP-COMPRESSION | TIFF compression | P1 | ✅ | `cmd:app.export` (`tiffCompression`: none / lzw / zip) | | -| LR-EXP-COLORSPACE | Output colour space | P0 | ✅ | `cmd:app.export` (`colorSpace`), `crates/pipeline/src/output.rs`, `crates/engine/src/export.rs` | sRGB, Display P3, Adobe RGB (1998) compatible, ProPhoto RGB, Rec. 2020: rendered from the working space with gamut mapping into the target gamut (CPU + GPU), own ICC profile embedded; AVIF stays sRGB (muxer has no ICC) | -| LR-EXP-HDR | HDR output | P2 | 🟡 | `cmd:app.export`, `crates/ui-egui/src/panels/dialogs.rs` | HDR output checkbox in the export dialog: gain map JPEG, PQ AVIF, float TIFF; no HDR presets yet | +| LR-EXP-COLORSPACE | Output colour space | P0 | ✅ | `cmd:app.export` (`colorSpace`), `crates/pipeline/src/output.rs`, `crates/engine/src/export.rs` | sRGB, Display P3, Adobe RGB (1998) compatible, ProPhoto RGB, Rec. 2020: rendered from the working space with gamut mapping into the target gamut (CPU + GPU), own ICC profile embedded; AVIF stays sRGB (muxer has no ICC); lossy JPEG XL declares sRGB / P3 / Rec. 2020 (Adobe RGB and ProPhoto written as Rec. 2020), lossless JPEG XL embeds the profile | +| LR-EXP-HDR | HDR output | P2 | 🟡 | `cmd:app.export`, `crates/ui-egui/src/panels/dialogs.rs` | HDR output checkbox in the export dialog: gain map JPEG, PQ AVIF, PQ JPEG XL, float TIFF; no HDR presets yet | | LR-EXP-SHARPEN | Output sharpening | P1 | ✅ | `cmd:app.export` (`sharpen`, `sharpenAmount`) | | | LR-EXP-METADATA | Metadata policy | P1 | ✅ | `cmd:app.export` (`metadata`, `removeLocation`) | | | LR-EXP-WATERMARK | Watermark | P1 | ✅ | `cmd:app.export` (`watermark`: text, or `{text, vertical, size, opacity, anchor, inset, color, shadow, image, imageWidth}`), `crates/engine/src/export.rs` (`Watermark`, `WATERMARK_PARAMS`) | text (`size` = height as a fraction of the short edge, 0.005..0.5, default 0.035; `color` sRGB `[r,g,b]`; `shadow`; Japanese vertical glyph forms and origins, tested by `biz_ud_vertical_watermarks_place_punctuation_at_the_top_right`; extended grapheme cells keep decomposed dakuten/handakuten and Latin accents together, tested by `vertical_watermarks_keep_combining_marks_in_one_cell` and `upright_watermark_cell_retains_multiple_glyphs` (see `docs/vertical-watermarks.md`)) or a graphic with transparency (`image` path, `imageWidth` = fraction of the photo's width, 0.01..1, converted to the output colour space); `anchor`, `inset` (fraction of the short edge, 0..0.4), `opacity` 0..1. Unknown keys and out-of-range values are errors (issue #183) | diff --git a/docs/web.md b/docs/web.md index 2e26f749..2a5b6cdb 100644 --- a/docs/web.md +++ b/docs/web.md @@ -168,6 +168,8 @@ are the same in every version, so serve them with `Cache-Control: no-cache` (not - **Preset files** (import/export `.json`) aren't wired to browser pickers yet. - **Control channel / MCP.** These are desktop-only, because they need a TCP socket. - **AVIF export** is untested in the browser; it's the one encoder that may not be wasm-safe. +- **JPEG XL export** is native only: the encoder (`jixel`) starts threads of its own, which wasm32 has not got; the + web build reports "not available in this build". JPEG XL import works. ## Measuring diff --git a/docs/whats-new.md b/docs/whats-new.md index fb1fd316..d12ffda4 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -2,6 +2,15 @@ ## October 2026 +### JPEG XL export +- Export as JPEG XL (issue #247): Format ▸ JPEG XL in the Export dialog, `format: "jxl"` for agents and the CLI + (`render in.raw -o out.jxl`). Quality as for JPEG, or Lossless; Effort (Fast / Normal) trades time for a + smaller file; Progressive files show a coarse version first. 8- or 16-bit, sRGB, Display P3 or Rec. 2020 (lossless + files also Adobe RGB and ProPhoto), metadata as for JPEG. With HDR output, photos edited in HDR are written as + 16-bit Rec. 2020 PQ. Not in the web version yet. +- Lossy JPEG XL files in Display P3 or Rec. 2020 now open in their real colours; saturated colours used to come in + noticeably duller. + ### Activity stack - Long-running tasks show in one place, top left under the top bar, as in Lightroom Classic (issue #345): imports and folder scans, exports (and the contact sheet PDF), Synchronize Folder, Build / Smart Previews, Lightroom catalog import, Photo Merge, model downloads, the face scan, From 22a836316d1f0588bfb30aa2ee594c1752ea0c25 Mon Sep 17 00:00:00 2001 From: Wondarkus Date: Sun, 11 Oct 2026 00:55:44 +0400 Subject: [PATCH 09/24] ru: sync with uk (810 new strings) --- crates/ui-egui/locales/ru.json | 1082 +++++++++++++++++++++++--------- 1 file changed, 780 insertions(+), 302 deletions(-) diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index bf997474..bc026d80 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1,15 +1,57 @@ { + " (library default)": " (по умолчанию для библиотеки)", + " (inverted)": " (инвертировано)", + " · best of its burst": " · лучшее в серии", + " · in a burst": " · в серии", + "\"saved in memory but not written to disk\", the top bar shows a warning, and LightCraft keeps retrying until the": "«сохранено в памяти, но не записано на диск», в верхней панели появляется предупреждение, а LightCraft повторяет попытки, пока", + "#106): a DNG is checked against the raw data, written to a temp file, synced and read back before it gets its": "#106): DNG проверяется по данным RAW, записывается во временный файл, синхронизируется и перечитывается, прежде чем получит", + "% of all commits": "% всех коммитов", + "& Containing": "и родительские", + "(`gpuReason`, `gpuFallback`), Help ▸ System Info and Settings ▸ Performance say why the GPU isn't": "(`gpuReason`, `gpuFallback`), Справка → Сведения о системе и Настройки → Производительность объясняют, почему GPU не", + "(e.g. D750, D780, D850, D7500, Z 50) no longer render nearly black or with crushed shadows: their black level was": "(например, D750, D780, D850, D7500, Z 50) больше не отображаются почти чёрными или с задавленными тенями: их уровень чёрного", + "(issue #94): an HTTP request from a web page can no longer carry a command in its body. Lines are capped at": "(проблема #94): HTTP-запрос с веб-страницы больше не может содержать команду в теле. Длина строк ограничена", "+ Group": "+ Группа", "+ Rule": "+ Условие", + ", picked": ", выбрано", + ", rejected": ", отклонено", "0: nothing is rejected.": "0: ничего не отклоняется.", + "1 day": "1 день", + "123 on studio-mac)\") instead of letting both write and silently drop each other's edits. A crash never leaves the": "123 на studio-mac)»), вместо того чтобы позволить обоим писать и незаметно терять правки друг друга. Сбой никогда не оставляет", + "1620 × 1080 preview, and import with their metadata: capture time, camera, lens, exposure, GPS and XMP. Their raw": "просмотр 1620 × 1080 и импортируются с метаданными: время съёмки, камера, объектив, экспозиция, GPS и XMP. Их данные RAW", + "2026-04 or 2026-04-12": "2026-04 или 2026-04-12", + "23 new built-in presets: Portrait, Landscape, Urban, Food, Seasons, Vintage and B&W toners.": "23 новых встроенных пресета: портрет, пейзаж, город, еда, времена года, винтаж и тонирование Ч/Б.", + "4 MiB and connections at 16.": "4 МиБ, а число соединений — 16.", + "630 drivers): on Windows the window and GPU rendering use DirectX 12 only and never load the Vulkan driver unless": "630): в Windows окно и рендеринг на GPU используют только DirectX 12 и не загружают драйвер Vulkan, пока вы", + "= exactly": "= точно", + "A blank template keeps the original names.": "Пустой шаблон сохраняет исходные имена.", + "A change that can't be saved to disk (full or unplugged drive) is no longer silent: the command reports": "Изменение, которое не удаётся сохранить на диск (диск заполнен или отключён), больше не остаётся незамеченным: команда сообщает", "A filter is hiding this view's photos: change it, or clear it (View → Clear Filters)": "Фильтр скрывает фото в этом представлении: измените его или сбросьте (Вид → Сбросить фильтры)", + "A folder is already being synchronized": "Папка уже синхронизируется", + "A folder is being synchronized": "Папка синхронизируется", + "A keyword set needs a name": "Набору ключевых слов нужно название", + "A library is open in one program at a time (issue #99): opening a library that LightCraft or `lightcraft-cli` already": "Библиотека открыта только в одной программе одновременно (проблема #99): открытие библиотеки, которую LightCraft или `lightcraft-cli` уже", "A one-time download of about": "Однократная загрузка около", + "A save that fails part-way (a full disk, a network share that drops) no longer looks like a damaged catalog": "Неудачное частичное сохранение (диск заполнен, обрыв сетевого соединения) больше не выглядит как повреждённый каталог", + "A sleeping NAS, a dropped network share or a USB drive spinning up no longer freezes the window (issue #104):": "Засыпающий NAS, оборванное сетевое соединение или раскручивающийся USB-диск больше не замораживают окно (проблема #104):", + "AI Denoise": "ИИ-шумоподавление", + "AI Denoise Model": "Модель ИИ-шумоподавления", + "AI Denoise is enabled on the requesting photo": "ИИ-шумоподавление включено для запросившего фото", + "AI Denoise is off: press Use on a model.": "ИИ-шумоподавление выключено: нажмите «Использовать» у модели.", + "AI Denoise needs a model. Install one in Settings.": "Для ИИ-шумоподавления нужна модель. Установите её в настройках.", + "AI Mask Model Status": "Состояние модели ИИ-масок", + "AI masks with SAM 3 (Object and Describe in the Masking panel): click an object to select it (⌥-click leaves a": "ИИ-маски на основе SAM 3 («Объект» и «Описать» на панели масок): щёлкните объект, чтобы выделить его (⌥+щелчок исключает", + "About": "О программе", "About LightCraft": "О программе LightCraft", + "Accept & Download": "Принять и загрузить", + "Active": "Активно", "Activity": "Активность", "Actual size": "Реальный размер", "Add": "Добавить", "Add Catchlight": "Добавить блик в глазах", + "Add Face Model…": "Добавить модель лиц…", "Add Folder to Local": "Добавить папку в «Локальное»", + "Add Mask": "Добавить маску", + "Add Object Click": "Добавить объект щелчком", "Add Point Color Sample": "Добавить точечный образец цвета", "Add Red Eye Correction": "Добавить коррекцию красных глаз", "Add Remove Spot": "Добавить удаление пятна", @@ -21,7 +63,9 @@ "Add number": "Добавить номер", "Add to": "Добавить в", "Add to Album": "Добавить в альбом", + "Add to All Selected Photos": "Добавить ко всем выбранным фото", "Add to Favorites": "Добавить в избранное", + "Add to Local": "Добавить в «Локальное»", "Add to My Photos": "Добавить в «Мои фото»", "Add to Selected Photos": "Добавить к выбранным фото", "Add to Target Album": "Добавить в целевой альбом", @@ -29,13 +73,14 @@ "Add/Subtract/Intersect Mask": "Добавить/вычесть/пересечь маску", "Added to photos you import that don't already have it.": "Добавляется к импортируемым фото, у которых этого ещё нет.", "Adding": "Добавление", - "Adding Lightroom photos…": "Добавление фото из Lightroom…", + "Adding Lightroom photos…": "Добавление фото Lightroom…", "Advanced search…": "Расширенный поиск…", "After": "После", "After 30 days": "Через 30 дней", "After 7 days": "Через 7 дней", "After 90 days": "Через 90 дней", "After a year": "Через год", + "After installation, AI Denoise will turn on for the requesting photo.": "После установки ИИ-шумоподавление включится для запросившего фото.", "Album": "Альбом", "Albums": "Альбомы", "All": "Все", @@ -43,18 +88,28 @@ "All Metadata…": "Все метаданные…", "All Photos": "Все фото", "All rights reserved": "Все права защищены", + "All the photos imported from this disk are included.": "Включаются все фото, импортированные с этого диска.", "All types": "Все типы", + "Already installed. Remove it before installing a replacement.": "Уже установлено. Удалите перед установкой замены.", "Alt Text": "Альтернативный текст", "Always": "Всегда", "Amount": "Количество", + "Analyzing the photo for AI masks…": "Анализ фото для ИИ-масок…", + "Angle": "Угол", + "Any Searchable Text": "Любой текст для поиска", + "Any camera": "Любая камера", + "Any keyword": "Любое ключевое слово", + "Any lens": "Любой объектив", "Aperture": "Диафрагма", "Application": "Приложение", + "Apply": "Применить", "Apply Color Label Set": "Применить набор цветовых меток", "Apply Filter Preset": "Применить пресет фильтра", "Apply Metadata Preset": "Применить пресет метаданных", "Apply Point Curve Preset": "Применить пресет точечной кривой", "Apply Preset": "Применить пресет", "Apply Settings JSON": "Применить JSON настроек", + "Apply the local tone mapping stored in the raw (Apple ProRAW), as the camera renders it": "Применить локальное тональное отображение, сохранённое в RAW (Apple ProRAW), как его отображает камера", "Approved": "Одобрено", "Aqua": "Голубой", "Aqua Hue": "Тон голубого", @@ -82,16 +137,21 @@ "Auto Import Now": "Выполнить автоимпорт сейчас", "Auto Import Settings": "Настройки автоимпорта", "Auto Mask": "Автомаска", + "Auto Select": "Автоматическое выделение", "Auto Settings": "Автонастройки", "Auto Straighten": "Автовыравнивание горизонта", "Auto Sync": "Автосинхронизация", + "Auto Sync: edits apply to every selected photo. Auto B&W mix. Automatic versions.": "Автосинхронизация: правки применяются ко всем выбранным фото. Авто-микс Ч/Б. Автоматические версии.", + "Auto settings applied": "Автонастройки применены", "Auto-Stack by Capture Time": "Автостопка по времени съёмки", "Auto-Stack by Capture Time…": "Автостопка по времени съёмки…", "Auto-Tag Photos from Tracklog": "Автопривязка фото по треку", "Auto-Tag from Tracklog": "Автопривязка по треку", + "Auto-Tag from Tracklog: GPS locations for your photos from a GPX track log, matched by capture time.": "Автопривязка по треку: GPS-координаты для ваших фото из GPX-трека по времени съёмки.", "Auto-Tag from Tracklog…": "Автопривязка по треку…", "Automatic": "Автоматически", - "Automatic renders the Detail view at the size it is shown, up to the photo's own pixels. Choose a size to cap it: smaller is faster.": "«Автоматически» отрисовывает детальный просмотр в показанном размере, но не больше собственных пикселей фото. Выберите размер, чтобы ограничить его: меньше — быстрее.", + "Automatic renders the Detail view at the size it is shown, up to the photo's own pixels. Choose a size to cap it: smaller is faster.": "Автоматический режим рендерит «Детализацию» в отображаемом размере, вплоть до собственных пикселей фото. Выберите размер, чтобы ограничить: меньше — быстрее.", + "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "Автоматический режим использует видеокарту, когда она быстрее процессора: время обоих измеряется на этом компьютере при первой обработке фото.", "Automatically Write Changes into XMP": "Автоматически записывать изменения в XMP", "Automatically write changes into XMP sidecars": "Автоматически записывать изменения в XMP-файлы", "Autumn Gold": "Осеннее золото", @@ -103,6 +163,7 @@ "Back to Edit": "Вернуться к редактированию", "Back to Edit (E)": "Вернуться к редактированию (E)", "Back to Grid": "Вернуться к сетке", + "Back to this person": "Вернуться к этому человеку", "Background": "Фон", "Balance": "Баланс", "Basic": "Основные", @@ -113,6 +174,9 @@ "Before/After Split Top/Bottom": "Разделение «до/после» сверху/снизу", "Before/After Top/Bottom": "До/после сверху/снизу", "Begin Interaction": "Начать взаимодействие", + "Best of Similar Shots": "Лучшие из похожих кадров", + "Binary assets added": "Добавленные бинарные ресурсы", + "Binary assets removed": "Удалённые бинарные ресурсы", "Bit depth": "Разрядность", "Black & White": "Чёрно-белое", "Blacks": "Чёрные", @@ -132,38 +196,70 @@ "Boundary Warp": "Деформация границ", "Bright & Airy": "Светлый и воздушный", "Bring All to Front": "Показать все окна", + "Bring the library up to date with this folder and the folders inside it:": "Обновить библиотеку по этой папке и вложенным папкам:", "Browse Folder": "Выбрать папку", "Browse Folder…": "Выбрать папку…", "Browse Profiles": "Обзор профилей", + "Browser version (experimental), keeping a library safe (issue #107): File ▸ Back Up Library… downloads the catalog": "Браузерная версия (экспериментальная), защита библиотеки (проблема #107): Файл → Резервная копия библиотеки… скачивает каталог", "Browse…": "Обзор…", "Brush": "Кисть", "Brush Settings": "Настройки кисти", "Brush Stroke": "Мазок кисти", "Build Previews": "Создать просмотры", "Build Smart Previews": "Создать смарт-просмотры", + "Building previews": "Создание просмотров", + "Building smart previews": "Создание смарт-просмотров", "By Date": "По дате", "By day (YYYY/YYYY-MM-DD)": "По дням (ГГГГ/ГГГГ-ММ-ДД)", "By month (YYYY/YYYY-MM)": "По месяцам (ГГГГ/ГГГГ-ММ)", + "CPU threads": "Потоки CPU", + "Cache": "Кэш", "Calibration": "Калибровка", "Camera": "Камера", + "Camera Info": "Данные камеры", + "Camera local tone mapping": "Локальное тональное отображение камеры", + "Camera make and model": "Производитель и модель камеры", "Cameras and Cards": "Камеры и карты", "Cancel": "Отмена", + "Cancel AI Mask Model Download": "Отменить загрузку модели ИИ-масок", + "Cancel Denoise": "Отменить шумоподавление", + "Cancel Denoise Model Download": "Отменить загрузку модели шумоподавления", "Cancel Download": "Отменить загрузку", + "Cancel Face Model Download": "Отменить загрузку модели лиц", "Cancel Interaction": "Отменить взаимодействие", + "Cancel Pending Photo Action": "Отменить отложенное действие с фото", "Cancel Preview Build": "Отменить создание просмотров", + "Cancel pending action": "Отменить отложенное действие", "Candidate": "Кандидат", + "Canon CR2 photos from the EOS 7D, 50D, 60D, 550D, 600D, 1200D, 1300D, 5D Mark II and 1D Mark IV (and other": "Фото Canon CR2 с EOS 7D, 50D, 60D, 550D, 600D, 1200D, 1300D, 5D Mark II и 1D Mark IV (и других", + "Canon CR3 files show their full-size embedded JPEG (e.g. 6960 × 4640 on an EOS R6 Mark III) instead of the": "Файлы Canon CR3 показывают полноразмерный встроенный JPEG (например, 6960 × 4640 на EOS R6 Mark III) вместо", "Caption": "Подпись", "Capture Date": "Дата съёмки", + "Capture date and time in your own format (directives below)": "Дата и время съёмки в вашем формате (директивы ниже)", + "Capture date as YYYYMMDD": "Дата съёмки в формате YYYYMMDD", "Captured": "Снято", + "Captured from": "Снято с", + "Captured until": "Снято по", "Catalog Statistics": "Статистика каталога", + "Catchlight": "Блик в глазах", + "Change": "Изменить", + "Change Component Mode": "Изменить режим компонента", + "Characters not allowed in file names (/ \\ : * ? \" < > |) become -; an existing name gets -1, -2….": "Символы, недопустимые в именах файлов (/ \\ : * ? \" < > |), заменяются на -; к существующему имени добавляется -1, -2….", "Check All": "Отметить все", "Check Output Path": "Проверить путь вывода", - "Check the files of this type": "Отметить файлы этого типа", + "Check the files of this type": "Проверить файлы этого типа", + "Checking and installing the model…": "Проверка и установка модели…", + "Checking names…": "Проверка имён…", + "Checking…": "Проверка…", "Choose Another Library…": "Выбрать другую библиотеку…", + "Choose Denoise Model": "Выбрать модель шумоподавления", "Choose Edit Settings to Copy": "Выбрать настройки для копирования", "Choose Edit Settings to Copy…": "Выбрать настройки для копирования…", + "Choose Face Recognition Model": "Выбрать модель распознавания лиц", "Choose Folder…": "Выбрать папку…", + "Choose a model below (Use) to start.": "Выберите модель ниже («Использовать»), чтобы начать.", "Choose a photo in the grid or filmstrip": "Выберите фото в сетке или на ленте", + "Choose an album…": "Выберите альбом…", "Choose…": "Выбрать…", "Cinematic": "Кинематографичный", "City": "Город", @@ -171,6 +267,7 @@ "Clean Studio": "Чистая студия", "Clear": "Очистить", "Clear Cache": "Очистить кэш", + "Clear Denoise Cache": "Очистить кэш шумоподавления", "Clear Filters": "Сбросить фильтры", "Clear Guides": "Убрать направляющие", "Clear History": "Очистить историю", @@ -178,16 +275,21 @@ "Clear Quick Collection": "Очистить быструю коллекцию", "Clear Reference": "Убрать эталон", "Clear all": "Очистить всё", + "Clear cache": "Очистить кэш", "Clear form": "Очистить форму", - "Clear rating": "Снять оценку", - "Clear rating and advance": "Снять оценку и перейти дальше", + "Clear rating": "Сбросить оценку", + "Clear rating and advance": "Сбросить оценку и перейти дальше", "Cleared History": "История очищена", "Click Zoom Ratio": "Масштаб по щелчку", - "Click a shortcut, then press the new keys (Esc cancels).": "Щёлкните сочетание и нажмите новые клавиши (Esc — отмена).", + "Click a shortcut, then press the new keys (Esc cancels).": "Щёлкните сочетание клавиш, затем нажмите новые (Esc отменяет).", "Click a tag to insert it. Examples are for IMG_0042.CR3 taken 14 Jan 2026, 05:58:48.": "Щёлкните по тегу, чтобы вставить его. Примеры приведены для IMG_0042.CR3, снятого 14 января 2026 в 05:58:48.", "Click photos in the grid to toggle the keyword; Esc stops": "Щёлкайте по фото в сетке, чтобы переключать ключевое слово; Esc — остановить", + "Click the object to select it · ⌥-click leaves a part out": "Щёлкните объект, чтобы выделить его · ⌥+щелчок исключает часть", "Click the photo to pick a colour · ⇧-click adds more": "Щёлкните по фото, чтобы выбрать цвет · ⇧+щелчок добавляет ещё", "Click the photo to pick a colour; ⇧-click adds more (up to 5)": "Щёлкните по фото, чтобы выбрать цвет; ⇧+щелчок добавляет ещё (до 5)", + "Click to clear these filters": "Щёлкните, чтобы сбросить эти фильтры", + "Click to restore · double-click to rename": "Щёлкните, чтобы восстановить · двойной щелчок переименовывает", + "Click to type a value": "Щёлкните, чтобы ввести значение", "Clone": "Клонировать", "Close": "Закрыть", "Close Panel": "Закрыть панель", @@ -195,42 +297,57 @@ "Close all": "Закрыть все", "Close file": "Закрыть файл", "Cloudy": "Облачно", + "Cmd+Shift+R, keeps the photo and clears its edits.)": "Cmd+Shift+R, сохраняет фото и сбрасывает его правки.)", + "Collapse": "Свернуть", "Collapse All Stacks": "Свернуть все стопки", + "Collapse Stack": "Свернуть стопку", "Color": "Цвет", "Color Grading": "Цветокоррекция", + "Color Label": "Цветовая метка", "Color Label Names": "Названия цветовых меток", "Color Label Sets": "Наборы цветовых меток", "Color Mixer": "Цветовой микшер", "Color Noise Reduction": "Подавление цветового шума", "Color Priority": "Приоритет цвета", "Color Range": "Цветовой диапазон", - "Color label cleared": "Цветовая метка снята", + "Color label cleared": "Цветовая метка сброшена", "Color space": "Цветовое пространство", "Color space: sRGB (AVIF)": "Цветовое пространство: sRGB (AVIF)", + "Colors": "Цвета", + "Colour-label filter with several labels at once; expandable folder tree in Local.": "Фильтр по цветовым меткам с несколькими метками сразу; раскрывающееся дерево папок в «Локальном».", + "Colour-range masks: click the photo to sample. Luminance ranges: range bar, smoothness, luminance map.": "Маски по цветовому диапазону: щёлкните по фото, чтобы взять образец. Диапазоны яркости: полоса диапазона, плавность, карта яркости.", "Combine files…": "Объединить файлы…", "Command Journal": "Журнал команд", "Comments panel": "Панель комментариев", + "Commits": "Коммиты", + "Company": "Компания", "Compare": "Сравнение", "Compare (Shift+C)": "Сравнение (Shift+C)", "Compare Before and After": "Сравнить «до» и «после»", "Compare needs at least two photos": "Для сравнения нужно минимум два фото", "Component options": "Параметры компонента", + "Compressed Nikon NEFs (lossless and lossy compressed, 12- and 14-bit — e.g. D3200, D5100, D7000, D750, D850, Z 50)": "Сжатые Nikon NEF (со сжатием без потерь и с потерями, 12- и 14-битные — например, D3200, D5100, D7000, D750, D850, Z 50)", "Compression": "Сжатие", "Concrete Cool": "Холодный бетон", "Confirm before deleting": "Подтверждать удаление", "Confirm before moving photos to Recently Deleted": "Подтверждать перемещение фото в «Недавно удалённые»", "Constrain Crop": "Ограничить кадрирование", + "Continue": "Продолжить", "Continue Without Saving": "Продолжить без сохранения", "Contrast": "Контрастность", + "Contributors": "Участники", "Convert to DNG": "Преобразовать в DNG", + "Convert to DNG, Copy as DNG, Photo Merge and smart previews no longer write straight to the final file (issue": "Конвертация в DNG, копирование как DNG, слияние фото и смарт-просмотры больше не пишут сразу в конечный файл (проблема", "Cool Fade": "Холодное выцветание", "Cool Morning": "Холодное утро", "Copy": "Копировать", "Copy After's Settings to Before": "Скопировать настройки «после» в «до»", + "Copy Before to After": "Копировать «до» в «после»", "Copy Before's Settings to After": "Скопировать настройки «до» в «после»", "Copy Edit Settings": "Копировать правки", "Copy History Step Settings to Before": "Скопировать настройки шага истории в «до»", "Copy Metadata": "Копировать метаданные", + "Copy Name": "Название копии", "Copy as DNG": "Копировать как DNG", "Copy into the library (else use the files where they are)": "Копировать в библиотеку (иначе файлы используются там, где лежат)", "Copy pages": "Копировать страницы", @@ -241,11 +358,15 @@ "Copyright": "Авторские права", "Copyright Info URL": "URL сведений об авторских правах", "Copyright Status": "Статус авторских прав", + "Copyright status, rights usage terms and copyright info URL in Info, metadata presets and exports.": "Статус авторских прав, условия использования прав и URL сведений об авторских правах в «Информации», пресетах метаданных и экспорте.", + "Copyrighted": "Защищено авторским правом", "Country": "Страна", + "Create": "Создать", "Create Album": "Создать альбом", "Create Album…": "Создать альбом…", "Create Folder": "Создать папку", "Create Folder…": "Создать папку…", + "Create Keyword Tag": "Создать ключевое слово", "Create New Mask": "Создать новую маску", "Create PDF from clipboard": "Создать PDF из буфера обмена", "Create PDF from file…": "Создать PDF из файла…", @@ -261,16 +382,20 @@ "Create Version from Step": "Создать версию из шага", "Create Virtual Copy": "Создать виртуальную копию", "Creator": "Автор", + "Creator metadata": "Метаданные автора", "Crisp Landscape": "Чёткий пейзаж", "Crisp Vista": "Чёткая панорама", "Crop": "Кадрирование", "Crop & Rotate": "Кадрирование и поворот", "Crop & Rotate (C)": "Кадрирование и поворот (C)", "Crop Aspect": "Пропорции кадра", + "Crop, Rotate, Geometry": "Кадрирование, поворот, геометрия", + "Cropped": "Кадрировано", "Cross Process": "Кросс-процесс", "Culling": "Отбор", "Curve": "Кривая", "Custom": "Свои", + "Custom template…": "Свой шаблон…", "Custom…": "Свои…", "Cut": "Вырезать", "Cut pages": "Вырезать страницы", @@ -283,11 +408,13 @@ "Dark & Moody": "Тёмный и атмосферный", "Darken": "Затемнить", "Darks": "Тёмные тона", + "Date": "Дата", "Date Groups": "Группы по датам", "Day": "День", "Daylight": "Дневной свет", "Decrease Brush Feather": "Уменьшить растушёвку кисти", "Decrease Brush Size": "Уменьшить размер кисти", + "Decrease Rating": "Уменьшить оценку", "Deep Sky": "Глубокое небо", "Defringe": "Убрать кайму", "Deghost Amount": "Степень удаления артефактов", @@ -297,19 +424,23 @@ "Delete Album": "Удалить альбом", "Delete All Masks": "Удалить все маски", "Delete Color Label Set": "Удалить набор цветовых меток", + "Delete Component": "Удалить компонент", "Delete Export Preset": "Удалить пресет экспорта", "Delete Filter Preset": "Удалить пресет фильтра", "Delete Imported Profile": "Удалить импортированный профиль", "Delete Keyword": "Удалить ключевое слово", "Delete Keyword Set": "Удалить набор ключевых слов", + "Delete Keyword…": "Удалить ключевое слово…", "Delete Mask": "Удалить маску", "Delete Metadata Preset": "Удалить пресет метаданных", "Delete Permanently": "Удалить навсегда", "Delete Photo": "Удалить фото", "Delete Photos": "Удалить фото", + "Delete Point Color": "Удалить точечный цвет", "Delete Point Color Sample": "Удалить точечный образец цвета", "Delete Point Curve Preset": "Удалить пресет точечной кривой", "Delete Preset": "Удалить пресет", + "Delete Red Eye": "Удалить красные глаза", "Delete Red Eye Correction": "Удалить коррекцию красных глаз", "Delete Spot": "Удалить пятно", "Delete Version": "Удалить версию", @@ -317,32 +448,63 @@ "Delete all spots": "Удалить все пятна", "Delete mask": "Удалить маску", "Delete them": "Удалить их", + "Denoise Model Downloads": "Загрузки моделей шумоподавления", + "Denoise Models": "Модели шумоподавления", + "Denoise Photos": "Подавить шум на фото", + "Denoise Pump": "Запустить очередь шумоподавления", + "Denoise Settings": "Настройки шумоподавления", + "Denoise Status": "Состояние шумоподавления", + "Denoise all photos with an Amount": "Подавить шум на всех фото с заданным количеством", "Density": "Плотность", "Depth Range": "Диапазон глубины", "Descending": "По убыванию", + "Describe": "Описать", + "Describe what to keep": "Опишите, что оставить", + "Describe what to leave out": "Опишите, что исключить", + "Describe what to select": "Опишите, что выделить", "Describe…": "Описать…", + "Description": "Описание", "Deselect": "Снять выделение", "Deselect All": "Снять выделение со всех", "Desert Heat": "Жар пустыни", "Desert Warmth": "Тепло пустыни", + "Desktop": "Рабочий стол", "Detail": "Детализация", "Detail (D)": "Детализация (D)", "Details": "Подробности", + "Detect Faces": "Найти лица", + "Detect Faces (Photo menu) needs it: a small one-time download.": "Для поиска лиц (меню «Фото») нужна небольшая однократная загрузка.", + "Develop": "Коррекция", "Diagonal": "По диагонали", "Discard Preview Cache": "Удалить кэш просмотров", + "Discarding smart previews": "Удаление смарт-просмотров", "Display P3": "Display P3", + "Display name": "Отображаемое имя", "Display theme": "Тема оформления", "Distortion": "Искажение", "Document properties…": "Свойства документа…", "Don't enlarge": "Не увеличивать", "Done": "Готово", + "Download": "Загрузить", + "Download AI Mask Model": "Загрузить модель ИИ-масок", + "Download Denoise Model": "Загрузить модель шумоподавления", + "Download Face Model": "Загрузить модель лиц", + "Download the SAM 3 Model?": "Загрузить модель SAM 3?", + "Downloaded and checked. Installing…": "Загружено и проверено. Установка…", + "Downloading model": "Загрузка модели", + "Downloads": "Загрузки", + "Drag Crop Handle": "Перетащить маркер кадрирования", "Drag along the horizon; double-click for Auto": "Ведите вдоль горизонта; двойной щелчок — авто", "Drag over an eye on the photo; the pupil inside is found automatically.": "Наведите на глаз на фото; зрачок внутри будет найден автоматически.", "Draw Guides": "Нарисовать направляющие", + "Drop here for the top level": "Перетащите сюда для верхнего уровня", "Duplicate": "Дублировать", + "Duplicate Component": "Дублировать компонент", "Duplicate Mask": "Дублировать маску", "Duplicate and Invert Mask": "Дублировать и инвертировать маску", "Each / starts a folder level; tags are filled in per photo (a level with missing metadata is \"unknown\").": "Каждый символ / начинает новый уровень папок; теги подставляются для каждого фото (уровень без метаданных называется «unknown»).", + "Edge": "Край", + "Edge setting, and get a sharper zoomed-in pass in the background. The model runs inside LightCraft in pure Rust": "настройка «Край», и получают более чёткий результат при увеличении в фоне. Модель работает внутри LightCraft на чистом Rust", "Edit": "Правка", "Edit (E)": "Правка (E)", "Edit Capture Time": "Изменить время съёмки", @@ -350,66 +512,112 @@ "Edit Color Label Names": "Изменить названия цветовых меток", "Edit Color Label Names…": "Изменить названия цветовых меток…", "Edit Copy for External Editor": "Править копию во внешнем редакторе", + "Edit Date": "Изменить дату", "Edit Info": "Править сведения", + "Edit Keyword Set": "Изменить набор ключевых слов", + "Edit Keyword Tag": "Изменить ключевое слово", + "Edit Keyword Tag…": "Изменить ключевое слово…", + "Edit Label Names": "Изменить названия меток", "Edit Label Names…": "Изменить названия меток…", + "Edit Mask": "Изменить маску", "Edit Mask Component": "Изменить компонент маски", "Edit Sections": "Изменить разделы", + "Edit Set…": "Изменить набор…", "Edit Smart Album": "Изменить смарт-альбом", "Edit Smart Album…": "Изменить смарт-альбом…", "Edit Spot": "Изменить пятно", "Edit in External Editor": "Править во внешнем редакторе", + "Edit in External Editor (⇧⌘E): a 16-bit TIFF copy, stacked, refreshed when you come back.": "Править во внешнем редакторе (⇧⌘E): 16-битная TIFF-копия, в стопке, обновляется при возвращении.", + "Edit settings copied": "Настройки правки скопированы", "Edited": "Изменено", + "Edited or not": "Изменено или нет", + "Editing": "Правка", "Effect": "Эффект", "Effects": "Эффекты", + "Empty Recently Deleted": "Очистить «Недавно удалённые»", "Enable Lens Corrections": "Включить коррекцию объектива", + "Enable Profile Corrections": "Включить коррекцию профиля", "End Interaction": "Завершить взаимодействие", "English": "Английский", "Enter Full Screen": "Полноэкранный режим", + "Enter a date as YYYY-MM-DD HH:MM:SS": "Введите дату в формате YYYY-MM-DD HH:MM:SS", "Erase": "Стереть", + "Evaluate Face Recognition": "Оценить распознавание лиц", "Every EXIF, GPS and XMP field in the file": "Все поля EXIF, GPS и XMP в файле", "Existing previews": "Имеющиеся просмотры", "Exit": "Выход", + "Expand": "Развернуть", "Expand All Stacks": "Развернуть все стопки", + "Expand Stack": "Развернуть стопку", "Expand/Collapse Stack": "Развернуть/свернуть стопку", "Export": "Экспорт", "Export Album with Preset": "Экспортировать альбом с пресетом", "Export Album…": "Экспортировать альбом…", "Export As…": "Экспортировать как…", + "Export Containing Keywords": "Экспортировать родительские ключевые слова", "Export Group…": "Экспортировать группу…", + "Export Keywords": "Экспортировать ключевые слова", + "Export Keywords…": "Экспортировать ключевые слова…", "Export Now": "Экспортировать сейчас", "Export Point Curve Presets": "Экспортировать пресеты точечной кривой", "Export Point Curve Presets…": "Экспортировать пресеты точечной кривой…", "Export Presets": "Экспортировать пресеты", "Export Presets…": "Экспортировать пресеты…", + "Export Synonyms": "Экспортировать синонимы", "Export User Presets…": "Экспортировать пользовательские пресеты…", + "Export file names use the Rename Photos tokens ({title}, {seq:2}, {date:%Y-%m-%d}…), plus new {num}, {folder}, {lens}, {iso}, {rating}, {creator}.": "Имена экспортируемых файлов используют теги переименования фото ({title}, {seq:2}, {date:%Y-%m-%d}…), а также новые {num}, {folder}, {lens}, {iso}, {rating}, {creator}.", + "Export stopped unexpectedly": "Экспорт неожиданно остановился", "Export with Preset": "Экспорт с пресетом", "Export with Previous": "Экспорт с предыдущими настройками", + "Exported files are written to a temp file and then renamed into place, so a full disk or an unplugged drive": "Экспортируемые файлы пишутся во временный файл, а затем переименовываются на место, поэтому заполненный или отключённый диск", "Exporting": "Экспорт", + "Exporting contact sheet": "Экспорт контактного листа", + "Exports and renders never write over a photo's original (issue #93): exporting into the photo's own folder with": "Экспорт и рендеринг никогда не перезаписывают оригинал фото (проблема #93): экспорт в собственную папку фото с", + "Exports are never black because of the GPU (issue #78): a GPU render that runs out of device": "Экспорт никогда не получается чёрным из-за GPU (проблема #78): рендеринг на GPU, которому не хватает памяти устройства,", "Export…": "Экспорт…", "Exposure": "Экспозиция", "Exposure (shutter · aperture · ISO)": "Экспозиция (выдержка · диафрагма · ISO)", "Extended Description": "Расширенное описание", "External Editor": "Внешний редактор", + "Face Boxes": "Рамки лиц", + "Face Model Downloads": "Загрузки моделей лиц", + "Face Models": "Модели лиц", + "Face Recognition On/Off": "Включить/выключить распознавание лиц", + "Face boxes": "Рамки лиц", + "Face detector": "Детектор лиц", + "Face names written to XMP by Lightroom and other apps show up here.": "Имена лиц, записанные в XMP в Lightroom и других приложениях, появляются здесь.", + "Face recognition": "Распознавание лиц", + "Faces": "Лица", + "Faces of a Person": "Лица человека", "Faded Matte": "Выцветший матовый", "Faded Slide": "Выцветший слайд", "Faded Urban": "Выцветший урбан", + "Failed": "Ошибки", + "Failed its last test": "Последняя проверка не пройдена", + "Faster exports and card imports, with the same protection for what can't be recreated (issue #134): exports,": "Более быстрые экспорт и импорт с карт при той же защите невосстановимых данных (проблема #134): экспорт,", "Favorite Preset": "Избранный пресет", "Favorite Profile": "Избранный профиль", "Favorites": "Избранное", "Feather": "Растушёвка", "File": "Файл", "File & date": "Файл и дата", + "File Extension": "Расширение файла", + "File Format": "Формат файла", "File Name": "Имя файла", "File Path": "Путь к файлу", "File Size": "Размер файла", + "File Type": "Тип файла", "File name": "Имя файла", "File types:": "Типы файлов:", + "Filename": "Имя файла", + "Fill": "Заливка", "Fill Edges": "Заполнить края", "Film": "Плёнка", "Filmstrip": "Лента", "Filmstrip (/)": "Лента (/)", "Filter": "Фильтр", "Filter Bar": "Панель фильтров", + "Filter Keywords": "Фильтровать ключевые слова", "Filter Presets": "Пресеты фильтров", "Filter bar": "Панель фильтров", "Filter fields": "Поля фильтра", @@ -417,17 +625,25 @@ "Find Missing Photos": "Найти отсутствующие фото", "Find Missing Photos…": "Найти отсутствующие фото…", "Find Similar Photos": "Найти похожие фото", + "Find photos added to or missing from this folder on disk, and XMP sidecars changed by other apps": "Найти фото, добавленные в эту папку на диске или отсутствующие в ней, и XMP-файлы, изменённые другими приложениями", "Find tools and commands…": "Найти инструменты и команды…", + "Finding faces": "Поиск лиц", "Find…": "Найти…", + "First": "Первый", + "First commit": "Первый коммит", "Fit": "Вписать", "Fit (F)": "Вписать (F)", "Fit on Screen": "Вписать в экран", "Fit to width": "По ширине", "Fit visible": "Вписать видимое", + "Five stars": "Пять звёзд", + "Fix the marked rules to save this album.": "Исправьте отмеченные условия, чтобы сохранить этот альбом.", "Fixed keys": "Фиксированные клавиши", "Flag": "Флаг", "Flag as Pick": "Пометить как выбранное", "Flag as Reject": "Пометить как отклонённое", + "Flagged as Pick": "Помечено как выбранное", + "Flagged as Reject": "Помечено как отклонённое", "Flash": "Вспышка", "Flip H": "Отразить по гор.", "Flip Horizontal": "Отразить по горизонтали", @@ -436,7 +652,9 @@ "Flip Vertical": "Отразить по вертикали", "Flow": "Поток", "Fluorescent": "Флуоресцентная лампа", + "Focal Length": "Фокусное расстояние", "Focal length": "Фокусное расстояние", + "Focus (assisted culling)": "Фокус (помощь при отборе)", "Folder": "Папка", "Folder name (renamed on disk; its photos follow)": "Имя папки (переименовывается на диске; фото следуют за ней)", "Folders": "Папки", @@ -447,6 +665,9 @@ "Form fields panel": "Панель полей формы", "Format": "Формат", "Forward": "Вперёд", + "Fr": "Пт", + "Frame time": "Время кадра", + "Frame update": "Обновление кадра", "Free": "Свободно", "Fresh & Bright": "Свежий и яркий", "Friday": "Пятница", @@ -456,19 +677,24 @@ "Full size": "Полный размер", "GPS Track Log": "GPS-трек", "GPU Rendering": "Рендеринг на GPU", + "GPU rendering": "Рендеринг на GPU", "General": "Общие", + "Generated demo photo": "Сгенерированное демо-фото", "Geometry": "Геометрия", "Get Develop Settings": "Получить настройки коррекции", "Global Hue": "Общий тон", "Global Luminance": "Общая яркость", "Global Saturation": "Общая насыщенность", "Glossy": "Глянцевый", + "Go back to listing them by name": "Вернуться к списку по названию", "Go to History Step": "Перейти к шагу истории", "Golden": "Золотой", "Golden Glow": "Золотое сияние", "Golden Hour": "Золотой час", + "Grab bag": "Случайная подборка", "Grain": "Зерно", "Graphic": "Графичный", + "Graphics card": "Видеокарта", "Green": "Зелёный", "Green Amount": "Количество зелёного", "Green Hue": "Тон зелёного", @@ -489,15 +715,23 @@ "Group by Year": "Группировать по годам", "Group by date": "Группировать по дате", "Group into Stack": "Сгруппировать в стопку", + "Group name": "Название группы", "Guided": "Направляемый", "Guided Upright": "Направляемое выравнивание", "Guided Upright Guides": "Направляющие выравнивания", "HDR Merge": "Слияние HDR", + "HDR Merge Preview": "Предпросмотр слияния HDR", "HDR Panorama Merge": "Слияние HDR-панорамы", + "HDR Panorama Merge Preview": "Предпросмотр слияния HDR-панорамы", "HDR Panorama with Last Settings": "HDR-панорама с последними настройками", + "HDR Panoramas": "HDR-панорамы", "HDR Panorama…": "HDR-панорама…", + "HDR panorama": "HDR-панорама", + "HDR panoramas": "HDR-панорамы", "HDR with Last Settings": "HDR с последними настройками", "HDR…": "HDR…", + "Has Edits": "Есть правки", + "Has GPS": "Есть GPS", "Heal": "Лечение", "Heal the small, soft dark spots sensor dust leaves on smooth areas": "Убирать мелкие мягкие тёмные пятна от пыли на матрице на ровных участках", "Height": "Высота", @@ -518,17 +752,25 @@ "Histogram": "Гистограмма", "History": "История", "History & Activity (Y)": "История и активность (Y)", + "Home": "Домашняя папка", "Horizontal": "По горизонтали", "Horizontal text": "Горизонтальный текст", + "Hours": "Часы", "How to install the model": "Как установить модель", "Hue": "Тон", "Hue Range": "Диапазон тонов", + "I have read these terms and accept them for my own use": "Я прочитал эти условия и принимаю их для собственного использования", + "ISO Speed": "Светочувствительность ISO", + "ISO speed": "Светочувствительность ISO", "If file exists": "Если файл существует", + "If your library can't be opened at launch (open in another program, unreadable, on a drive that isn't connected,": "Если библиотеку не удаётся открыть при запуске (она открыта в другой программе, не читается, на неподключённом диске,", "Image": "Изображение", "Import": "Импорт", "Import Date": "Дата импорта", - "Import Lightroom Catalog": "Импорт каталога Lightroom", - "Import Lightroom Catalog…": "Импорт каталога Lightroom…", + "Import Keywords": "Импортировать ключевые слова", + "Import Keywords…": "Импортировать ключевые слова…", + "Import Lightroom Catalog": "Импортировать каталог Lightroom", + "Import Lightroom Catalog…": "Импортировать каталог Lightroom…", "Import Photos": "Импорт фото", "Import Photos…": "Импорт фото…", "Import Point Curve Presets": "Импортировать пресеты точечной кривой", @@ -537,30 +779,55 @@ "Import Presets…": "Импортировать пресеты…", "Import Profiles": "Импортировать профили", "Import Profiles & Presets…": "Импортировать профили и пресеты…", + "Import as new": "Импортировать как новое", "Import from Device": "Импорт с устройства", "Import from Folder…": "Импорт из папки…", + "Import new photos": "Импортировать новые фото", "Import photos with File → Import Photos… (Cmd+Shift+I), or drop them here": "Импортируйте фото через Файл → Импорт фото… (Cmd+Shift+I) или перетащите их сюда", + "Import presets from other editors: XMP presets, classic `.lrtemplate` files, \"DNG presets\" from mobile apps and `.zip`": "Импорт пресетов из других редакторов: XMP-пресеты, классические файлы `.lrtemplate`, «DNG-пресеты» из мобильных приложений и `.zip`", + "Import ▸ Copy verifies every copy, like Move (issue #96): each file is written as a new file, synced to disk and": "Импорт → Копировать проверяет каждую копию, как и «Переместить» (проблема #96): каждый файл записывается как новый, синхронизируется на диск и", + "Import ▸ Move: photos go into the destination (with the same folders and renaming as Copy, e.g.": "Импорт → Переместить: фото попадают в папку назначения (с теми же папками и переименованием, что и при копировании, например", + "Import: copy to any folder, by day / by month / one folder, rename on import, metadata preset, Copy as DNG.": "Импорт: копирование в любую папку, по дням / по месяцам / в одну папку, переименование при импорте, пресет метаданных, копирование как DNG.", + "Imported": "Импортированные", "Imported Photos": "Импортированные фото", "Importing": "Импорт", "Importing Lightroom Catalog": "Импорт каталога Lightroom", + "Importing Lightroom catalog": "Импорт каталога Lightroom", "Importing scans the source for photos; it doesn't add the folder to Local (use Local → Browse Folder… to work in a folder without importing).": "При импорте источник сканируется на фото; папка не добавляется в «Локальное» (чтобы работать с папкой без импорта, используйте Локальное → Выбрать папку…).", + "In Color": "Цветное", "In Photo Grid and Square Grid: Flag as Pick and advance": "В сетке фото и квадратной сетке: пометить как выбранное и перейти дальше", "In Photo Grid and Square Grid: expand or collapse the stack": "В сетке фото и квадратной сетке: развернуть или свернуть стопку", "In Recently Deleted": "В «Недавно удалённых»", + "In a Stack": "В стопке", "In library": "В библиотеке", "In memory — nothing is saved": "В памяти — ничего не сохраняется", "In use": "Используется", "In-memory session: these settings last until LightCraft quits.": "Сеанс в памяти: эти настройки действуют до закрытия LightCraft.", + "Include on Export": "Включать при экспорте", "Include subfolders": "Включая вложенные папки", "Increase Brush Feather": "Увеличить растушёвку кисти", "Increase Brush Size": "Увеличить размер кисти", + "Increase Rating": "Повысить оценку", + "Index Faces": "Индексировать лица", + "Index Faces in the Background": "Индексировать лица в фоне", "Info": "Информация", "Info (I)": "Информация (I)", + "Info Overlay Off": "Выключить информационную накладку", + "Info Overlay: Exposure": "Информационная накладка: экспозиция", + "Info Overlay: File": "Информационная накладка: файл", "Info overlay": "Информационная накладка", + "Insert at the cursor": "Вставить в позицию курсора", + "Inspect Denoise Model": "Проверить модель шумоподавления", + "Inspect Face Model File": "Проверить файл модели лиц", "Inspect Lightroom Catalog": "Проверить каталог Lightroom", "Inspect Photo": "Просмотр фото", "Inspecting Lightroom Catalog": "Проверка каталога Lightroom", - "Install the SAM 3 Model": "Установка модели SAM 3", + "Install Denoise Model": "Установить модель шумоподавления", + "Install Face Model": "Установить модель лиц", + "Install a denoise model to enable AI Denoise on this photo": "Установите модель шумоподавления, чтобы включить ИИ-шумоподавление для этого фото", + "Install from file…": "Установить из файла…", + "Install the SAM 3 Model": "Установить модель SAM 3", + "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "Установите собственную модель ONNX, положив рядом denoise-model.json. В этой сборке загрузка моделей недоступна.", "Instant ’70s": "Мгновенный стиль 70-х", "Interface": "Интерфейс", "Interface language": "Язык интерфейса", @@ -568,42 +835,64 @@ "Intersect with": "Пересечь с", "Into one folder": "В одну папку", "Invert": "Инвертировать", + "Invert Component": "Инвертировать компонент", "Invert Mask": "Инвертировать маску", + "It tests this album, so testing it back would loop": "Он проверяет этот альбом, поэтому обратная проверка зациклилась бы", + "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "Его изображение с подавленным шумом ещё не создано, а изображения сами не создаются (Настройки, ИИ-шумоподавление).", "JPEG (Large)": "JPEG (большой)", "JPEG (Small)": "JPEG (малый)", + "JPEG with the same name (`IMG_0001.CR3` + `IMG_0001.JPG`) no longer share one sidecar: the raw keeps `IMG_0001.xmp`,": "JPEG с таким же именем (`IMG_0001.CR3` + `IMG_0001.JPG`) больше не используют общий XMP-файл: RAW сохраняет `IMG_0001.xmp`,", "Join the ArtCraft Discord": "Присоединяйтесь к Discord ArtCraft", "Join the ArtCraft Discord…": "Присоединиться к Discord ArtCraft…", + "Join the ArtCraft community on Discord": "Присоединяйтесь к сообществу ArtCraft в Discord", "Keep in Local": "Оставить в «Локальном»", "Keep it on a drive with room: smart previews are about 1 MB per photo. The thumbnail cache stays in the library.": "Храните их на диске со свободным местом: смарт-просмотр занимает около 1 МБ на фото. Кэш миниатюр остаётся в библиотеке.", "Keyboard Shortcuts": "Сочетания клавиш", "Keyboard shortcuts": "Сочетания клавиш", "Keyword": "Ключевое слово", + "Keyword Count": "Количество ключевых слов", + "Keyword List": "Список ключевых слов", + "Keyword Lists": "Списки ключевых слов", + "Keyword Name": "Название ключевого слова", "Keyword Painter": "Кисть ключевых слов", "Keyword Set": "Набор ключевых слов", "Keyword Sets": "Наборы ключевых слов", "Keyword Suggestions": "Подсказки ключевых слов", + "Keyword Tag Options": "Параметры ключевого слова", "Keyword to paint": "Ключевое слово для нанесения", "Keywords": "Ключевые слова", + "Keywords & People": "Ключевые слова и люди", "Keywords (K)": "Ключевые слова (K)", + "Keywords (⌥1–⌥9)": "Ключевые слова (⌥1–⌥9)", "Keywords you add appear here; ⌥1–⌥9 apply them.": "Добавленные вами ключевые слова появятся здесь; ⌥1–⌥9 применяют их.", + "Label": "Метка", + "Label Set": "Набор меток", "Landscape": "Пейзаж", + "Landscape (wide)": "Альбомная (широкая)", "Language": "Язык", "Last": "Последний", + "Last GPU fallback": "Последний откат с GPU", + "Last commit": "Последний коммит", + "Last loupe render": "Последний рендеринг лупы", "Last view": "Последний вид", "Lateral Chromatic Aberration": "Боковая хроматическая аберрация", "Layer": "Слой", "Layers": "Слои", "Leave them": "Оставить их", "Lens": "Объектив", + "Lens Blur": "Размытие объектива", "Lens Corrections": "Коррекция объектива", "Level": "Уровень", "Library": "Библиотека", + "Library Folders": "Папки библиотеки", "Library Info": "Сведения о библиотеке", "Library Originals": "Оригиналы библиотеки", "Library Preferences": "Настройки библиотеки", "Library State": "Состояние библиотеки", + "Library saved": "Библиотека сохранена", "Licence:": "Лицензия:", "Light": "Светлый", + "LightCraft 0.2.0 and older refuse it (\"unsupported format … v2\").": "LightCraft 0.2.0 и старше отказываются его открывать («неподдерживаемый формат … v2»).", "LightCraft Default": "По умолчанию в LightCraft", "LightCraft Help": "Справка LightCraft", "LightCraft Preset": "Пресет LightCraft", @@ -612,15 +901,23 @@ "LightCraft could not start": "Не удалось запустить LightCraft", "LightCraft couldn't find where to keep your library.": "LightCraft не удалось найти место для хранения вашей библиотеки.", "LightCraft couldn't open the library at": "LightCraft не удалось открыть библиотеку в", + "LightCraft does not know this model, so it assumed:": "LightCraft не знает эту модель и предполагает:", + "LightCraft no longer crashes at launch on Windows PCs whose Vulkan driver is broken (issue #136, e.g. some Intel UHD": "LightCraft больше не падает при запуске на ПК с Windows с неисправным драйвером Vulkan (проблема #136, например, некоторые Intel UHD", "LightCraft on GitHub": "LightCraft на GitHub", "LightCraft website": "Сайт LightCraft", "LightCraft — Second Window": "LightCraft — второе окно", "Lightroom Classic Catalog": "Каталог Lightroom Classic", - "Lightroom import cancelled": "Импорт из Lightroom отменён", + "Lightroom import cancelled": "Импорт Lightroom отменён", "Lightroom inspection cancelled": "Проверка Lightroom отменена", "Lights": "Светлые тона", + "Limit": "Ограничение", "Limit file size (KB, 0 = off)": "Ограничить размер файла (КБ, 0 = выкл.)", + "Line delta": "Разница строк", + "Linear": "Линейный", "Linear Gradient": "Линейный градиент", + "Lines +/−": "Строки +/−", + "Lines added": "Добавлено строк", + "Lines deleted": "Удалено строк", "List Albums": "Список альбомов", "List Develop Controls": "Список элементов управления коррекцией", "List History": "Список истории", @@ -633,20 +930,29 @@ "Locate Missing File…": "Указать отсутствующий файл…", "Locate Photo": "Найти фото", "Location": "Местоположение", + "Lock": "Заблокировать", + "Lock Crop Aspect": "Зафиксировать пропорции кадра", + "Locked": "Заблокировано", "Long Edge": "Длинная сторона", + "Looking for changes": "Поиск изменений", "Looking for photos…": "Поиск фото…", "Lossless": "Без потерь", + "Lossy DNG files and Smart Previews open as raw photos.": "DNG с потерями и смарт-просмотры открываются как RAW-фото.", "Low": "Низкий", "Luminance": "Яркость", "Luminance Range": "Диапазон яркости", + "Luminar looks: `.lmp` files and `.mplumpack` collections import as presets (grouped by collection); the sliders": "Стили Luminar: файлы `.lmp` и коллекции `.mplumpack` импортируются как пресеты (группируются по коллекции); ползунки", "Lush Greens": "Сочная зелень", "Magenta": "Пурпурный", "Magenta Hue": "Тон пурпурного", "Magenta Luminance": "Яркость пурпурного", "Magenta Saturation": "Насыщенность пурпурного", "Make Candidate the Select": "Сделать кандидата выбранным", + "Make it now": "Создать сейчас", + "Make the denoised picture of the photos I am looking at": "Создать изображение с подавленным шумом для фото, которые я просматриваю", "Manual": "Вручную", "Marquee zoom": "Масштаб рамкой", + "Mask Adjustment": "Коррекция маски", "Masking": "Маски", "Masking (M)": "Маски (M)", "Masks": "Маски", @@ -659,17 +965,22 @@ "Megapixels": "Мегапиксели", "Memory Budget": "Лимит памяти", "Memory Usage": "Использование памяти", + "Memory budget": "Лимит памяти", "Memory for caches": "Память для кэшей", "Menu": "Меню", "Merge": "Слияние", "Merge Keywords": "Объединить ключевые слова", "Merge into…": "Объединить с…", + "Merged": "Объединено", + "Merged PRs": "Влитые PR", + "Merging": "Слияние", "Merging…": "Слияние…", "Meta's terms, not LightCraft's. Downloading it means accepting them.": "Условия Meta, а не LightCraft. Загрузка означает их принятие.", "Metadata": "Метаданные", "Metadata Preset": "Пресет метаданных", "Metadata Presets": "Пресеты метаданных", "Metadata preset": "Пресет метаданных", + "Metadata read": "Метаданные прочитаны", "Midpoint": "Средняя точка", "Midtones": "Средние тона", "Midtones Hue": "Тон средних тонов", @@ -678,8 +989,13 @@ "Midtones split": "Разделение средних тонов", "Minimize": "Свернуть", "Missing Photos": "Отсутствующие фото", + "Missing metadata ({camera}, {title}…) leaves an empty gap; a name that comes out empty keeps the original name.": "Отсутствующие метаданные ({camera}, {title}…) оставляют пустое место; если получившееся имя пустое, сохраняется исходное.", + "Missing photos move to Recently Deleted and can be restored; no file on disk is touched.": "Отсутствующие фото перемещаются в «Недавно удалённые», их можно восстановить; файлы на диске не затрагиваются.", "Misty Morning": "Туманное утро", + "Mo": "Пн", "Mode": "Режим", + "Model": "Модель", + "Models": "Модели", "Modified Date": "Дата изменения", "Monday": "Понедельник", "Mono Red Filter": "Моно с красным фильтром", @@ -693,28 +1009,40 @@ "More preset options": "Другие параметры пресета", "Move": "Переместить", "Move Album": "Переместить альбом", + "Move Down": "Переместить вниз", "Move Down in Stack": "Опустить в стопке", "Move Folder": "Переместить папку", "Move Folder To…": "Переместить папку в…", "Move Mask": "Переместить маску", "Move Preset to Group": "Переместить пресет в группу", + "Move Preset to New Group": "Переместить пресет в новую группу", + "Move Up": "Переместить вверх", "Move Up in Stack": "Поднять в стопке", + "Move in Stack": "Переместить в стопке", "Move the files (into the library's Originals/, or a folder you choose), removing them from the source": "Переместить файлы (в папку «Originals/» библиотеки или в выбранную папку), удалив их из источника", "Move them": "Переместить их", "Move to": "Переместить в", "Move to Group": "Переместить в группу", "Move: the files are moved to the folder below; each original (and its XMP sidecar) is removed from the source only after its copy is verified. Duplicates and files that fail stay where they are.": "Перемещение: файлы перемещаются в указанную ниже папку; каждый оригинал (и его XMP-файл) удаляется из источника только после проверки копии. Дубликаты и файлы с ошибками остаются на месте.", + "Moves every photo imported from this disk to Recently Deleted; no file is touched": "Перемещает все фото, импортированные с этого диска, в «Недавно удалённые»; файлы не затрагиваются", + "Moves the photos imported from this folder to Recently Deleted; no file is touched": "Перемещает фото, импортированные из этой папки, в «Недавно удалённые»; файлы не затрагиваются", "Muted": "Приглушённый", "Muted Film": "Приглушённая плёнка", "My Photos": "Мои фото", "My Photos Panel": "Панель «Мои фото»", "Name": "Название", + "Name (A–Z)": "Имя (А–Я)", + "Name Face": "Назвать лицо", + "Name Faces": "Назвать лица", + "Name of the folder the original is in": "Название папки с оригиналом", "Named": "Именованные", + "Named People": "Люди с именами", "Names appear in the label menu, the filter bar and the Info panel, and are written to XMP. Empty = the colour's name.": "Названия показываются в меню меток, на панели фильтров и в панели «Информация» и записываются в XMP. Пусто = название цвета.", "Navigator": "Навигатор", "Needs Work": "Требует доработки", "Neon Dusk": "Неоновые сумерки", "Neon Night": "Неоновая ночь", + "Nested group": "Вложенная группа", "Neutral": "Нейтральный", "Never": "Никогда", "New": "Новый", @@ -729,27 +1057,42 @@ "New album…": "Новый альбом…", "New blank PDF": "Новый пустой PDF", "New name": "Новое имя", + "New time": "Новое время", "New…": "Новый…", "Next Photo": "Следующее фото", + "Next up for AI Denoise…": "Следующие для ИИ-шумоподавления…", "Night Blue": "Ночной синий", + "Nikon NEFs start from a colour and tone look fitted to the camera's own JPEG, as Sony ARWs do, instead of a muted,": "Nikon NEF начинают с цвета и тона, подогнанных под собственный JPEG камеры, как и Sony ARW, вместо приглушённого,", + "Nikon splits into two differently compressed halves still use the preview for now.)": "Nikon с разделением на две по-разному сжатые половины пока используют просмотр.)", "No": "Нет", "No Camera or Card Found": "Камера или карта не найдены", "No Date Groups": "Без групп по датам", + "No Label": "Без метки", "No Stars": "Без звёзд", "No Tool": "Нет инструмента", + "No albums match": "Подходящих альбомов нет", + "No albums yet": "Альбомов пока нет", "No automatic versions.": "Автоматических версий нет.", "No camera": "Нет камеры", + "No contributor data was built into this copy.": "В эту копию не встроены данные об участниках.", "No date": "Нет даты", "No dust spots found": "Пылинки не найдены", "No edits yet.": "Правок пока нет.", "No exposure information": "Нет данных об экспозиции", "No filter presets yet": "Пресетов фильтров пока нет", + "No keywords are exported": "Ключевые слова не экспортируются", + "No keywords match": "Подходящих ключевых слов нет", + "No keywords yet": "Ключевых слов пока нет", "No lens data embedded in this file (DNG lens opcodes only).": "В этом файле нет данных об объективе (только опкоды объектива DNG).", "No masks yet. Choose a mask type above.": "Масок пока нет. Выберите тип маски выше.", - "No matching commands": "Нет подходящих команд", + "No matching commands": "Подходящих команд нет", "No matching photos": "Нет подходящих фото", "No metadata found": "Метаданные не найдены", + "No model credits were built into this copy.": "В эту копию не встроены данные об используемых моделях.", + "No named people in these photos": "На этих фото нет людей с именами", + "No named people yet": "Людей с именами пока нет", "No new presets": "Новых пресетов нет", + "No photo has it; the keywords below it go too.": "Его нет ни на одном фото; вложенные в него ключевые слова тоже исчезнут.", "No photo selected": "Фото не выбрано", "No photos": "Нет фото", "No photos found": "Фото не найдены", @@ -764,14 +1107,23 @@ "Noise Reduction": "Подавление шума", "Non-raw photos": "Не RAW-фото", "None": "Нет", + "Not Now": "Не сейчас", + "Not saved": "Не сохранено", + "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "Ничего не загружается, пока вы не согласитесь. Модель хранится только на этом компьютере; LightCraft никогда не выгружает и не распространяет её.", "Nothing to compare": "Нечего сравнивать", "Nothing to survey": "Нечего просматривать в обзоре", "Nudge Develop Value": "Подправить значение коррекции", "OK": "OK", + "ONNX model": "Модель ONNX", "Object": "Объект", + "Object Mask": "Маска объекта", + "Object and Describe masks use SAM 3, Meta's segmentation model. It isn't part of LightCraft, and everything else works without it.": "Маски «Объект» и «Описать» используют SAM 3 — модель сегментации от Meta. Она не входит в LightCraft, а остальные функции работают без неё.", + "October 2026": "Октябрь 2026", "Off": "Выкл.", "Offset X": "Смещение по X", "Offset Y": "Смещение по Y", + "Only": "Только", + "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "Создаются только изображения для фото, у которых значение ИИ-шумоподавления больше 0. Экспорт всегда создаёт нужное изображение, если его нет.", "Opacity": "Непрозрачность", "Opacity (%)": "Непрозрачность (%)", "Open Library": "Открыть библиотеку", @@ -779,7 +1131,10 @@ "Open Log Folder": "Открыть папку журналов", "Open in": "Открыть в", "Open in Detail": "Открыть в режиме «Детализация»", + "Open page": "Открыть страницу", + "Open the model's page": "Открыть страницу модели", "Open the place in OpenStreetMap": "Открыть место в OpenStreetMap", + "Opens the model's own page in your browser, to read about it or get the file yourself.": "Открывает страницу модели в браузере, чтобы прочитать о ней или получить файл самостоятельно.", "Open…": "Открыть…", "Optics": "Оптика", "Optimize Library": "Оптимизировать библиотеку", @@ -792,6 +1147,8 @@ "Orientation": "Ориентация", "Original": "Оригинал", "Original + Settings": "Оригинал + настройки", + "Original extension without the dot": "Исходное расширение без точки", + "Original file name without its extension": "Исходное имя файла без расширения", "Original files": "Оригиналы файлов", "Original offline · editing the smart preview": "Оригинал недоступен · правка смарт-просмотра", "Original offline · no smart preview": "Оригинал недоступен · нет смарт-просмотра", @@ -804,21 +1161,32 @@ "Paint only areas like the one under the brush": "Закрашивать только области, похожие на ту, что под кистью", "Paint over a distraction on the photo to remove it.": "Закрасьте лишнее на фото, чтобы убрать это.", "Paint overlay": "Наложение закраски", + "Panasonic and Leica raws (RW2, RWL and the older RAW files, DMC-LX1 to DC-S1R II) develop from the raw data in": "RAW Panasonic и Leica (RW2, RWL и старые файлы RAW, от DMC-LX1 до DC-S1R II) обрабатываются из данных RAW в", + "Panorama": "Панорама", "Panorama Merge": "Слияние панорамы", + "Panorama Merge Preview": "Предпросмотр слияния панорамы", "Panorama with Last Settings": "Панорама с последними настройками", + "Panoramas": "Панорамы", "Panorama…": "Панорама…", "Parametric curve": "Параметрическая кривая", "Paste": "Вставить", - "Paste Edit Settings": "Вставить настройки правки", + "Paste Edit Settings": "Вставить правки", "Paste Metadata": "Вставить метаданные", "Paste Selected Settings": "Вставить выбранные настройки", "Paste Selected Settings…": "Вставить выбранные настройки…", + "Paste Settings": "Вставить настройки", "Paste Settings from Previous": "Вставить настройки из предыдущего", + "Paste from Previous": "Вставить из предыдущего", "Paste pages": "Вставить страницы", "Pastel Haze": "Пастельная дымка", + "Pending photo action cancelled": "Отложенное действие с фото отменено", + "Pending photo action cancelled because its library or photo changed": "Отложенное действие с фото отменено, так как изменилась библиотека или фото", + "People": "Люди", + "People Count": "Число людей", "Percent": "Проценты", "Percentage": "Процент", "Performance": "Производительность", + "Person": "Человек", "Perspective": "Перспектива", "Pet Eye": "Глаза питомца", "Pet Eye Catchlight": "Блик в глазах питомца", @@ -826,39 +1194,51 @@ "Photo Grid": "Сетка фото", "Photo Grid (G)": "Сетка фото (G)", "Photo Merge": "Слияние фото", + "Photo menu. The filmstrip has the photo context menu too. Adding a file again that is in Recently Deleted no": "меню «Фото». У плёнки тоже есть контекстное меню фото. Повторное добавление файла из «Недавно удалённых» больше не", "Photo ▸ Edit in External Editor (⇧⌘E) renders a 16-bit TIFF copy, stacks it with the original and opens it here (an app name on macOS, a program path elsewhere).": "Фото → Править во внешнем редакторе (⇧⌘E) создаёт 16-битную TIFF-копию, помещает её в стопку с оригиналом и открывает здесь (имя приложения в macOS, путь к программе в других системах).", "Photos": "Фото", + "Photos in Recently Deleted can be restored from the app: right-click ▸ Restore (or Delete Permanently), also in the": "Фото из «Недавно удалённых» можно восстановить в приложении: щёлкните правой кнопкой → Восстановить (или Удалить навсегда), также в", + "Photos per bracket (0 = auto)": "Фото в брекетинге (0 = авто)", "Photos seen in Local but never added or changed leave the catalog when their folder hasn't been browsed for this long (checked when the library opens). Files stay on disk; browsing the folder shows them again.": "Фото, которые видели в «Локальном», но ни разу не добавляли и не меняли, исключаются из каталога, если папку не открывали так долго (проверяется при открытии библиотеки). Файлы остаются на диске; при просмотре папки они появятся снова.", "Photos that arrive in this folder (tethering, a scanner, a sync app) are added as soon as they're complete.": "Фото, поступающие в эту папку (тетеринг, сканер, приложение синхронизации), добавляются, как только будут готовы.", + "Photos); importing (also by drag and drop), Find Missing Photos, Build / Discard Smart Previews, the Rename preview,": "фото); импорт (в том числе перетаскиванием), поиск отсутствующих фото, создание / удаление смарт-просмотров, предпросмотр переименования,", "Pick": "Выбрать", + "Pick Flag": "Флаг «Выбрано»", "Pick a colour on the photo with the eyedropper.": "Выберите цвет на фото пипеткой.", + "Pick a date": "Выберите дату", "Pick the sharpest photo of each burst": "Выбирать самое чёткое фото в каждой серии", "Picked": "Выбрано", "Picks": "Выбранные", + "Pictures": "Изображения", "Pixels": "Пиксели", "Point Color": "Точечный цвет", "Point Curve": "Точечная кривая", "Point Curve Presets": "Пресеты точечной кривой", "Point curve": "Точечная кривая", "Portrait": "Портрет", + "Portrait (tall)": "Портретная (высокая)", "Position": "Положение", "Preferences": "Настройки", "Preferences…": "Настройки…", + "Prepare AI Masks": "Подготовить ИИ-маски", "Preset": "Пресет", "Preset name": "Название пресета", "Presets": "Пресеты", "Presets & Profiles": "Пресеты и профили", "Presets (Shift+P)": "Пресеты (Shift+P)", + "Presets and profiles": "Пресеты и профили", "Presets ▾": "Пресеты ⌄", "Press keys…": "Нажмите клавиши…", "Preview": "Просмотр", "Preview Build Progress": "Ход создания просмотров", "Preview only": "Только просмотр", + "Preview only: editing the camera's embedded JPEG": "Только просмотр: правка встроенного JPEG камеры", "Preview size": "Размер просмотра", "Previews": "Просмотры", "Previous Photo": "Предыдущее фото", "Print": "Печать", "Print…": "Печать…", + "Processor": "Процессор", "Profile": "Профиль", "Profile Browser": "Обозреватель профилей", "Profile Distortion": "Дисторсия профиля", @@ -867,9 +1247,12 @@ "Profile:": "Профиль:", "Profiles": "Профили", "Projection": "Проекция", + "Proof copy created": "Пробная копия создана", "Properties": "Свойства", + "Public Domain": "Общественное достояние", "Public domain": "Общественное достояние", "Pupil Size": "Размер зрачка", + "Purge Unused Keywords": "Очистить неиспользуемые ключевые слова", "Purple": "Фиолетовый", "Purple Amount": "Количество фиолетового", "Purple Hue": "Тон фиолетового", @@ -877,43 +1260,62 @@ "Purple Hue Low": "Тон фиолетового (низ)", "Purple Luminance": "Яркость фиолетового", "Purple Saturation": "Насыщенность фиолетового", + "Put New Keywords Inside This Keyword": "Добавлять новые ключевые слова внутрь этого ключевого слова", "Put the locations you removed from Local back (no files change)": "Вернуть в «Локальное» удалённые вами места (файлы не меняются)", "Quality": "Качество", "Query Photos": "Запросить фото", "Quick Collection": "Быстрая коллекция", + "Quick Collection and target album (B in the grid), keyword sets (⌥1–⌥9), colour-label sets.": "Быстрая коллекция и целевой альбом (B в сетке), наборы ключевых слов (⌥1–⌥9), наборы цветовых меток.", "Quick Develop": "Быстрая коррекция", "Quiet Green": "Тихий зелёный", "Quit": "Выход", + "Quit Anyway": "Всё равно выйти", "Quit LightCraft": "Выйти из LightCraft", + "Quit while tasks are running?": "Выйти, пока выполняются задачи?", + "Quit with unsaved changes?": "Выйти с несохранёнными изменениями?", + "RAW decoding": "Декодирование RAW", + "Radial": "Радиальный", "Radial Gradient": "Радиальный градиент", "Radius": "Радиус", "Random": "Случайно", "Range": "Диапазон", - "Rate ★": "Оценка ★", - "Rate ★ and advance": "Оценка ★ и перейти дальше", - "Rate ★★": "Оценка ★★", - "Rate ★★ and advance": "Оценка ★★ и перейти дальше", - "Rate ★★★": "Оценка ★★★", - "Rate ★★★ and advance": "Оценка ★★★ и перейти дальше", - "Rate ★★★★": "Оценка ★★★★", - "Rate ★★★★ and advance": "Оценка ★★★★ и перейти дальше", - "Rate ★★★★★": "Оценка ★★★★★", - "Rate ★★★★★ and advance": "Оценка ★★★★★ и перейти дальше", + "Rate ★": "Оценить ★", + "Rate ★ and advance": "Оценить ★ и перейти дальше", + "Rate ★★": "Оценить ★★", + "Rate ★★ and advance": "Оценить ★★ и перейти дальше", + "Rate ★★★": "Оценить ★★★", + "Rate ★★★ and advance": "Оценить ★★★ и перейти дальше", + "Rate ★★★★": "Оценить ★★★★", + "Rate ★★★★ and advance": "Оценить ★★★★ и перейти дальше", + "Rate ★★★★★": "Оценить ★★★★★", + "Rate ★★★★★ and advance": "Оценить ★★★★★ и перейти дальше", "Rating": "Оценка", + "Rating cleared": "Оценка сброшена", "Ratings & flags": "Оценки и флаги", + "Raw": "RAW", "Raw default": "Стандарт RAW", "Raw defaults": "Стандарты RAW", "Raw files": "RAW-файлы", "Raw photos": "RAW-фото", "Raw photos as DNG, with the edits embedded. Size, color and output options don't apply.": "RAW-фото как DNG со встроенными правками. Параметры размера, цвета и вывода не применяются.", "Read Metadata from File": "Прочитать метаданные из файла", + "Read metadata updates from XMP sidecars": "Прочитать обновлённые метаданные из XMP-файлов", "Read mode": "Режим чтения", "Read the SAM License": "Прочитать лицензию SAM", "Reading": "Чтение", + "Reading Lightroom catalog": "Чтение каталога Lightroom", "Reading Lightroom catalog…": "Чтение каталога Lightroom…", + "Reading a sidecar replaces the photo's rating, labels, metadata and edits with what it says.": "Чтение XMP-файла заменяет оценку, метки, метаданные и правки фото его содержимым.", + "Reading folder": "Чтение папки", "Reading folder…": "Чтение папки…", + "Real name": "Настоящее имя", + "Recent": "Недавние", + "Recent Keywords": "Недавние ключевые слова", + "Recent Keywords is built in: choose another name": "«Недавние ключевые слова» — встроенный набор: выберите другое название", "Recently Added": "Недавно добавленные", "Recently Deleted": "Недавно удалённые", + "Recognise faces (experimental)": "Распознавать лица (экспериментально)", + "Recognition models": "Модели распознавания", "Red": "Красный", "Red Eye": "Красные глаза", "Red Hue": "Тон красного", @@ -927,40 +1329,64 @@ "Reference": "Эталон", "Reference View": "Режим эталона", "Reference the files where they are": "Ссылаться на файлы там, где они лежат", + "Refine": "Уточнить", + "Refine AI Mask Detail": "Уточнить детали ИИ-маски", + "Refine Edges": "Уточнить края", "Refine Mask Edges": "Уточнить края маски", + "Refine Saturation": "Уточнить насыщенность", + "Refining the mask's detail…": "Уточнение деталей маски…", "Refresh Source": "Обновить источник", "Refresh Source (/)": "Обновить источник (/)", "Reject": "Отклонить", "Reject below focus": "Отклонять при недостаточной резкости", "Rejected": "Отклонено", "Rejects": "Отклонённые", + "Reliability": "Надёжность", + "Relink Photo": "Повторно связать фото", + "Relink photos whose file was renamed or moved": "Повторно связать фото, файл которых переименован или перемещён", + "Relinked": "Связано повторно", + "Reload Changed Files": "Перезагрузить изменённые файлы", "Reload from Disk": "Перезагрузить с диска", "Remove": "Удалить", "Remove (H)": "Удалить (H)", "Remove CA": "Убрать ХА", "Remove Chromatic Aberration": "Убрать хроматические аберрации", - "Camera local tone mapping": "Локальная тональная коррекция камеры", - "Apply the local tone mapping stored in the raw (Apple ProRAW), as the camera renders it": "Применить локальную тональную коррекцию, сохранённую в raw (Apple ProRAW), как её рендерит камера", + "Remove Denoise Model": "Удалить модель шумоподавления", + "Remove Disk from Library": "Убрать диск из библиотеки", + "Remove Disk from Library…": "Убрать диск из библиотеки…", + "Remove Face Box": "Удалить рамку лица", + "Remove Face Model": "Удалить модель лиц", + "Remove Folder from Library": "Убрать папку из библиотеки", + "Remove a filter above, or choose Clear all": "Уберите фильтр выше или выберите «Очистить всё»", "Remove from Album": "Удалить из альбома", "Remove from Favorites": "Удалить из избранного", + "Remove from Library…": "Убрать из библиотеки…", "Remove from Local": "Убрать из «Локального»", "Remove from Photo": "Убрать с фото", "Remove from Selected Photos": "Убрать с выбранных фото", "Remove from Stack": "Убрать из стопки", + "Remove from Target Album": "Убрать из целевого альбома", "Remove from survey": "Убрать из обзора", "Remove location info": "Удалить сведения о местоположении", - "Remove shortcut": "Удалить сочетание", + "Remove missing photos from the library": "Убрать отсутствующие фото из библиотеки", + "Remove shortcut": "Удалить сочетание клавиш", + "Remove this face box (undo with Edit ▸ Undo)": "Удалить эту рамку лица (отмена: Правка → Отменить)", "Remove this filter": "Убрать этот фильтр", "Remove this group": "Убрать эту группу", "Remove this rule": "Убрать это условие", + "Removed": "Убрано", "Rename": "Переименовать", "Rename Album": "Переименовать альбом", + "Rename Component": "Переименовать компонент", "Rename Folder": "Переименовать папку", "Rename Folder…": "Переименовать папку…", + "Rename Folder… and Move Folder To… (Local) can be undone and redone (issue #97): the folder moves back on disk with": "Переименование папки… и перемещение папки в… («Локальное») можно отменить и повторить (проблема #97): папка возвращается на диске вместе с", "Rename Keyword": "Переименовать ключевое слово", "Rename Keyword…": "Переименовать ключевое слово…", "Rename Mask": "Переименовать маску", "Rename Photos": "Переименовать фото", + "Rename Photos never overwrites another photo when only the letter case changes (issue #95): on case-sensitive": "Переименование фото никогда не перезаписывает другое фото, когда меняется только регистр букв (проблема #95): на томах с учётом регистра", + "Rename Photos reports files it could not move back after a failure (issue #105), e.g. when a network share drops": "Переименование фото сообщает о файлах, которые не удалось вернуть после сбоя (проблема #105), например при обрыве сетевого соединения", "Rename Photos…": "Переименовать фото…", "Rename Preset": "Переименовать пресет", "Rename Preview": "Предпросмотр переименования", @@ -969,6 +1395,8 @@ "Rename…": "Переименовать…", "Rendering": "Рендеринг", "Rendering…": "Рендеринг…", + "Reorder Album": "Изменить порядок альбома", + "Reorder Masks": "Изменить порядок масок", "Reset": "Сбросить", "Reset All": "Сбросить всё", "Reset All Curves": "Сбросить все кривые", @@ -983,25 +1411,33 @@ "Reset all curves: point curves (all channels) and parametric (double-click a channel to reset only that one)": "Сбросить все кривые: точечные (все каналы) и параметрическую (двойной щелчок по каналу сбрасывает только его)", "Reset all edits (Cmd+Shift+R)": "Сбросить все правки (Cmd+Shift+R)", "Reshuffle": "Перемешать", + "Resize Face Box": "Изменить размер рамки лица", "Resize to": "Изменить размер до", "Resolution (ppi)": "Разрешение (ppi)", "Restore": "Восстановить", - "Restore (no shortcut)": "Восстановить (без сочетания)", + "Restore (no shortcut)": "Восстановить (без сочетания клавиш)", "Restore Library from Backup…": "Восстановить библиотеку из резервной копии…", "Restore Version": "Восстановить версию", + "Restore keeps their edits; Import as new deletes the old record first.": "Восстановление сохраняет правки; «Импортировать как новое» сначала удаляет старую запись.", + "Restore them": "Восстановить их", "Retouch": "Ретушь", + "Reverse the order": "Обратный порядок", "Revert": "Вернуть", + "Review": "Проверка", "Review Import": "Проверить импорт", "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Щёлкните фото правой кнопкой → Назначить эталонным фото, затем Вид → Режим эталона (Shift+R)", "Right-click a preset to delete it": "Щёлкните пресет правой кнопкой, чтобы удалить его", "Rights Usage Terms": "Условия использования прав", "Rotate": "Повернуть", + "Rotate Crop": "Поворачивать кадрирование", "Rotate Crop Aspect": "Повернуть пропорции кадра", "Rotate Left": "Повернуть влево", "Rotate Right": "Повернуть вправо", "Roughness": "Шероховатость", "Roundness": "Округлость", + "Rules": "Условия", "SAM 3 download stopped: it resumes where it left off next time.": "Загрузка SAM 3 остановлена: в следующий раз она продолжится с того же места.", + "Sa": "Сб", "Same as raw default": "Как стандарт RAW", "Sample Mask Color": "Взять образец цвета маски", "Sample a colour on the photo": "Взять образец цвета на фото", @@ -1022,18 +1458,26 @@ "Save Point Curve Preset": "Сохранить пресет точечной кривой", "Save Point Curve…": "Сохранить точечную кривую…", "Save as Preset": "Сохранить как пресет", + "Save as Set…": "Сохранить как набор…", "Save as Smart Album…": "Сохранить как смарт-альбом…", + "Save as a new set": "Сохранить как новый набор", "Save as set": "Сохранить как набор", "Save as…": "Сохранить как…", "Save preset": "Сохранить пресет", "Saving Lightroom import index…": "Сохранение индекса импорта Lightroom…", + "Saving metadata to an XMP sidecar another application wrote no longer replaces it (issue #92): LightCraft merges its": "Сохранение метаданных в XMP-файл, записанный другим приложением, больше не заменяет его (проблема #92): LightCraft добавляет свои", "Scale": "Масштаб", + "Scan failed": "Сбой сканирования", "Scanning": "Сканирование", - "Scores stay on the photos: filter or make smart albums with Focus and Best of Similar Shots.": "Оценки остаются на фото: фильтруйте или создавайте смарт-альбомы по Focus и Best of Similar Shots.", + "Scanning folder": "Сканирование папки", + "Scanning…": "Сканирование…", + "Scores stay on the photos: filter or make smart albums with Focus and Best of Similar Shots.": "Оценки остаются на фото: фильтруйте или создавайте смарт-альбомы по полям «Фокус» и «Лучшие из похожих кадров».", "Screen": "Экран", "Scroll menu down": "Прокрутить меню вниз", "Scroll menu up": "Прокрутить меню вверх", + "Search": "Поиск", "Search Photos": "Поиск фото", + "Search albums": "Поиск альбомов", "Search commands or keys": "Поиск команд или клавиш", "Search…": "Поиск…", "Seasons": "Времена года", @@ -1047,32 +1491,47 @@ "Select a photo to edit": "Выберите фото для правки", "Select by": "Выбрать по", "Select by Flag, Rating or Label": "Выбрать по флагу, оценке или метке", + "Select photos to give them keywords.": "Выберите фото, чтобы добавить им ключевые слова.", "Select photos, then choose View → Survey (N)": "Выберите фото, затем Вид → Обзор (N)", "Select two photos, then choose View → Compare (Shift+C)": "Выберите два фото, затем Вид → Сравнение (Shift+C)", "Selected": "Выделено", + "Selecting…": "Выделение…", "Selenium Tone": "Селеновое тонирование", "Send Feedback…": "Отправить отзыв…", + "Separated by commas": "Через запятую", "Sepia Tone": "Сепия", + "Sequence number zero-padded to N digits (1–9)": "Порядковый номер, дополненный нулями до N цифр (1–9)", + "Sequence number, counting from the start number": "Порядковый номер, отсчитываемый от начального числа", "Services": "Службы", + "Set": "Задать", + "Set Album Cover": "Задать обложку альбома", "Set Before": "Назначить «до»", "Set Color Label": "Назначить цветовую метку", "Set Crop": "Задать кадрирование", "Set Develop Values": "Задать значения коррекции", "Set Flag": "Задать флаг", + "Set Image Zoom and Pan": "Задать масштаб и панорамирование изображения", "Set Keyboard Shortcut": "Задать сочетание клавиш", "Set Mask Adjustments": "Задать настройки маски", "Set Point Curve": "Задать точечную кривую", "Set Profile": "Задать профиль", "Set Rating": "Задать оценку", + "Set Top of Stack": "Сделать верхним в стопке", "Set as Album Cover": "Сделать обложкой альбома", "Set as Before": "Назначить как «до»", "Set as Reference Photo": "Назначить эталонным фото", "Set as Target Album": "Назначить целевым альбомом", "Set as Target Album (B adds to it)": "Назначить целевым альбомом (B добавляет в него)", "Set as Top of Stack": "Сделать верхним в стопке", + "Set date & time": "Задать дату и время", + "Set name": "Название набора", "Set the mix from the photo's colours": "Задать микс по цветам фото", + "Set up AI Denoise…": "Настроить ИИ-шумоподавление…", "Settings": "Настройки", + "Settings file problem": "Проблема с файлом настроек", + "Settings files are never quietly reset (issue #103): a damaged `prefs.json`, `presets.json` or `view.json` is kept": "Файлы настроек никогда не сбрасываются незаметно (проблема #103): повреждённый `prefs.json`, `presets.json` или `view.json` сохраняется", "Settings new raw photos start from. Changing them doesn't touch photos already in the library.": "Настройки, с которых начинаются новые RAW-фото. Их изменение не затрагивает фото, уже находящиеся в библиотеке.", + "Settings pasted": "Настройки вставлены", "Settings to include": "Включаемые настройки", "Settings…": "Настройки…", "Shade": "Тень", @@ -1086,7 +1545,10 @@ "Sharpen": "Повысить резкость", "Sharpening": "Резкость", "Sharpness": "Резкость", + "Shift": "Shift", + "Shift by": "Сдвинуть на", "Short Edge": "Короткая сторона", + "Show": "Показать", "Show All": "Показать все", "Show Clipping": "Показать отсечение", "Show Deghost Overlay": "Показать наложение удаления артефактов", @@ -1096,18 +1558,20 @@ "Show Mask Overlay": "Показать наложение маски", "Show Mask Pins": "Показать булавки масок", "Show Original": "Показать оригинал", + "Show Person": "Показать человека", "Show Photo Counts": "Показывать количество фото", "Show Photos with Keyword": "Показать фото с ключевым словом", "Show Pins": "Показать булавки", "Show Source": "Показать источник", "Show Thumbnails": "Показывать миниатюры", - "Show commands without a shortcut": "Показывать команды без сочетаний", + "Show commands without a shortcut": "Показать команды без сочетания клавиш", "Show destination gamut warning": "Показывать предупреждение о выходе за гамму устройства вывода", "Show display gamut warning": "Показывать предупреждение о выходе за гамму дисплея", "Show file names": "Показывать имена файлов", - "Show in Explorer": "Показать в Проводнике", + "Show in Explorer": "Показать в «Проводнике»", "Show in File Manager": "Показать в файловом менеджере", "Show in Finder": "Показать в Finder", + "Show less": "Показать меньше", "Show on Map": "Показать на карте", "Show overlay (O)": "Показать наложение (O)", "Show ratings, flags and edit badges": "Показывать оценки, флаги и значки правок", @@ -1115,6 +1579,7 @@ "Show the template tags; click one to insert it": "Показать теги шаблона; щёлкните по тегу, чтобы вставить его", "Show/Hide Mask": "Показать/скрыть маску", "Show/hide My Photos panel": "Показать/скрыть панель «Мои фото»", + "Shutter Speed": "Выдержка", "Shutter speed": "Выдержка", "Side panels": "Боковые панели", "Sidecar names": "Имена XMP-файлов", @@ -1122,27 +1587,44 @@ "Size (% of short edge)": "Размер (% от короткой стороны)", "Size limit": "Ограничение размера", "Skip": "Пропустить", + "Skipped: files already in the library elsewhere": "Пропущено: файлы, которые уже есть в библиотеке в другом месте", + "Skipped: files that can't be read": "Пропущено: файлы, которые не удаётся прочитать", "Sky": "Небо", "Slideshow": "Слайд-шоу", - "Slideshow · Space pauses · Esc ends": "Слайд-шоу · Пробел — пауза · Esc — выход", + "Slideshow paused": "Слайд-шоу приостановлено", + "Slideshow resumed": "Слайд-шоу возобновлено", + "Slideshow · Space pauses · Esc ends": "Слайд-шоу · Space — пауза · Esc — завершить", + "Slideshow, second window, All Metadata, System Info.": "Слайд-шоу, второе окно, все метаданные, сведения о системе.", + "Smaller, faster catalogs: photos you only looked at in Local (never added, rated or edited) are forgotten once": "Меньшие и более быстрые каталоги: фото, которые вы только просматривали в «Локальном» (никогда не добавляли, не оценивали и не правили), забываются, если", "Smart Album": "Смарт-альбом", "Smart Album Rule Fields": "Поля условий смарт-альбома", "Smart Preview Status": "Состояние смарт-просмотров", "Smart Previews Location": "Расположение смарт-просмотров", + "Smart albums with a rule editor: match all / any / none, nested groups, 26 fields.": "Смарт-альбомы с редактором условий: все / любое / ни одно, вложенные группы, 26 полей.", + "Smart preview available": "Смарт-просмотр доступен", "Smart previews": "Смарт-просмотры", + "Smart previews folder changed": "Папка смарт-просмотров изменена", "Smoothness": "Плавность", "Soft B&W": "Мягкое Ч/Б", "Soft Pastel": "Мягкий пастельный", "Soft Proofing": "Экранная цветопроба", "Soft Skin": "Мягкая кожа", + "Some rules need fixing (Edit Smart Album…):": "Некоторые условия нужно исправить (Изменить смарт-альбом…):", + "Sony ARWs from before about 2017 (RX100, RX100 II–V, RX10, NEX, SLT, ILCE-6000, A7 / A7 II / A7R II and their": "Sony ARW примерно до 2017 года (RX100, RX100 II–V, RX10, NEX, SLT, ILCE-6000, A7 / A7 II / A7R II и родственные", + "Sony ILCE-7M4 downsized lossless ARWs now decode subsampled YCbCr tiles into linear RGB,": "Sony ILCE-7M4: уменьшенные ARW без потерь теперь декодируют плитки YCbCr с субдискретизацией в линейный RGB,", "Sort": "Сортировка", + "Sort Albums A–Z": "Сортировать альбомы А–Я", + "Sort Albums by Name": "Сортировать альбомы по названию", + "Sort Contents A–Z": "Сортировать содержимое А–Я", "Sort by": "Сортировать по", "Source": "Источник", "Source code on GitHub": "Исходный код на GitHub", + "Speed": "Скорость", "Spherical": "Сферическая", "Spiral": "Спираль", "Split Stack": "Разделить стопку", "Spring Fresh": "Весенняя свежесть", + "Square": "Квадрат", "Square Grid": "Квадратная сетка", "Square Grid (G toggles)": "Квадратная сетка (G переключает)", "Square Grid: show file names and formats": "Квадратная сетка: показывать имена и форматы файлов", @@ -1150,37 +1632,55 @@ "Stack photos taken within this time of each other:": "Объединять в стопку фото, снятые с интервалом не более:", "Stacks": "Стопки", "Standard": "Стандартный", + "Star rating (0–5)": "Оценка в звёздах (0–5)", "Start at": "Начать с", "Start number": "Начальный номер", + "Starting": "Запуск", + "Starting…": "Запуск…", "Startup": "Запуск", "State / Province": "Область / регион", "Stop": "Остановить", "Stop Building Previews": "Остановить создание просмотров", "Stop Using as Target Album": "Больше не использовать как целевой альбом", + "Stopped": "Остановлено", + "Stopped by an error (see the log)": "Остановлено из-за ошибки (см. журнал)", "Stopping…": "Остановка…", "Straighten": "Выровнять", "Straighten Tool": "Инструмент «Выпрямление»", "Strong Contrast": "Сильный контраст", "Style": "Стиль", + "Su": "Вс", "Subfolder": "Подпапка", "Subfolder name": "Название подпапки", "Subject": "Главный объект", "Subtract": "Вычесть", "Subtract from this mask": "Вычесть из этой маски", + "Suggest Names for Faces": "Предложить имена для лиц", "Suggestions": "Подсказки", + "Suggests who is in a photo from the faces you have named. Everything stays on your computer.": "Предлагает, кто изображён на фото, по лицам, которым вы уже дали имена. Всё остаётся на вашем компьютере.", "Summer Haze": "Летняя дымка", "Sunday": "Воскресенье", "Survey": "Обзор", "Survey (N)": "Обзор (N)", "Swap": "Поменять", "Swap Aspect (X)": "Поменять пропорции (X)", + "Swap Before and After": "Поменять «до» и «после» местами", "Swap Before and After Settings": "Поменять местами настройки «до» и «после»", "Swap Compare Photos": "Поменять местами фото для сравнения", "Switch light / dark theme": "Переключить светлую/тёмную тему", "Sync Settings": "Синхронизировать настройки", + "Synchronize": "Синхронизировать", + "Synchronize Folder": "Синхронизировать папку", + "Synchronize Folder…": "Синхронизировать папку…", + "Synchronizing folder": "Синхронизация папки", + "Synchronizing…": "Синхронизация…", + "Synonyms": "Синонимы", + "System": "Система", "System Info": "Сведения о системе", "System Info…": "Сведения о системе…", "System default": "Как в системе", + "TZ/ZS series, Leica D-Lux, V-Lux and C-Lux, and the GH6, GH7, G9 II, S5 II and S9 generation. They start from a": "серии TZ/ZS, Leica D-Lux, V-Lux и C-Lux, а также поколение GH6, GH7, G9 II, S5 II и S9. Они начинают с", + "Table": "Таблица", "Tags": "Теги", "Tags: see Tags beside the template. Files are renamed on disk (with their XMP sidecars); existing names get -1, -2…": "Теги: см. «Теги» рядом с шаблоном. Файлы переименовываются на диске (вместе с XMP-файлами); к существующим именам добавляется -1, -2…", "Take a snapshot": "Сделать снимок", @@ -1193,26 +1693,57 @@ "Temperature": "Температура", "Template": "Шаблон", "Temporary session — your library isn't open, and nothing you do here is saved.": "Временный сеанс — ваша библиотека не открыта, и ничего из сделанного здесь не сохраняется.", + "Test Denoise Model": "Проверить модель шумоподавления", + "Test Face Model": "Проверить модель лиц", + "Tested and works on this computer": "Проверено и работает на этом компьютере", "Text": "Текст", "Text direction": "Направление текста", "Texture": "Текстура", - "That key can't be used as a shortcut": "Эту клавишу нельзя использовать в сочетании", + "Th": "Чт", + "That includes the photos in the folders inside it.": "Сюда входят и фото во вложенных папках.", + "That key can't be used as a shortcut": "Эту клавишу нельзя использовать как сочетание", "The SAM 3 model is installed: Object and Describe masks are ready.": "Модель SAM 3 установлена: маски «Объект» и «Описать» готовы.", + "The catalog has a format version (issue #102). Opening a library from an older LightCraft upgrades it; a library": "У каталога есть версия формата (проблема #102). Открытие библиотеки от более старого LightCraft обновляет её; библиотека,", + "The control port (`--control`) closes a connection as soon as it receives anything that isn't a JSON request": "Порт управления (`--control`) закрывает соединение, как только получает что-либо, кроме JSON-запроса", + "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "Модель шумоподавления установлена и используется: ползунок «Количество ИИ-шумоподавления» в разделе «Детализация» теперь работает", "The download didn't work:": "Загрузка не удалась:", + "The graphics card is set up when the next photo is made.": "Видеокарта настраивается при создании следующего фото.", + "The library is up to date with this folder.": "Библиотека соответствует этой папке.", + "The model is kept on this computer only. LightCraft never uploads or shares it.": "Модель хранится только на этом компьютере. LightCraft никогда не выгружает и не распространяет её.", + "The number at the end of the original name (IMG_0042 → 0042)": "Число в конце исходного имени (IMG_0042 → 0042)", + "The original extension is always added; {ext} only puts it inside the name as well.": "Исходное расширение добавляется всегда; {ext} лишь вставляет его ещё и внутрь имени.", "The original files, unchanged, each with an XMP sidecar holding its edits.": "Исходные файлы без изменений, у каждого — XMP-файл с его правками.", "The photo has these settings": "У фото есть эти настройки", + "The processor does the work.": "Работу выполняет процессор.", + "The thumbnail cache only ever counts and deletes its own files (issue #98): a library opened on a folder that": "Кэш миниатюр считает и удаляет только собственные файлы (проблема #98): библиотека, открытая в папке, где", + "Their photos and the keywords below them come together under one keyword.": "Их фото и вложенные ключевые слова объединяются под одним ключевым словом.", "Theme": "Тема", + "There is no model file to install": "Нет файла модели для установки", "They can be restored from Recently Deleted until it is emptied.": "Их можно восстановить из «Недавно удалённых», пока этот раздел не очищен.", + "They move to Recently Deleted and can be restored; no file on disk is touched.": "Они перемещаются в «Недавно удалённые», их можно восстановить; файлы на диске не затрагиваются.", "Thirds": "Правило третей", - "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "Эта сборка пока не умеет скачивать модель. Чтобы установить её вручную, положите model.safetensors, vocab.json и merges.txt из facebook/sam3 от Meta в папку выше: «Объект» и «Описать» заработают, как только файлы окажутся там.", + "This Computer": "Этот компьютер", + "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "Эта сборка пока не может загрузить модель. Чтобы установить её вручную, положите model.safetensors, vocab.json и merges.txt из facebook/sam3 от Meta в папку выше: «Объект» и «Описать» заработают сразу после этого.", + "This build cannot run denoise models.": "Эта сборка не может запускать модели шумоподавления.", + "This build cannot run recognition models: they can be added and chosen, not used.": "Эта сборка не может запускать модели распознавания: их можно добавить и выбрать, но не использовать.", + "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "В этой сборке негде хранить модели шумоподавления, поэтому ИИ-шумоподавление недоступно. (В настольном приложении есть.)", + "This build has nowhere to keep face models, so the model list is not available. (The desktop app has.)": "В этой сборке негде хранить модели лиц, поэтому список моделей недоступен. (В настольном приложении есть.)", + "This file cannot be used yet": "Этот файл пока нельзя использовать", "This folder is not available (drive disconnected?). Smart previews are not built or used until it is back or you choose another folder.": "Эта папка недоступна (диск отключён?). Смарт-просмотры не создаются и не используются, пока она не появится снова или вы не выберете другую папку.", + "This folder isn't there: it was moved or renamed, or its disk isn't connected.": "Этой папки нет: её переместили или переименовали, либо её диск не подключён.", + "This group is empty: add a rule or remove it.": "Эта группа пуста: добавьте условие или удалите её.", + "This is the album you're editing": "Это альбом, который вы редактируете", "This photo can't be opened": "Это фото не удаётся открыть", "Threshold": "Порог", "Thumbnail cache": "Кэш миниатюр", "Thursday": "Четверг", + "Tick the box to accept the model's terms first": "Сначала отметьте согласие с условиями модели", + "Time zone": "Часовой пояс", "Tint": "Оттенок", "Title": "Заголовок", + "Title metadata": "Метаданные заголовка", "Toggle Keyword from Set": "Переключить ключевое слово из набора", + "Toggle Mask": "Переключить маску", "Toggle Section": "Свернуть/развернуть раздел", "Toggle Zoom": "Переключить масштаб", "Tone Curve": "Тоновая кривая", @@ -1222,19 +1753,29 @@ "Treatment": "Обработка", "Triangle": "Треугольник", "Try Again": "Повторить попытку", + "Try Saving Again": "Повторить сохранение", + "Try again": "Повторить попытку", + "Try the graphics card again": "Повторить попытку с видеокартой", + "Tu": "Вт", "Tuesday": "Вторник", "Tungsten": "Лампа накаливания", "Turn Off": "Выключить", + "Turn off": "Выключить", "Turn on Include subfolders, or pick another folder under Local": "Включите «Включая вложенные папки» или выберите другую папку в «Локальном»", "Type": "Тип", "Uncheck All": "Снять все отметки", - "Uncheck the files of this type": "Снять отметку с файлов этого типа", + "Uncheck the files of this type": "Снять отметки с файлов этого типа", "Undo": "Отменить", "Unedited": "Без правок", + "Unedited raws": "Неизменённые RAW", "Unflag": "Снять флаг", "Unflagged": "Без флага", "Ungroup Stack": "Разгруппировать стопку", + "Unknown": "Неизвестно", "Unknown Date": "Дата неизвестна", + "Unknown licence": "Неизвестная лицензия", + "Unknown tags stay as typed — check the preview for a {typo}.": "Неизвестные теги остаются как введены — проверьте предпросмотр на наличие {typo}.", + "Unnamed Faces": "Лица без имён", "Unrated": "Без оценки", "Unreadable": "Не читается", "Update": "Обновить", @@ -1244,13 +1785,16 @@ "Update Version": "Обновить версию", "Update Version with Current Settings": "Обновить версию текущими настройками", "Update with Current Settings": "Обновить текущими настройками", + "Updated the edits saved in the external editor": "Правки, сохранённые во внешнем редакторе, обновлены", "Upright": "Выравнивание (Upright)", "Urban": "Урбан", + "Use": "Использовать", "Use Keyword Set": "Использовать набор ключевых слов", "Use Library Folder": "Использовать папку библиотеки", "Use camera-specific defaults": "Использовать настройки для конкретной камеры", "Use the GPU for rendering": "Использовать GPU для рендеринга", "User Presets": "Пользовательские пресеты", + "Username": "Имя пользователя", "Value": "Значение", "Variance": "Разброс", "Version": "Версия", @@ -1260,8 +1804,11 @@ "Vertical": "По вертикали", "Vertical text": "Вертикальный текст", "Vibrance": "Красочность", + "Video": "Видео", + "Video Duration": "Длительность видео", "Videos": "Видео", "View": "Вид", + "Viewing and sharing": "Просмотр и обмен", "Vignette": "Виньетка", "Vignetting": "Виньетирование", "Vintage": "Винтаж", @@ -1272,12 +1819,16 @@ "Visualize range": "Показать диапазон", "Vivid": "Яркий", "Vivid Pop": "Яркий поп", + "Wait for the import to finish": "Дождитесь завершения импорта", + "Waiting to start…": "Ожидание запуска…", "Warm Film": "Тёплая плёнка", "Warm Glow": "Тёплое сияние", "Warm Print": "Тёплый отпечаток", "Warm Table": "Тёплый стол", "Watched folder": "Отслеживаемая папка", + "Watched-folder auto import; Convert to DNG; Duplicate; Build Standard / 1:1 / Smart Previews.": "Автоимпорт из отслеживаемой папки; конвертация в DNG; дублирование; создание стандартных просмотров / 1:1 / смарт-просмотров.", "Watermark": "Водяной знак", + "We": "Ср", "Wednesday": "Среда", "What's New": "Что нового", "When rated or hovered": "При оценке или наведении", @@ -1289,8 +1840,13 @@ "Width (% of photo)": "Ширина (% от фото)", "Width (px)": "Ширина (px)", "Width × Height": "Ширина × высота", + "Will Export": "Будет экспортировано", + "Will be replaced": "Будет заменено", + "Will be restored": "Будет восстановлено", "Window": "Окно", "Winter Blue": "Зимняя синева", + "Without Saving and Quit (issue #100). It no longer quietly starts a demo session that looked like a reset library and": "Выйти без сохранения (проблема #100). Больше не запускается незаметно демо-сеанс, похожий на сброшенную библиотеку, и", + "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "Работает с RAW-файлами камер со стандартным (Байера) сенсором. Остальные файлы сохраняют обычное шумоподавление.", "XMP Sidecar Preferences": "Настройки XMP-файлов", "XMP sidecars": "XMP-файлы", "Year": "Год", @@ -1301,6 +1857,7 @@ "Yellow label": "Жёлтая метка", "Yes": "Да", "You can close this: the download continues, and resumes if interrupted.": "Это окно можно закрыть: загрузка продолжится и возобновится после обрыва.", + "Your Name": "Ваше имя", "Your library couldn't be opened": "Не удалось открыть вашу библиотеку", "Zoom": "Масштаб", "Zoom 100%": "Масштаб 100%", @@ -1308,287 +1865,208 @@ "Zoom Out": "Уменьшить", "Zoom to Fit": "Масштаб по размеру окна", "Zoom to page level": "Масштаб до уровня страницы", + "`Photos/2026/20260114/20260114_001.jpg`) together with their XMP sidecars; each original leaves the card only after its": "`Photos/2026/20260114/20260114_001.jpg`) вместе с XMP-файлами; каждый оригинал покидает карту только после того, как его", + "`img_1-1.JPG`; on case-insensitive volumes the case change still goes through.": "`img_1-1.JPG`; на томах без учёта регистра смена регистра всё равно выполняется.", + "a full drive) no longer counts as built and Build Smart Previews replaces it.": "полный диск) больше не считается созданным, и «Создать смарт-просмотры» заменяет его.", + "a literal %": "буквальный символ %", + "a removed photo": "удалённое фото", + "afterwards (issue #101): the partial write is cut off before LightCraft retries, so the next launch replays every": "после этого (проблема #101): частичная запись отсекается до повторной попытки LightCraft, поэтому следующий запуск воспроизводит каждое", + "again. Change the period (or turn it off) in Settings → Performance.": "снова. Измените период (или отключите) в Настройки → Производительность.", + "against the library's originals no longer scans the whole library when nothing is at that path.": "по оригиналам библиотеки больше не сканирует всю библиотеку, когда по этому пути ничего нет.", + "all": "все", + "already has a `thumbs/` folder of other pictures no longer loses them when the cache is trimmed or cleared.": "уже есть папка `thumbs/` с другими изображениями, больше не теряет их при усечении или очистке кэша.", "and": "и", - "comma, separated": "через запятую", - "memory only": "только в памяти", - "none": "нет", - "not available here": "здесь недоступно", - "of the following rules:": "из следующих условий:", - "saved in": "сохранено в", - "this photo": "это фото", - "− harder border · + softer border": "− жёстче край · + мягче край", - "Back to this person": "Back to this person", - "AI Denoise": "AI Denoise", - "AI Denoise Model": "AI Denoise Model", - "AI Denoise is off: press Use on a model.": "AI Denoise is off: press Use on a model.", - "AI Denoise needs a model. Install one in Settings.": "AI Denoise needs a model. Install one in Settings.", - "Accept & Download": "Accept & Download", - "Already installed. Remove it before installing a replacement.": "Already installed. Remove it before installing a replacement.", - "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.": "Automatic uses the graphics card where it is faster than the processor: both are timed on this computer the first time a photo is made.", - "Cache": "Cache", - "Checking and installing the model…": "Checking and installing the model…", - "Checking…": "Checking…", - "Clear cache": "Clear cache", - "Denoise Models": "Denoise Models", - "Denoise Settings": "Denoise Settings", - "Denoise all photos with an Amount": "Denoise all photos with an Amount", - "Downloaded and checked. Installing…": "Downloaded and checked. Installing…", - "Failed its last test": "Failed its last test", - "Graphics card": "Graphics card", - "I have read these terms and accept them for my own use": "I have read these terms and accept them for my own use", - "Inspect Denoise Model": "Inspect Denoise Model", - "Install Denoise Model": "Install Denoise Model", - "Install from file…": "Install from file…", - "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.": "Install your own ONNX model with denoise-model.json beside it. This build offers no model downloads.", - "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).": "Its denoised picture is not made yet, and pictures are not made on their own (Settings, AI Denoise).", - "LightCraft does not know this model, so it assumed:": "LightCraft does not know this model, so it assumed:", - "Limit": "Limit", - "Make it now": "Make it now", - "Make the denoised picture of the photos I am looking at": "Make the denoised picture of the photos I am looking at", - "Models": "Models", - "Next up for AI Denoise…": "Next up for AI Denoise…", - "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.": "Nothing is fetched until you accept. The model is kept on this computer only; LightCraft never uploads or shares it.", - "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.": "Only photos whose AI Denoise amount is above 0 are made. Exports always make theirs if it is missing.", - "Open page": "Open page", - "Open the model's page": "Open the model's page", - "Opens the model's own page in your browser, to read about it or get the file yourself.": "Opens the model's own page in your browser, to read about it or get the file yourself.", - "Processor": "Processor", - "Set up AI Denoise…": "Set up AI Denoise…", - "Speed": "Speed", - "Starting…": "Starting…", - "Tested and works on this computer": "Tested and works on this computer", - "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works": "The denoise model is installed and in use: the AI Denoise Amount slider under Detail now works", - "The graphics card is set up when the next photo is made.": "The graphics card is set up when the next photo is made.", - "The model is kept on this computer only. LightCraft never uploads or shares it.": "The model is kept on this computer only. LightCraft never uploads or shares it.", - "The processor does the work.": "The processor does the work.", - "There is no model file to install": "There is no model file to install", - "This build cannot run denoise models.": "This build cannot run denoise models.", - "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)": "This build has nowhere to keep denoise models, so AI Denoise is not available. (The desktop app has.)", - "This file cannot be used yet": "This file cannot be used yet", - "Tick the box to accept the model's terms first": "Tick the box to accept the model's terms first", - "Try again": "Try again", - "Try the graphics card again": "Try the graphics card again", - "Turn off": "Turn off", - "Unknown licence": "Unknown licence", - "Use": "Use", - "Waiting to start…": "Waiting to start…", - "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.": "Works on raw files from cameras with a standard (Bayer) sensor. Other files keep their normal noise reduction.", - "cached picture": "cached picture", - "cached pictures": "cached pictures", - "commercial use allowed": "commercial use allowed", - "denoiseModel.accept": "denoiseModel.accept", - "in use": "in use", - "installed": "installed", - "makes AI Denoise available": "makes AI Denoise available", - "non-commercial use only": "non-commercial use only", - "photo has its denoised picture.": "photo has its denoised picture.", - "photo waiting": "photo waiting", - "photos have their denoised picture.": "photos have their denoised picture.", - "photos waiting": "photos waiting", - "starts using it for AI Denoise": "starts using it for AI Denoise", - "terms unclear": "terms unclear", - "the graphics card": "the graphics card", - "the graphics card is not used": "the graphics card is not used", - "Cancel Denoise": "Cancel Denoise", - "Cancel Denoise Model Download": "Cancel Denoise Model Download", - "Choose Denoise Model": "Choose Denoise Model", - "Clear Denoise Cache": "Clear Denoise Cache", - "Denoise Model Downloads": "Denoise Model Downloads", - "Denoise Photos": "Denoise Photos", - "Denoise Pump": "Denoise Pump", - "Denoise Status": "Denoise Status", - "Download Denoise Model": "Download Denoise Model", - "Remove Denoise Model": "Remove Denoise Model", - "Test Denoise Model": "Test Denoise Model", - "AI Denoise is enabled on the requesting photo": "AI Denoise is enabled on the requesting photo", - "After installation, AI Denoise will turn on for the requesting photo.": "After installation, AI Denoise will turn on for the requesting photo.", - "Cancel pending action": "Cancel pending action", - "Install a denoise model to enable AI Denoise on this photo": "Install a denoise model to enable AI Denoise on this photo", - "Pending photo action cancelled": "Pending photo action cancelled", - "Pending photo action cancelled because its library or photo changed": "Pending photo action cancelled because its library or photo changed", - "ONNX model": "ONNX model", - "commercial use subject to GPL conditions": "commercial use subject to GPL conditions", - "Decrease Rating": "Уменьшить оценку", - "Increase Rating": "Увеличить оценку", - "Click to type a value": "Щёлкните, чтобы ввести значение", - "Angle": "Угол", - "Raw": "RAW", - "Video": "Видео", - "Copyrighted": "Защищено авторским правом", - "Public Domain": "Общественное достояние", - "Unknown": "Неизвестно", - "Landscape (wide)": "Альбомная", - "Portrait (tall)": "Книжная", - "Square": "Квадратная", - "In Color": "Цветное", - "No Label": "Без метки", - "Choose an album…": "Выберите альбом…", - "No albums yet": "Альбомов пока нет", - "Fix the marked rules to save this album.": "Исправьте отмеченные условия, чтобы сохранить альбом.", - "Some rules need fixing (Edit Smart Album…):": "Некоторые условия нужно исправить (Изменить смарт-альбом…):", - "isn't a field rules know": "неизвестное поле", + "and checks the file before using it. Masks keep their selection, so they render and export without the model.": "и проверяет файл перед использованием. Маски сохраняют выделение, поэтому рендерятся и экспортируются без модели.", + "and every imported photo as one zip, and File ▸ Restore Library from Backup… brings it back (the current library": "и каждое импортированное фото одним zip, а Файл → Восстановить библиотеку из резервной копии… возвращает его (текущая библиотека", + "and never freezes the window. It is optional and not part of LightCraft (Meta's SAM License, about 3.4 GB): the": "и никогда не замораживает окно. Она необязательна и не входит в LightCraft (лицензия SAM от Meta, около 3,4 ГБ):", + "any": "любое", + "are empty": "пустые", + "aren't empty": "не пустые", + "as `.corrupt-