mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-31 05:28:25 -06:00
export: preserve graphic watermark insets when fitting
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user