mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
LR-EXP-DIM: Width & Height limits each edge to its own number (#1620)
Lightroom's Resize to Fit has two boxes: Dimensions (the larger number to the long edge, whichever way round the photo is) and Width & Height (the width limited to W and the height to H as the photo stands). LightCraft had only the first, labelled "Width × Height". Add `ResizeMode::WidthHeight`, `fit: widthHeight` beside `width` + `height` in app.export, the mode in the Export dialog (the old one is now called Dimensions), and the docs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
31a677fe3d
commit
780476de1c
@@ -82,7 +82,11 @@ pub enum ResizeMode {
|
||||
Width,
|
||||
/// `value` = height in pixels.
|
||||
Height,
|
||||
/// Fit inside `value × height` pixels, whichever way round the photo is (long edge ≤ the larger).
|
||||
/// Fit inside `value` wide × `height` tall as the photo stands: width ≤ `value`, height ≤
|
||||
/// `height` (Lightroom's "Width & Height").
|
||||
WidthHeight,
|
||||
/// Fit inside `value × height` pixels, whichever way round the photo is (long edge ≤ the
|
||||
/// larger; Lightroom's "Dimensions").
|
||||
Dimensions,
|
||||
/// `value` = megapixels.
|
||||
Megapixels,
|
||||
@@ -96,7 +100,7 @@ pub enum ResizeMode {
|
||||
pub struct Resize {
|
||||
pub mode: ResizeMode,
|
||||
pub value: f32,
|
||||
/// Second dimension for [`ResizeMode::Dimensions`].
|
||||
/// Second dimension for [`ResizeMode::Dimensions`] and [`ResizeMode::WidthHeight`].
|
||||
pub height: u32,
|
||||
/// Never upscale past the photo's own (cropped) size.
|
||||
pub dont_enlarge: bool,
|
||||
@@ -127,6 +131,7 @@ impl Resize {
|
||||
ResizeMode::ShortEdge => v / short,
|
||||
ResizeMode::Width => v / w,
|
||||
ResizeMode::Height => v / h,
|
||||
ResizeMode::WidthHeight => (v / w).min(self.height as f64 / h),
|
||||
ResizeMode::Dimensions => {
|
||||
let (a, b) = (v, self.height as f64);
|
||||
(a.max(b) / long).min(a.min(b) / short)
|
||||
@@ -721,6 +726,7 @@ pub const OPTION_PARAMS: &[&str] = &[
|
||||
"height",
|
||||
"megapixels",
|
||||
"percent",
|
||||
"fit",
|
||||
"dontEnlarge",
|
||||
"ppi",
|
||||
"limitKb",
|
||||
@@ -894,6 +900,7 @@ impl ExportOptions {
|
||||
"longEdge" | "shortEdge" | "width" | "height" | "megapixels" | "percent" => {
|
||||
num(k, v, 0.0, 1.0e9)?;
|
||||
}
|
||||
"fit" => one_of(k, v, &["dimensions", "widthHeight"])?,
|
||||
"resize" => {
|
||||
let Some(r) = v.as_object() else { return Err(bad(format!("`{k}` must be an object {{mode, value, height?, dontEnlarge?}}"))) };
|
||||
if let Some(x) = r.keys().find(|x| !matches!(x.as_str(), "mode" | "value" | "height" | "dontEnlarge")) {
|
||||
@@ -1075,8 +1082,12 @@ impl ExportOptions {
|
||||
return (r.value > 0.0).then_some(Resize { dont_enlarge: p.get("dontEnlarge").and_then(Value::as_bool).unwrap_or(r.dont_enlarge), ..r });
|
||||
}
|
||||
let r = |mode, value: f64| Resize { mode, value: value.min(65_535.0) as f32, height: 0, dont_enlarge };
|
||||
// `fit: widthHeight` binds each edge to its own number; the default fits either orientation
|
||||
let bound = p.get("fit").and_then(Value::as_str).is_some_and(|fit| fit.eq_ignore_ascii_case("widthHeight"));
|
||||
Some(match (f("width"), f("height")) {
|
||||
(Some(w), Some(h)) => Resize { height: h.min(65_535.0) as u32, ..r(ResizeMode::Dimensions, w) },
|
||||
(Some(w), Some(h)) => {
|
||||
Resize { height: h.min(65_535.0) as u32, ..r(if bound { ResizeMode::WidthHeight } else { ResizeMode::Dimensions }, w) }
|
||||
}
|
||||
(Some(w), None) => r(ResizeMode::Width, w),
|
||||
(None, Some(h)) => r(ResizeMode::Height, h),
|
||||
(None, None) => {
|
||||
@@ -2813,6 +2824,13 @@ mod tests {
|
||||
assert_eq!(size(json!({"width": 1000, "height": 800}), 6000.0, 4000.0), Some((1000, 667)));
|
||||
assert_eq!(size(json!({"width": 1000, "height": 800}), 4000.0, 6000.0), Some((667, 1000)));
|
||||
assert_eq!(size(json!({"width": 1000, "height": 500}), 6000.0, 4000.0), Some((750, 500)));
|
||||
assert_eq!(size(json!({"width": 270, "height": 337, "fit": "dimensions"}), 480.0, 320.0), Some((337, 225)));
|
||||
// Width & Height (`fit: widthHeight`) binds each edge to its own number: W limits the width, H the height (#1620)
|
||||
assert_eq!(size(json!({"width": 270, "height": 337, "fit": "widthHeight"}), 480.0, 320.0), Some((270, 180)));
|
||||
assert_eq!(size(json!({"width": 270, "height": 337, "fit": "widthHeight"}), 320.0, 480.0), Some((225, 337)));
|
||||
assert_eq!(size(json!({"width": 240, "height": 300, "fit": "widthHeight"}), 480.0, 320.0), Some((240, 160)));
|
||||
// `fit` only matters with both numbers
|
||||
assert_eq!(size(json!({"width": 1000, "fit": "widthHeight"}), 6000.0, 4000.0), Some((1000, 667)));
|
||||
let (w, h) = size(json!({"megapixels": 6}), 6000.0, 4000.0).unwrap();
|
||||
assert!(((w * h) as f64 - 6e6).abs() < 6e6 * 0.002, "{w}×{h}");
|
||||
assert_eq!(size(json!({"percent": 25}), 6000.0, 4000.0), Some((1500, 1000)));
|
||||
@@ -2827,6 +2845,8 @@ mod tests {
|
||||
let r = Resize { mode: ResizeMode::ShortEdge, value: 720.0, height: 0, dont_enlarge: false };
|
||||
let back = ExportOptions::from_json(&ExportOptions::resize_json(Some(&r))).resize;
|
||||
assert_eq!(back, Some(r));
|
||||
let r = Resize { mode: ResizeMode::WidthHeight, value: 270.0, height: 337, dont_enlarge: true };
|
||||
assert_eq!(ExportOptions::from_json(&ExportOptions::resize_json(Some(&r))).resize, Some(r));
|
||||
assert_eq!(ExportOptions::from_json(&ExportOptions::resize_json(None)).resize, None);
|
||||
assert_eq!(ExportOptions::from_json(&json!({"ppi": 300})).ppi, 300);
|
||||
// never larger than the encoders' limit, never empty
|
||||
@@ -2898,6 +2918,7 @@ mod tests {
|
||||
mode(ResizeMode::Width, value, 0),
|
||||
mode(ResizeMode::Height, value, 0),
|
||||
mode(ResizeMode::Dimensions, value, 1365),
|
||||
mode(ResizeMode::WidthHeight, value, 1365),
|
||||
mode(ResizeMode::Megapixels, value / 1000.0, 0),
|
||||
mode(ResizeMode::Percent, value / 100.0, 0),
|
||||
] {
|
||||
@@ -3194,6 +3215,7 @@ mod tests {
|
||||
assert!(err(json!({"bitDepth": 12})).contains("`bitDepth` must be 8, 10, 16 or 32"));
|
||||
assert!(err(json!({"colorSpace": "cmyk"})).contains("`colorSpace` must be one of"));
|
||||
assert!(err(json!({"dontEnlarge": "no"})).contains("`dontEnlarge` must be true or false"));
|
||||
assert!(err(json!({"fit": "sideways"})).contains("`fit` must be one of dimensions|widthHeight"));
|
||||
assert!(err(json!({"resize": {"mode": "longEdge", "valeu": 10}})).contains("did you mean `value`"));
|
||||
assert!(err(json!({"resize": {"mode": "diagonal", "value": 10}})).contains("`resize`:"));
|
||||
assert!(err(json!({"ids": [1, "x"]})).contains("`ids` must be an array of photo ids"));
|
||||
|
||||
@@ -135,7 +135,7 @@ impl Backend for Headless {
|
||||
"engine.commands" => {
|
||||
let mut v: Vec<Value> = 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/.jxl/.psd/.dng) | dir?, ids?, preset?, format?: jpeg|png|tiff|webp|avif|jxl|psd|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 / PNG / JPEG XL, float TIFF), sharpen?: none|screen|matte|glossy, sharpenAmount?: low|standard|high, metadata?: all|allExceptCamera|copyright|none, removeLocation?, naming?, startNumber?, destinationMode?: specificFolder|sameAsOriginal (sameAsOriginal = each photo into its original's folder; `dir` unused), subfolder?, conflict?: unique|overwrite|skip, tiffCompression?: none|lzw|zip, dngCompression?: lossless|deflate|uncompressed, addToPhotos?: bool (macOS: then add the files written to Apple Photos; outcome in `applePhotos`), photosAlbum?: top-level album name (made when missing), 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, insetX? (margin from the left/right edge, 0..0.4 of the short edge; 0.025), insetY? (from the top/bottom edge; 0.025), inset? (sets both), rotation?: 0|90|180|270 (degrees clockwise; 0), 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/.psd/.dng) | dir?, ids?, preset?, format?: jpeg|png|tiff|webp|avif|jxl|psd|dng|original, longEdge?|shortEdge?|width?|height?|megapixels?|percent? (default longEdge 3000; longEdge 0 = full size), fit?: dimensions|widthHeight (with width + height: dimensions fits either orientation, widthHeight limits the width to `width` and the height to `height`), 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 / PNG / JPEG XL, float TIFF), sharpen?: none|screen|matte|glossy, sharpenAmount?: low|standard|high, metadata?: all|allExceptCamera|copyright|none, removeLocation?, naming?, startNumber?, destinationMode?: specificFolder|sameAsOriginal (sameAsOriginal = each photo into its original's folder; `dir` unused), subfolder?, conflict?: unique|overwrite|skip, tiffCompression?: none|lzw|zip, dngCompression?: lossless|deflate|uncompressed, addToPhotos?: bool (macOS: then add the files written to Apple Photos; outcome in `applePhotos`), photosAlbum?: top-level album name (made when missing), 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, insetX? (margin from the left/right edge, 0..0.4 of the short edge; 0.025), insetY? (from the top/bottom edge; 0.025), inset? (sets both), rotation?: 0|90|180|270 (degrees clockwise; 0), 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))
|
||||
}
|
||||
|
||||
@@ -243,8 +243,9 @@ pub fn helper_tools(has_ui: bool) -> Vec<Value> {
|
||||
"format": {"type": "string", "enum": ["jpeg", "png", "tiff", "psd", "webp", "avif", "jxl", "original", "dng", "rendereddng"], "description": "psd = a flat PSD (no layers); rendereddng = Render to DNG, a linear 16-bit float DNG of the rendered photo with the edit baked in; 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)"},
|
||||
"width": {"type": "integer", "description": "Output width in px (with height: fit inside width × height, either orientation, unless `fit` says otherwise)"},
|
||||
"height": {"type": "integer", "description": "Output height in px"},
|
||||
"fit": {"type": "string", "enum": ["dimensions", "widthHeight"], "description": "With width + height: dimensions (default) fits either orientation (the larger number goes to the long edge); widthHeight limits the width to `width` and the height to `height` as the photo stands"},
|
||||
"megapixels": {"type": "number", "description": "Output size in megapixels"},
|
||||
"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)"},
|
||||
@@ -731,6 +732,7 @@ pub fn call_tool(b: &mut dyn Backend, name: &str, args: &Value) -> ToolResult {
|
||||
"height",
|
||||
"megapixels",
|
||||
"percent",
|
||||
"fit",
|
||||
"dontEnlarge",
|
||||
"ppi",
|
||||
"quality",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"Adding": "Přidávání",
|
||||
"Adding Lightroom photos…": "Přidávání fotografií z Lightroom…",
|
||||
"Adding to Apple Photos": "Přidávání do Apple Photos",
|
||||
"Dimensions": "Rozměry",
|
||||
"No graphics processor found: LightCraft renders on the CPU, which is much slower. Details in Help → System Info.": "Nebyl nalezen grafický procesor: LightCraft vykresluje na CPU, což je mnohem pomalejší. Podrobnosti najdete v nabídce Nápověda → Informace o systému.",
|
||||
"On NixOS, start it from `nix develop`.": "V NixOS ho spusťte z `nix develop`.",
|
||||
"Files with the same content as another file in this import: only the first is imported.": "Soubory se stejným obsahem jako jiný soubor v tomto importu: importuje se jen první z nich.",
|
||||
@@ -1873,7 +1874,7 @@
|
||||
"Width": "Šířka",
|
||||
"Width (% of photo)": "Šířka (% fotografie)",
|
||||
"Width (px)": "Šířka (px)",
|
||||
"Width × Height": "Šířka × výška",
|
||||
"Width & Height": "Šířka & výška",
|
||||
"Will Export": "Bude exportováno",
|
||||
"Will be replaced": "Bude nahrazeno",
|
||||
"Will be restored": "Bude obnoveno",
|
||||
|
||||
@@ -431,6 +431,7 @@
|
||||
"Detail (D)": "Detail (D)",
|
||||
"Details": "Details",
|
||||
"Diagonal": "Diagonal",
|
||||
"Dimensions": "Abmessungen",
|
||||
"Discard Preview Cache": "Vorschau-Cache verwerfen",
|
||||
"Display P3": "Display P3",
|
||||
"Display name": "Anzeigename",
|
||||
@@ -1644,7 +1645,7 @@
|
||||
"Width": "Breite",
|
||||
"Width (% of photo)": "Breite (% des Fotos)",
|
||||
"Width (px)": "Breite (px)",
|
||||
"Width × Height": "Breite × Höhe",
|
||||
"Width & Height": "Breite & Höhe",
|
||||
"Will be replaced": "Wird ersetzt",
|
||||
"Will be restored": "Wird wiederhergestellt",
|
||||
"Window": "Fenster",
|
||||
|
||||
@@ -420,6 +420,7 @@
|
||||
"Detect Faces": "Detectar caras",
|
||||
"Detect Faces (Photo menu) needs it: a small one-time download.": "Detectar caras (menú Foto) lo necesita: una pequeña descarga única.",
|
||||
"Diagonal": "Diagonal",
|
||||
"Dimensions": "Dimensiones",
|
||||
"Discard Preview Cache": "Descartar caché de vistas previas",
|
||||
"Display P3": "Display P3",
|
||||
"Display name": "Nombre visible",
|
||||
@@ -1749,7 +1750,7 @@
|
||||
"Width": "Ancho",
|
||||
"Width (% of photo)": "Ancho (% de la foto)",
|
||||
"Width (px)": "Ancho (px)",
|
||||
"Width × Height": "Ancho × alto",
|
||||
"Width & Height": "Ancho & alto",
|
||||
"Will be replaced": "Se reemplazará",
|
||||
"Will be restored": "Se restaurará",
|
||||
"Window": "Ventana",
|
||||
|
||||
@@ -403,6 +403,7 @@
|
||||
"Detail (D)": "Détail (D)",
|
||||
"Details": "Détails",
|
||||
"Diagonal": "Diagonale",
|
||||
"Dimensions": "Dimensions",
|
||||
"Discard Preview Cache": "Supprimer le cache des aperçus",
|
||||
"Display P3": "Display P3",
|
||||
"Display name": "Nom affiché",
|
||||
@@ -1571,7 +1572,7 @@
|
||||
"Width": "Largeur",
|
||||
"Width (% of photo)": "Largeur (% de la photo)",
|
||||
"Width (px)": "Largeur (px)",
|
||||
"Width × Height": "Largeur × hauteur",
|
||||
"Width & Height": "Largeur & hauteur",
|
||||
"Will be replaced": "Sera remplacée",
|
||||
"Will be restored": "Sera restaurée",
|
||||
"Window": "Fenêtre",
|
||||
|
||||
@@ -389,6 +389,7 @@
|
||||
"Detail (D)": "Dettaglio (D)",
|
||||
"Details": "Dettagli",
|
||||
"Diagonal": "Diagonale",
|
||||
"Dimensions": "Dimensioni",
|
||||
"Discard Preview Cache": "Elimina cache delle anteprime",
|
||||
"Display P3": "Display P3",
|
||||
"Display name": "Nome visualizzato",
|
||||
@@ -1545,7 +1546,7 @@
|
||||
"Width": "Larghezza",
|
||||
"Width (% of photo)": "Larghezza (% della foto)",
|
||||
"Width (px)": "Larghezza (px)",
|
||||
"Width × Height": "Larghezza × altezza",
|
||||
"Width & Height": "Larghezza & altezza",
|
||||
"Will be replaced": "Verrà sostituita",
|
||||
"Will be restored": "Verrà ripristinata",
|
||||
"Window": "Finestra",
|
||||
|
||||
@@ -391,6 +391,7 @@
|
||||
"Detail (D)": "詳細(D)",
|
||||
"Details": "詳細",
|
||||
"Diagonal": "対角線",
|
||||
"Dimensions": "寸法",
|
||||
"Discard Preview Cache": "プレビューキャッシュを破棄",
|
||||
"Display P3": "Display P3",
|
||||
"Display name": "表示名",
|
||||
@@ -1499,7 +1500,7 @@
|
||||
"Width": "幅",
|
||||
"Width (% of photo)": "幅(写真に対する%)",
|
||||
"Width (px)": "幅(px)",
|
||||
"Width × Height": "幅 × 高さ",
|
||||
"Width & Height": "幅 & 高さ",
|
||||
"Will be replaced": "置き換えられます",
|
||||
"Will be restored": "復元されます",
|
||||
"Window": "ウィンドウ",
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"Detail (D)": "Szczegóły (D)",
|
||||
"Details": "Szczegóły",
|
||||
"Diagonal": "Przekątna",
|
||||
"Dimensions": "Wymiary",
|
||||
"Discard Preview Cache": "Odrzuć pamięć podręczną podglądów",
|
||||
"Display P3": "Display P3",
|
||||
"Display name": "Nazwa wyświetlana",
|
||||
@@ -1541,7 +1542,7 @@
|
||||
"Width": "Szerokość",
|
||||
"Width (% of photo)": "Szerokość (% zdjęcia)",
|
||||
"Width (px)": "Szerokość (px)",
|
||||
"Width × Height": "Szerokość × wysokość",
|
||||
"Width & Height": "Szerokość & wysokość",
|
||||
"Will be replaced": "Zostanie zastąpiony",
|
||||
"Will be restored": "Zostanie przywrócony",
|
||||
"Window": "Okno",
|
||||
|
||||
@@ -411,6 +411,7 @@
|
||||
"Detail (D)": "Detalhe (D)",
|
||||
"Details": "Detalhes",
|
||||
"Diagonal": "Diagonal",
|
||||
"Dimensions": "Dimensões",
|
||||
"Discard Preview Cache": "Descartar cache de prévias",
|
||||
"Display P3": "Display P3",
|
||||
"Display name": "Nome de exibição",
|
||||
@@ -1594,7 +1595,7 @@
|
||||
"Width": "Largura",
|
||||
"Width (% of photo)": "Largura (% da foto)",
|
||||
"Width (px)": "Largura (px)",
|
||||
"Width × Height": "Largura × altura",
|
||||
"Width & Height": "Largura & altura",
|
||||
"Will be replaced": "Será substituída",
|
||||
"Will be restored": "Será restaurada",
|
||||
"Window": "Janela",
|
||||
|
||||
@@ -347,6 +347,7 @@
|
||||
"Detail (D)": "Детализация (D)",
|
||||
"Details": "Подробности",
|
||||
"Diagonal": "По диагонали",
|
||||
"Dimensions": "Размеры",
|
||||
"Discard Preview Cache": "Удалить кэш просмотров",
|
||||
"Display P3": "Display P3",
|
||||
"Display theme": "Тема оформления",
|
||||
@@ -1339,7 +1340,7 @@
|
||||
"Width": "Ширина",
|
||||
"Width (% of photo)": "Ширина (% от фото)",
|
||||
"Width (px)": "Ширина (px)",
|
||||
"Width × Height": "Ширина × высота",
|
||||
"Width & Height": "Ширина & высота",
|
||||
"Window": "Окно",
|
||||
"Winter Blue": "Зимняя синева",
|
||||
"Wrote the XMP sidecars that were waiting": "Ожидавшие XMP-файлы записаны",
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"Deselect All": "Зняти всі виділення",
|
||||
"Detail": "Деталі",
|
||||
"Details": "Подробиці",
|
||||
"Dimensions": "Розміри",
|
||||
"Display P3": "Display P3",
|
||||
"Display name": "Відображуване ім’я",
|
||||
"Distortion": "Спотворення",
|
||||
@@ -1528,7 +1529,7 @@
|
||||
"White Balance Selector (W)": "Вибір балансу білого (W)",
|
||||
"Width (% of photo)": "Ширина (% фото)",
|
||||
"Width (px)": "Ширина (пікселів)",
|
||||
"Width × Height": "Ширина × висота",
|
||||
"Width & Height": "Ширина & висота",
|
||||
"Will be replaced": "Буде замінено",
|
||||
"Will be restored": "Буде відновлено",
|
||||
"Winter Blue": "Зимова синява",
|
||||
|
||||
@@ -409,6 +409,7 @@
|
||||
"Detail (D)": "细节 (D)",
|
||||
"Details": "详细信息",
|
||||
"Diagonal": "对角线",
|
||||
"Dimensions": "尺寸",
|
||||
"Discard Preview Cache": "丢弃预览缓存",
|
||||
"Display P3": "Display P3",
|
||||
"Display name": "显示名称",
|
||||
@@ -1575,7 +1576,7 @@
|
||||
"Width": "宽度",
|
||||
"Width (% of photo)": "宽度(照片百分比)",
|
||||
"Width (px)": "宽度 (px)",
|
||||
"Width × Height": "宽度 × 高度",
|
||||
"Width & Height": "宽度 & 高度",
|
||||
"Will be replaced": "将替换",
|
||||
"Will be restored": "将恢复",
|
||||
"Window": "窗口",
|
||||
|
||||
@@ -409,6 +409,7 @@
|
||||
"Detail (D)": "詳細檢視(D)",
|
||||
"Details": "詳細資訊",
|
||||
"Diagonal": "對角線",
|
||||
"Dimensions": "尺寸",
|
||||
"Discard Preview Cache": "捨棄預覽快取",
|
||||
"Display P3": "Display P3",
|
||||
"Display name": "顯示名稱",
|
||||
@@ -1575,7 +1576,7 @@
|
||||
"Width": "寬度",
|
||||
"Width (% of photo)": "寬度(照片百分比)",
|
||||
"Width (px)": "寬度(像素)",
|
||||
"Width × Height": "寬度 × 高度",
|
||||
"Width & Height": "寬度 & 高度",
|
||||
"Will be replaced": "將取代",
|
||||
"Will be restored": "將還原",
|
||||
"Window": "視窗",
|
||||
|
||||
@@ -2013,12 +2013,13 @@ const PPI: ControlSpec = spec("export.ppi", "Resolution (ppi)", 1.0, 1200.0, 240
|
||||
/// Image Sizing: full size, or a resize mode and its value(s), don't enlarge, ppi.
|
||||
fn export_size(ui: &mut egui::Ui, full: &mut bool, r: &mut lightcraft_engine::export::Resize, ppi: &mut u16) {
|
||||
use lightcraft_engine::export::ResizeMode as R;
|
||||
const MODES: [(R, &str); 7] = [
|
||||
const MODES: [(R, &str); 8] = [
|
||||
(R::LongEdge, "Long Edge"),
|
||||
(R::ShortEdge, "Short Edge"),
|
||||
(R::Width, "Width"),
|
||||
(R::Height, "Height"),
|
||||
(R::Dimensions, "Width × Height"),
|
||||
(R::WidthHeight, "Width & Height"),
|
||||
(R::Dimensions, "Dimensions"),
|
||||
(R::Megapixels, "Megapixels"),
|
||||
(R::Percent, "Percentage"),
|
||||
];
|
||||
@@ -2042,7 +2043,7 @@ fn export_size(ui: &mut egui::Ui, full: &mut bool, r: &mut lightcraft_engine::ex
|
||||
_ if matches!(before, R::Megapixels | R::Percent) => 2048.0,
|
||||
_ => r.value,
|
||||
};
|
||||
if r.mode == R::Dimensions && r.height == 0 {
|
||||
if matches!(r.mode, R::Dimensions | R::WidthHeight) && r.height == 0 {
|
||||
r.height = r.value as u32;
|
||||
}
|
||||
}
|
||||
@@ -2051,13 +2052,13 @@ fn export_size(ui: &mut egui::Ui, full: &mut bool, r: &mut lightcraft_engine::ex
|
||||
let spec = match r.mode {
|
||||
R::Megapixels => &SIZE_MP,
|
||||
R::Percent => &SIZE_PCT,
|
||||
R::Dimensions => &SIZE_W,
|
||||
R::Dimensions | R::WidthHeight => &SIZE_W,
|
||||
_ => &SIZE_PX,
|
||||
};
|
||||
if num(ui, spec, &mut v) {
|
||||
r.value = v as f32;
|
||||
}
|
||||
if r.mode == R::Dimensions {
|
||||
if matches!(r.mode, R::Dimensions | R::WidthHeight) {
|
||||
let mut h = r.height as f64;
|
||||
if num(ui, &SIZE_H, &mut h) {
|
||||
r.height = h as u32;
|
||||
|
||||
+1
-1
@@ -176,7 +176,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`. The crop always ends inside the photo: an `angle` alone keeps the crop if it still fits, else shrinks it to the largest of its aspect that does |
|
||||
| `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` / `.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 |
|
||||
| `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 (Lightroom's Dimensions); with `fit: "widthHeight"` they limit the width and the height as the photo stands (Width & Height); `dontEnlarge` defaults to true |
|
||||
|
||||
`get_develop {id}` and `render_photo {id}` use that photo without changing the selection.
|
||||
`set_develop`, `crop` and `export` with a supplied photo `id` end the current edit interaction,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Lightroom parity checklist
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (LRC-PRINT-PAGE: Print Page Options) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** patch (LR-EXP-DIM: Width & Height box, #1620; before: minor (LRC-PRINT-PAGE: Print Page Options)) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
|
||||
The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we
|
||||
have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good
|
||||
@@ -391,7 +391,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
|---|---|---|---|---|---|
|
||||
| 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; on a short window the options scroll and Cancel / OK stay visible (LR-BEHAV-DIALOGS, issue #781) |
|
||||
| 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, PSD (flat, 8/16-bit, no layers: `crates/codecs/src/psd.rs`), 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; a raw without colour matrices of its own, as a NEF, gets the colour it opens with as `ColorMatrix1` / `ForwardMatrix1` / `AsShotNeutral`, also in Convert / Copy as DNG, so other raw converters open it, issue #1745), Original (+ XMP sidecar). Non-raw → DNG not supported; no JPEG XL export in the web build. Opaque 8-bit PNG/TIFF/WebP exports omit the redundant alpha channel; real transparency is retained (#1670). |
|
||||
| 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; at most 65,535 px an edge, and an export enlarged past the photo's size stops at 512 megapixels (Lightroom's ceiling too) and is refused, with the size it would need, when it needs more memory than the computer has (it aborted the process before) |
|
||||
| LR-EXP-DIM | Output size | P0 | ✅ | `cmd:app.export` (`longEdge`, `shortEdge`, `width`, `height`, `fit`, `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); both of Lightroom's boxes: Dimensions (W × H fits either orientation) and Width & Height (`fit: widthHeight`, each edge bound to its own number, #1620); ppi written to JFIF / pHYs / TIFF tags; at most 65,535 px an edge, and an export enlarged past the photo's size stops at 512 megapixels (Lightroom's ceiling too) and is refused, with the size it would need, when it needs more memory than the computer has (it aborted the process before) |
|
||||
| LR-EXP-QUALITY | JPEG quality | P0 | ✅ | `cmd:app.export` (`quality`, `limitKb`), `crates/codecs/src/jpeg_par.rs` | 4:4:4 chroma (4:2:0 when a file-size limit is set); Huffman tables made for each image from its own symbol counts (T.81 Annex K.2) instead of the example tables: the same pixels, 12–14 % smaller files at quality 80–100 on a 24 MP photo-like image, still interleaved baseline (issue #1373) |
|
||||
| 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) | |
|
||||
@@ -891,3 +891,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
||||
| 2026-10-13 | minor | LRC-LIB-CATALOG-IMPORT review: offline Local promotion and result dismissal across library switches covered; redundant operation/settings copies removed. Status and summary counts unchanged. |
|
||||
| 2026-10-14 | patch | LRC-LIB-CATALOG-IMPORT: reconciled with upstream phased progress, cancellation kinds, WAL-only import, translations and current progress tracking. Status and summary counts unchanged. |
|
||||
| 2026-10-13 | minor | KEYC-COMPARE: `=` / `-` (also `+`) increase / decrease the grid thumbnail size (`view.thumbSizeUp` / `view.thumbSizeDown`, View menu), grids only; status unchanged (🟡) |
|
||||
| 2026-10-15 | patch | LR-EXP-DIM: Width & Height (`fit: widthHeight`) limits the width and the height as the photo stands, beside Dimensions (W × H, either orientation), as in Lightroom (#1620); status unchanged (✅) |
|
||||
|
||||
@@ -13,7 +13,7 @@ Select the photos, then choose **File** > **Export…**. Pick a format, a size a
|
||||
2. Choose **File** > **Export…**. The **Export** window shows how many photos it will export.
|
||||
3. Set **Format**: **JPEG**, **PNG**, **TIFF**, **PSD**, **WebP**, **AVIF**, **JPEG XL**, **DNG** or **Original files**. **DNG** writes raw photos as DNG with the edits embedded. **Original files** copies the files unchanged, each with an XMP sidecar that holds its edits.
|
||||
4. For **JPEG**, **AVIF** or lossy **JPEG XL**, set **Quality**. For **JPEG XL**, tick **Lossless** to keep every pixel exactly, at the cost of larger files. **Effort** trades time for file size, and **Progressive** makes the file show a coarse version first while it loads.
|
||||
5. Under the size options, tick **Full size**, or clear it and choose **Resize to** one of **Long Edge**, **Short Edge**, **Width**, **Height**, **Width × Height**, **Megapixels** or **Percentage**, then enter the value.
|
||||
5. Under the size options, tick **Full size**, or clear it and choose **Resize to** one of **Long Edge**, **Short Edge**, **Width**, **Height**, **Width & Height** (the width and the height each limited to its number, as the photo stands), **Dimensions** (the larger number to the long edge, whichever way round the photo is), **Megapixels** or **Percentage**, then enter the value.
|
||||
6. Set **Color space** (**sRGB**, **P3**, **Adobe RGB**, **ProPhoto** or **Rec.2020**) if you need something other than the default. Lossy **JPEG XL** writes **Adobe RGB** and **ProPhoto** as **Rec.2020**. Tick **Lossless** to keep them.
|
||||
7. Optional: set **Sharpen** for **Screen**, **Matte** or **Glossy** output, set **Metadata** to **All**, **No camera**, **Copyright** or **None**, and tick **Remove location info** or **Watermark**.
|
||||
8. Next to **Folder**, click **Choose…** and pick where the files go. **Subfolder** adds a folder inside it.
|
||||
|
||||
Reference in New Issue
Block a user