Implement watermark rotation for text and image watermarks
- Add rotate_watermark_image() using imageproc rotate_about_center for 45/-45 degree rotations and image::rotate90 for 90 degrees - Add render_text_to_image() helper that renders text to a transparent buffer for rotation before compositing - Apply rotation to single text, tiled text, single image, and tiled image watermark modes - Fix tiled image watermark to use actual overlay dimensions (which change after rotation) instead of pre-rotation values
This commit is contained in:
@@ -62,14 +62,14 @@ pub fn apply_watermark(
|
|||||||
position,
|
position,
|
||||||
opacity,
|
opacity,
|
||||||
scale,
|
scale,
|
||||||
rotation: _,
|
rotation,
|
||||||
tiled,
|
tiled,
|
||||||
margin,
|
margin,
|
||||||
} => {
|
} => {
|
||||||
if *tiled {
|
if *tiled {
|
||||||
apply_tiled_image_watermark(img, path, *opacity, *scale, *margin)
|
apply_tiled_image_watermark(img, path, *opacity, *scale, *rotation, *margin)
|
||||||
} else {
|
} else {
|
||||||
apply_image_watermark(img, path, *position, *opacity, *scale)
|
apply_image_watermark(img, path, *position, *opacity, *scale, *rotation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +147,52 @@ fn walkdir(dir: &std::path::Path) -> std::io::Result<Vec<std::path::PathBuf>> {
|
|||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render text onto a transparent RGBA buffer and return it as a DynamicImage
|
||||||
|
fn render_text_to_image(
|
||||||
|
text: &str,
|
||||||
|
font: &ab_glyph::FontArc,
|
||||||
|
font_size: f32,
|
||||||
|
color: [u8; 4],
|
||||||
|
opacity: f32,
|
||||||
|
) -> image::RgbaImage {
|
||||||
|
let scale = ab_glyph::PxScale::from(font_size);
|
||||||
|
let text_width = (text.len() as f32 * font_size * 0.6) as u32 + 4;
|
||||||
|
let text_height = (font_size * 1.4) as u32 + 4;
|
||||||
|
|
||||||
|
let alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8;
|
||||||
|
let draw_color = Rgba([color[0], color[1], color[2], alpha]);
|
||||||
|
|
||||||
|
let mut buf = image::RgbaImage::new(text_width.max(1), text_height.max(1));
|
||||||
|
draw_text_mut(&mut buf, draw_color, 2, 2, scale, font, text);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rotate an RGBA image by the given WatermarkRotation
|
||||||
|
fn rotate_watermark_image(
|
||||||
|
img: DynamicImage,
|
||||||
|
rotation: super::WatermarkRotation,
|
||||||
|
) -> DynamicImage {
|
||||||
|
match rotation {
|
||||||
|
super::WatermarkRotation::Degrees90 => img.rotate90(),
|
||||||
|
super::WatermarkRotation::Degrees45 => {
|
||||||
|
imageproc::geometric_transformations::rotate_about_center(
|
||||||
|
&img.to_rgba8(),
|
||||||
|
std::f32::consts::FRAC_PI_4,
|
||||||
|
imageproc::geometric_transformations::Interpolation::Bilinear,
|
||||||
|
Rgba([0, 0, 0, 0]),
|
||||||
|
).into()
|
||||||
|
}
|
||||||
|
super::WatermarkRotation::DegreesNeg45 => {
|
||||||
|
imageproc::geometric_transformations::rotate_about_center(
|
||||||
|
&img.to_rgba8(),
|
||||||
|
-std::f32::consts::FRAC_PI_4,
|
||||||
|
imageproc::geometric_transformations::Interpolation::Bilinear,
|
||||||
|
Rgba([0, 0, 0, 0]),
|
||||||
|
).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_text_watermark(
|
fn apply_text_watermark(
|
||||||
img: DynamicImage,
|
img: DynamicImage,
|
||||||
text: &str,
|
text: &str,
|
||||||
@@ -155,7 +201,7 @@ fn apply_text_watermark(
|
|||||||
opacity: f32,
|
opacity: f32,
|
||||||
color: [u8; 4],
|
color: [u8; 4],
|
||||||
font_family: Option<&str>,
|
font_family: Option<&str>,
|
||||||
_rotation: Option<super::WatermarkRotation>,
|
rotation: Option<super::WatermarkRotation>,
|
||||||
margin_px: u32,
|
margin_px: u32,
|
||||||
) -> Result<DynamicImage> {
|
) -> Result<DynamicImage> {
|
||||||
let font_data = find_system_font(font_family)?;
|
let font_data = find_system_font(font_family)?;
|
||||||
@@ -166,21 +212,38 @@ fn apply_text_watermark(
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let scale = ab_glyph::PxScale::from(font_size);
|
if let Some(rot) = rotation {
|
||||||
|
// Render text to buffer, rotate, then overlay
|
||||||
|
let text_buf = render_text_to_image(text, &font, font_size, color, opacity);
|
||||||
|
let rotated = rotate_watermark_image(DynamicImage::ImageRgba8(text_buf), rot);
|
||||||
|
|
||||||
// Estimate text dimensions for positioning
|
let wm_dims = Dimensions {
|
||||||
|
width: rotated.width(),
|
||||||
|
height: rotated.height(),
|
||||||
|
};
|
||||||
|
let image_dims = Dimensions {
|
||||||
|
width: img.width(),
|
||||||
|
height: img.height(),
|
||||||
|
};
|
||||||
|
let margin = if margin_px > 0 { margin_px } else { (font_size * 0.5) as u32 };
|
||||||
|
let (x, y) = calculate_position(position, image_dims, wm_dims, margin);
|
||||||
|
|
||||||
|
let mut rgba = img.into_rgba8();
|
||||||
|
image::imageops::overlay(&mut rgba, &rotated.to_rgba8(), x as i64, y as i64);
|
||||||
|
Ok(DynamicImage::ImageRgba8(rgba))
|
||||||
|
} else {
|
||||||
|
// No rotation - draw text directly (faster)
|
||||||
|
let scale = ab_glyph::PxScale::from(font_size);
|
||||||
let text_width = (text.len() as f32 * font_size * 0.6) as u32;
|
let text_width = (text.len() as f32 * font_size * 0.6) as u32;
|
||||||
let text_height = font_size as u32;
|
let text_height = font_size as u32;
|
||||||
let text_dims = Dimensions {
|
let text_dims = Dimensions {
|
||||||
width: text_width,
|
width: text_width,
|
||||||
height: text_height,
|
height: text_height,
|
||||||
};
|
};
|
||||||
|
|
||||||
let image_dims = Dimensions {
|
let image_dims = Dimensions {
|
||||||
width: img.width(),
|
width: img.width(),
|
||||||
height: img.height(),
|
height: img.height(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let margin = if margin_px > 0 { margin_px } else { (font_size * 0.5) as u32 };
|
let margin = if margin_px > 0 { margin_px } else { (font_size * 0.5) as u32 };
|
||||||
let (x, y) = calculate_position(position, image_dims, text_dims, margin);
|
let (x, y) = calculate_position(position, image_dims, text_dims, margin);
|
||||||
|
|
||||||
@@ -189,9 +252,9 @@ fn apply_text_watermark(
|
|||||||
|
|
||||||
let mut rgba = img.into_rgba8();
|
let mut rgba = img.into_rgba8();
|
||||||
draw_text_mut(&mut rgba, draw_color, x as i32, y as i32, scale, &font, text);
|
draw_text_mut(&mut rgba, draw_color, x as i32, y as i32, scale, &font, text);
|
||||||
|
|
||||||
Ok(DynamicImage::ImageRgba8(rgba))
|
Ok(DynamicImage::ImageRgba8(rgba))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_tiled_text_watermark(
|
fn apply_tiled_text_watermark(
|
||||||
img: DynamicImage,
|
img: DynamicImage,
|
||||||
@@ -200,7 +263,7 @@ fn apply_tiled_text_watermark(
|
|||||||
opacity: f32,
|
opacity: f32,
|
||||||
color: [u8; 4],
|
color: [u8; 4],
|
||||||
font_family: Option<&str>,
|
font_family: Option<&str>,
|
||||||
_rotation: Option<super::WatermarkRotation>,
|
rotation: Option<super::WatermarkRotation>,
|
||||||
margin: u32,
|
margin: u32,
|
||||||
) -> Result<DynamicImage> {
|
) -> Result<DynamicImage> {
|
||||||
let font_data = find_system_font(font_family)?;
|
let font_data = find_system_font(font_family)?;
|
||||||
@@ -211,16 +274,35 @@ fn apply_tiled_text_watermark(
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let spacing = margin.max(20);
|
||||||
|
let mut rgba = img.into_rgba8();
|
||||||
|
let (iw, ih) = (rgba.width(), rgba.height());
|
||||||
|
|
||||||
|
if let Some(rot) = rotation {
|
||||||
|
// Render a single tile, rotate it, then tile across the image
|
||||||
|
let tile_buf = render_text_to_image(text, &font, font_size, color, opacity);
|
||||||
|
let rotated = rotate_watermark_image(DynamicImage::ImageRgba8(tile_buf), rot);
|
||||||
|
let tile = rotated.to_rgba8();
|
||||||
|
let tw = tile.width();
|
||||||
|
let th = tile.height();
|
||||||
|
|
||||||
|
let mut y: i64 = spacing as i64;
|
||||||
|
while y < ih as i64 {
|
||||||
|
let mut x: i64 = spacing as i64;
|
||||||
|
while x < iw as i64 {
|
||||||
|
image::imageops::overlay(&mut rgba, &tile, x, y);
|
||||||
|
x += tw as i64 + spacing as i64;
|
||||||
|
}
|
||||||
|
y += th as i64 + spacing as i64;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No rotation - draw text directly (faster)
|
||||||
let scale = ab_glyph::PxScale::from(font_size);
|
let scale = ab_glyph::PxScale::from(font_size);
|
||||||
let alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8;
|
let alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8;
|
||||||
let draw_color = Rgba([color[0], color[1], color[2], alpha]);
|
let draw_color = Rgba([color[0], color[1], color[2], alpha]);
|
||||||
|
|
||||||
let text_width = (text.len() as f32 * font_size * 0.6) as u32;
|
let text_width = (text.len() as f32 * font_size * 0.6) as u32;
|
||||||
let text_height = font_size as u32;
|
let text_height = font_size as u32;
|
||||||
let spacing = margin.max(20);
|
|
||||||
|
|
||||||
let mut rgba = img.into_rgba8();
|
|
||||||
let (iw, ih) = (rgba.width(), rgba.height());
|
|
||||||
|
|
||||||
let mut y = spacing as i32;
|
let mut y = spacing as i32;
|
||||||
while y < ih as i32 {
|
while y < ih as i32 {
|
||||||
@@ -231,6 +313,7 @@ fn apply_tiled_text_watermark(
|
|||||||
}
|
}
|
||||||
y += text_height as i32 + spacing as i32;
|
y += text_height as i32 + spacing as i32;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(DynamicImage::ImageRgba8(rgba))
|
Ok(DynamicImage::ImageRgba8(rgba))
|
||||||
}
|
}
|
||||||
@@ -240,6 +323,7 @@ fn apply_tiled_image_watermark(
|
|||||||
watermark_path: &std::path::Path,
|
watermark_path: &std::path::Path,
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
|
rotation: Option<super::WatermarkRotation>,
|
||||||
margin: u32,
|
margin: u32,
|
||||||
) -> Result<DynamicImage> {
|
) -> Result<DynamicImage> {
|
||||||
let watermark = image::open(watermark_path).map_err(|e| PixstripError::Processing {
|
let watermark = image::open(watermark_path).map_err(|e| PixstripError::Processing {
|
||||||
@@ -254,8 +338,13 @@ fn apply_tiled_image_watermark(
|
|||||||
return Ok(img);
|
return Ok(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
let watermark = watermark.resize_exact(wm_width, wm_height, image::imageops::FilterType::Lanczos3);
|
let mut watermark = watermark.resize_exact(wm_width, wm_height, image::imageops::FilterType::Lanczos3);
|
||||||
|
if let Some(rot) = rotation {
|
||||||
|
watermark = rotate_watermark_image(watermark, rot);
|
||||||
|
}
|
||||||
let overlay = watermark.to_rgba8();
|
let overlay = watermark.to_rgba8();
|
||||||
|
let ow = overlay.width();
|
||||||
|
let oh = overlay.height();
|
||||||
let alpha_factor = opacity.clamp(0.0, 1.0);
|
let alpha_factor = opacity.clamp(0.0, 1.0);
|
||||||
let spacing = margin.max(10);
|
let spacing = margin.max(10);
|
||||||
|
|
||||||
@@ -266,8 +355,8 @@ fn apply_tiled_image_watermark(
|
|||||||
while ty < ih {
|
while ty < ih {
|
||||||
let mut tx = spacing;
|
let mut tx = spacing;
|
||||||
while tx < iw {
|
while tx < iw {
|
||||||
for oy in 0..wm_height {
|
for oy in 0..oh {
|
||||||
for ox in 0..wm_width {
|
for ox in 0..ow {
|
||||||
let px = tx + ox;
|
let px = tx + ox;
|
||||||
let py = ty + oy;
|
let py = ty + oy;
|
||||||
if px < iw && py < ih {
|
if px < iw && py < ih {
|
||||||
@@ -286,9 +375,9 @@ fn apply_tiled_image_watermark(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tx += wm_width + spacing;
|
tx += ow + spacing;
|
||||||
}
|
}
|
||||||
ty += wm_height + spacing;
|
ty += oh + spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(DynamicImage::ImageRgba8(base))
|
Ok(DynamicImage::ImageRgba8(base))
|
||||||
@@ -300,6 +389,7 @@ fn apply_image_watermark(
|
|||||||
position: WatermarkPosition,
|
position: WatermarkPosition,
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
|
rotation: Option<super::WatermarkRotation>,
|
||||||
) -> Result<DynamicImage> {
|
) -> Result<DynamicImage> {
|
||||||
let watermark = image::open(watermark_path).map_err(|e| PixstripError::Processing {
|
let watermark = image::open(watermark_path).map_err(|e| PixstripError::Processing {
|
||||||
operation: "watermark".into(),
|
operation: "watermark".into(),
|
||||||
@@ -314,19 +404,23 @@ fn apply_image_watermark(
|
|||||||
return Ok(img);
|
return Ok(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
let watermark = watermark.resize_exact(
|
let mut watermark = watermark.resize_exact(
|
||||||
wm_width,
|
wm_width,
|
||||||
wm_height,
|
wm_height,
|
||||||
image::imageops::FilterType::Lanczos3,
|
image::imageops::FilterType::Lanczos3,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if let Some(rot) = rotation {
|
||||||
|
watermark = rotate_watermark_image(watermark, rot);
|
||||||
|
}
|
||||||
|
|
||||||
let image_dims = Dimensions {
|
let image_dims = Dimensions {
|
||||||
width: img.width(),
|
width: img.width(),
|
||||||
height: img.height(),
|
height: img.height(),
|
||||||
};
|
};
|
||||||
let wm_dims = Dimensions {
|
let wm_dims = Dimensions {
|
||||||
width: wm_width,
|
width: watermark.width(),
|
||||||
height: wm_height,
|
height: watermark.height(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let margin = 10;
|
let margin = 10;
|
||||||
@@ -334,11 +428,13 @@ fn apply_image_watermark(
|
|||||||
|
|
||||||
let mut base = img.into_rgba8();
|
let mut base = img.into_rgba8();
|
||||||
let overlay = watermark.to_rgba8();
|
let overlay = watermark.to_rgba8();
|
||||||
|
let ow = overlay.width();
|
||||||
|
let oh = overlay.height();
|
||||||
|
|
||||||
let alpha_factor = opacity.clamp(0.0, 1.0);
|
let alpha_factor = opacity.clamp(0.0, 1.0);
|
||||||
|
|
||||||
for oy in 0..wm_height {
|
for oy in 0..oh {
|
||||||
for ox in 0..wm_width {
|
for ox in 0..ow {
|
||||||
let px = x + ox;
|
let px = x + ox;
|
||||||
let py = y + oy;
|
let py = y + oy;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user