diff --git a/pixstrip-core/src/operations/watermark.rs b/pixstrip-core/src/operations/watermark.rs index 2dd6318..dec262f 100644 --- a/pixstrip-core/src/operations/watermark.rs +++ b/pixstrip-core/src/operations/watermark.rs @@ -62,14 +62,14 @@ pub fn apply_watermark( position, opacity, scale, - rotation: _, + rotation, tiled, margin, } => { if *tiled { - apply_tiled_image_watermark(img, path, *opacity, *scale, *margin) + apply_tiled_image_watermark(img, path, *opacity, *scale, *rotation, *margin) } 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> { 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( img: DynamicImage, text: &str, @@ -155,7 +201,7 @@ fn apply_text_watermark( opacity: f32, color: [u8; 4], font_family: Option<&str>, - _rotation: Option, + rotation: Option, margin_px: u32, ) -> Result { let font_data = find_system_font(font_family)?; @@ -166,31 +212,48 @@ 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 text_width = (text.len() as f32 * font_size * 0.6) as u32; - let text_height = font_size as u32; - let text_dims = Dimensions { - width: text_width, - height: text_height, - }; + 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 image_dims = Dimensions { - width: img.width(), - height: img.height(), - }; + 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_height = font_size as u32; + let text_dims = Dimensions { + width: text_width, + height: text_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, text_dims, margin); - 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 alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8; + let draw_color = Rgba([color[0], color[1], color[2], alpha]); - 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 rgba = img.into_rgba8(); - draw_text_mut(&mut rgba, draw_color, x as i32, y as i32, scale, &font, text); - - Ok(DynamicImage::ImageRgba8(rgba)) + let mut rgba = img.into_rgba8(); + draw_text_mut(&mut rgba, draw_color, x as i32, y as i32, scale, &font, text); + Ok(DynamicImage::ImageRgba8(rgba)) + } } fn apply_tiled_text_watermark( @@ -200,7 +263,7 @@ fn apply_tiled_text_watermark( opacity: f32, color: [u8; 4], font_family: Option<&str>, - _rotation: Option, + rotation: Option, margin: u32, ) -> Result { let font_data = find_system_font(font_family)?; @@ -211,25 +274,45 @@ fn apply_tiled_text_watermark( } })?; - let scale = ab_glyph::PxScale::from(font_size); - let alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8; - 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_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; - while y < ih as i32 { - let mut x = spacing as i32; - while x < iw as i32 { - draw_text_mut(&mut rgba, draw_color, x, y, scale, &font, text); - x += text_width as i32 + spacing as i32; + 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 alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8; + 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_height = font_size as u32; + + let mut y = spacing as i32; + while y < ih as i32 { + let mut x = spacing as i32; + while x < iw as i32 { + draw_text_mut(&mut rgba, draw_color, x, y, scale, &font, text); + x += text_width as i32 + spacing as i32; + } + y += text_height as i32 + spacing as i32; } - y += text_height as i32 + spacing as i32; } Ok(DynamicImage::ImageRgba8(rgba)) @@ -240,6 +323,7 @@ fn apply_tiled_image_watermark( watermark_path: &std::path::Path, opacity: f32, scale: f32, + rotation: Option, margin: u32, ) -> Result { let watermark = image::open(watermark_path).map_err(|e| PixstripError::Processing { @@ -254,8 +338,13 @@ fn apply_tiled_image_watermark( 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 ow = overlay.width(); + let oh = overlay.height(); let alpha_factor = opacity.clamp(0.0, 1.0); let spacing = margin.max(10); @@ -266,8 +355,8 @@ fn apply_tiled_image_watermark( while ty < ih { let mut tx = spacing; while tx < iw { - for oy in 0..wm_height { - for ox in 0..wm_width { + for oy in 0..oh { + for ox in 0..ow { let px = tx + ox; let py = ty + oy; 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)) @@ -300,6 +389,7 @@ fn apply_image_watermark( position: WatermarkPosition, opacity: f32, scale: f32, + rotation: Option, ) -> Result { let watermark = image::open(watermark_path).map_err(|e| PixstripError::Processing { operation: "watermark".into(), @@ -314,19 +404,23 @@ fn apply_image_watermark( return Ok(img); } - let watermark = watermark.resize_exact( + 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 image_dims = Dimensions { width: img.width(), height: img.height(), }; let wm_dims = Dimensions { - width: wm_width, - height: wm_height, + width: watermark.width(), + height: watermark.height(), }; let margin = 10; @@ -334,11 +428,13 @@ fn apply_image_watermark( let mut base = img.into_rgba8(); let overlay = watermark.to_rgba8(); + let ow = overlay.width(); + let oh = overlay.height(); let alpha_factor = opacity.clamp(0.0, 1.0); - for oy in 0..wm_height { - for ox in 0..wm_width { + for oy in 0..oh { + for ox in 0..ow { let px = x + ox; let py = y + oy;