diff --git a/crates/engine/src/export.rs b/crates/engine/src/export.rs index 261064ccf..895664064 100644 --- a/crates/engine/src/export.rs +++ b/crates/engine/src/export.rs @@ -515,11 +515,16 @@ fn logo_coverage(width: usize, height: usize, wm: &Watermark, mut blend_px: impl return; } let turns = wm.quarter_turns(); - // Fit the turned box with one scale: clamping height alone distorts tall graphics. - let (max_w, max_h) = if turns % 2 == 1 { (height, width) } else { (width, height) }; - let scale = (wm.image_width.clamp(0.01, 1.0) as f64 * width as f64 / logo.width as f64) - .min(max_w as f64 / logo.width as f64) - .min(max_h as f64 / logo.height as f64); + // Reserve the inset on anchored axes before fitting the turned box. Centered axes + // ignore insets, just as placement below does. One scale preserves the graphic's shape. + let short = width.min(height) as f32; + let (ix, iy) = (wm.inset_x.clamp(0.0, 0.4) * short, wm.inset_y.clamp(0.0, 0.4) * short); + use Anchor::*; + let available_w = width as f64 - if matches!(wm.anchor, Top | Center | Bottom) { 0.0 } else { ix as f64 }; + let available_h = height as f64 - if matches!(wm.anchor, Left | Center | Right) { 0.0 } else { iy as f64 }; + let (max_w, max_h) = if turns % 2 == 1 { (available_h, available_w) } else { (available_w, available_h) }; + let scale = + (wm.image_width.clamp(0.01, 1.0) as f64 * width as f64 / logo.width as f64).min(max_w / logo.width as f64).min(max_h / logo.height as f64); let lw = (logo.width as f64 * scale).round().max(1.0) as usize; let lh = (logo.height as f64 * scale).round().max(1.0) as usize; let pre: lightcraft_raster::Image<[f32; 4]> = lightcraft_raster::Image::from_fn(logo.width, logo.height, |x, y| { @@ -528,11 +533,8 @@ fn logo_coverage(width: usize, height: usize, wm: &Watermark, mut blend_px: impl [p[0] as f32 * a, p[1] as f32 * a, p[2] as f32 * a, a] }); let scaled = lightcraft_raster::resample::resize(&pre, lw, lh, lightcraft_raster::resample::Filter::Mitchell); - let short = width.min(height) as f32; - let (ix, iy) = (wm.inset_x.clamp(0.0, 0.4) * short, wm.inset_y.clamp(0.0, 0.4) * short); let (bw, bh) = if turns % 2 == 1 { (lh, lw) } else { (lw, lh) }; let (w, h, lwf, lhf) = (width as f32, height as f32, bw as f32, bh as f32); - use Anchor::*; let ox = match wm.anchor { TopLeft | Left | BottomLeft => ix, Top | Center | Bottom => (w - lwf) / 2.0, @@ -2441,6 +2443,49 @@ mod tests { std::fs::remove_dir_all(dir).unwrap(); } + #[test] + fn graphic_watermark_fit_preserves_anchor_insets() { + let dir = std::env::temp_dir().join(format!("lc-wm-insets-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("tall.png"); + let logo = Rgba8::from_fn(40, 160, |_, _| [255, 0, 0, 255]); + std::fs::write(&path, lightcraft_codecs::encode_png(&EncodeImage::rgba8(&logo), &EncodeMeta::default()).unwrap()).unwrap(); + use Anchor::*; + for (anchor, rotation, expected) in [ + (BottomRight, 90, (0, 44, 111, 71)), + (TopLeft, 270, (8, 8, 119, 35)), + (Right, 90, (0, 26, 111, 53)), + (BottomRight, 0, (94, 0, 111, 71)), + (TopLeft, 180, (8, 8, 25, 79)), + (Bottom, 0, (51, 0, 68, 71)), + // Centered axes ignore their inset rather than shrinking the image unnecessarily. + (Center, 90, (0, 25, 119, 54)), + (Top, 90, (0, 8, 119, 37)), + ] { + let wm = Watermark { + image: path.to_string_lossy().into(), + image_width: 0.5, + opacity: 1.0, + inset_x: 0.1, + inset_y: 0.1, + rotation, + anchor, + ..Default::default() + }; + let mut img = Rgba8::from_fn(120, 80, |_, _| [0, 0, 0, 255]); + draw_watermark(&mut img, &wm); + let lit: Vec<_> = img.data.iter().enumerate().filter(|(_, p)| p[0] > 200).map(|(i, _)| (i % 120, i / 120)).collect(); + let bounds = ( + lit.iter().map(|p| p.0).min().unwrap(), + lit.iter().map(|p| p.1).min().unwrap(), + lit.iter().map(|p| p.0).max().unwrap(), + lit.iter().map(|p| p.1).max().unwrap(), + ); + assert_eq!(bounds, expected, "{anchor:?}, {rotation} degrees"); + } + std::fs::remove_dir_all(dir).unwrap(); + } + #[test] fn sharpen_increases_edge_contrast() { let mut img = Rgba8::new(8, 8); diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index 85a0ae039..2c9e9da05 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -2180,5 +2180,11 @@ "Checking the GPU…": "Verificando a GPU…", "CPU": "CPU", "A face model uses the GPU only when a check shows it gives the same results faster.": "Um modelo de rostos só usa a GPU quando uma verificação mostra que ela dá os mesmos resultados mais rápido.", - "Checking the GPU for faces": "Verificando a GPU para rostos" + "Checking the GPU for faces": "Verificando a GPU para rostos", + "Include selected photos": "Incluir fotos selecionadas", + "Color Overlay": "Sobreposição de cor", + "Color Overlay on B&W": "Sobreposição de cor em P&B", + "Image on Black": "Imagem sobre preto", + "Image on White": "Imagem sobre branco", + "White on Black": "Branco sobre preto" } diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index c09583828..1a8a69590 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** maintenance (refresh export watermark and HDR size-limit fixes onto current main, retaining Print Page Options and opaque RGB export) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (graphic watermark fitting retains anchored insets) · **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 @@ -895,3 +895,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove ### 2026-10-18 — export regression integration (maintenance) Graphic watermarks honour EXIF rotation/mirroring through the shared `read_graphic` path and keep their aspect ratio when fitted to the output. HDR JPEG `limitKb` includes the gain map and metadata. Refreshed onto main fb7ac08b, retaining upstream export bounds, colour metadata, JPEG encoding and opaque RGB/real-alpha handling (#1691); no feature-count or readiness estimate changes. + +### 2026-10-20 — graphic watermark inset regression + +Oversized graphics fit the space remaining after the applicable anchor insets, with one aspect-preserving scale before quarter-turn rotation. Centered axes continue to ignore insets. Pixel-bound regressions cover edge/corner anchors, centered placement and all four rotations. Main d999e382 is integrated; HDR size-limit and EXIF-orientation fixes are unchanged.