export: preserve graphic watermark insets when fitting

This commit is contained in:
nevicia2010-hub
2026-10-20 09:50:18 +02:00
3 changed files with 65 additions and 10 deletions
+53 -8
View File
@@ -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);
+7 -1
View File
@@ -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"
}
+5 -1
View File
@@ -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.