Fix edge cases and consistency issues

This commit is contained in:
2026-03-07 19:47:23 +02:00
parent 6bf9d60430
commit 9e1562c4c4
44 changed files with 5748 additions and 2221 deletions

View File

@@ -19,8 +19,8 @@ pub fn calculate_position(
let center_x = iw.saturating_sub(ww) / 2;
let center_y = ih.saturating_sub(wh) / 2;
let right_x = iw.saturating_sub(ww + margin);
let bottom_y = ih.saturating_sub(wh + margin);
let right_x = iw.saturating_sub(ww).saturating_sub(margin);
let bottom_y = ih.saturating_sub(wh).saturating_sub(margin);
match position {
WatermarkPosition::TopLeft => (margin, margin),
@@ -69,7 +69,7 @@ pub fn apply_watermark(
if *tiled {
apply_tiled_image_watermark(img, path, *opacity, *scale, *rotation, *margin)
} else {
apply_image_watermark(img, path, *position, *opacity, *scale, *rotation)
apply_image_watermark(img, path, *position, *opacity, *scale, *rotation, *margin)
}
}
}
@@ -128,20 +128,29 @@ fn find_system_font(family: Option<&str>) -> Result<Vec<u8>> {
})
}
/// Recursively walk a directory and collect file paths
/// Recursively walk a directory and collect file paths (max depth 5)
fn walkdir(dir: &std::path::Path) -> std::io::Result<Vec<std::path::PathBuf>> {
walkdir_depth(dir, 5)
}
fn walkdir_depth(dir: &std::path::Path, max_depth: u32) -> std::io::Result<Vec<std::path::PathBuf>> {
const MAX_RESULTS: usize = 10_000;
let mut results = Vec::new();
if dir.is_dir() {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if let Ok(sub) = walkdir(&path) {
results.extend(sub);
}
} else {
results.push(path);
if max_depth == 0 || !dir.is_dir() {
return Ok(results);
}
for entry in std::fs::read_dir(dir)? {
if results.len() >= MAX_RESULTS {
break;
}
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if let Ok(sub) = walkdir_depth(&path, max_depth - 1) {
results.extend(sub);
}
} else {
results.push(path);
}
}
Ok(results)
@@ -156,8 +165,8 @@ fn render_text_to_image(
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 text_width = ((text.chars().count().min(10_000) as f32 * font_size.min(1000.0) * 0.6) as u32).saturating_add(4).min(8192);
let text_height = ((font_size.min(1000.0) * 1.4) as u32).saturating_add(4).min(4096);
let alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8;
let draw_color = Rgba([color[0], color[1], color[2], alpha]);
@@ -167,6 +176,30 @@ fn render_text_to_image(
buf
}
/// Expand canvas so rotated content fits without clipping, then rotate
fn rotate_on_expanded_canvas(img: &image::RgbaImage, radians: f32) -> DynamicImage {
let w = img.width() as f32;
let h = img.height() as f32;
let cos = radians.abs().cos();
let sin = radians.abs().sin();
// Bounding box of rotated rectangle
let new_w = (w * cos + h * sin).ceil() as u32 + 2;
let new_h = (w * sin + h * cos).ceil() as u32 + 2;
// Place original in center of expanded transparent canvas
let mut expanded = image::RgbaImage::new(new_w, new_h);
let ox = (new_w.saturating_sub(img.width())) / 2;
let oy = (new_h.saturating_sub(img.height())) / 2;
image::imageops::overlay(&mut expanded, img, ox as i64, oy as i64);
imageproc::geometric_transformations::rotate_about_center(
&expanded,
radians,
imageproc::geometric_transformations::Interpolation::Bilinear,
Rgba([0, 0, 0, 0]),
).into()
}
/// Rotate an RGBA image by the given WatermarkRotation
fn rotate_watermark_image(
img: DynamicImage,
@@ -175,20 +208,13 @@ fn rotate_watermark_image(
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()
rotate_on_expanded_canvas(&img.to_rgba8(), std::f32::consts::FRAC_PI_4)
}
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()
rotate_on_expanded_canvas(&img.to_rgba8(), -std::f32::consts::FRAC_PI_4)
}
super::WatermarkRotation::Custom(degrees) => {
rotate_on_expanded_canvas(&img.to_rgba8(), degrees.to_radians())
}
}
}
@@ -204,6 +230,9 @@ fn apply_text_watermark(
rotation: Option<super::WatermarkRotation>,
margin_px: u32,
) -> Result<DynamicImage> {
if text.is_empty() {
return Ok(img);
}
let font_data = find_system_font(font_family)?;
let font = ab_glyph::FontArc::try_from_vec(font_data).map_err(|_| {
PixstripError::Processing {
@@ -234,8 +263,8 @@ fn apply_text_watermark(
} 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_width = ((text.chars().count().min(10_000) as f32 * font_size.min(1000.0) * 0.6) as u32).saturating_add(4).min(8192);
let text_height = ((font_size.min(1000.0) * 1.4) as u32).saturating_add(4).min(4096);
let text_dims = Dimensions {
width: text_width,
height: text_height,
@@ -266,6 +295,9 @@ fn apply_tiled_text_watermark(
rotation: Option<super::WatermarkRotation>,
margin: u32,
) -> Result<DynamicImage> {
if text.is_empty() {
return Ok(img);
}
let font_data = find_system_font(font_family)?;
let font = ab_glyph::FontArc::try_from_vec(font_data).map_err(|_| {
PixstripError::Processing {
@@ -301,17 +333,17 @@ fn apply_tiled_text_watermark(
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 text_width = ((text.len().min(10_000) as f32 * font_size.min(1000.0) * 0.6) as i64 + 4).min(8192);
let text_height = ((font_size.min(1000.0) * 1.4) as i64 + 4).min(4096);
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;
let mut y = spacing as i64;
while y < ih as i64 {
let mut x = spacing as i64;
while x < iw as i64 {
draw_text_mut(&mut rgba, draw_color, x as i32, y as i32, scale, &font, text);
x += text_width + spacing as i64;
}
y += text_height as i32 + spacing as i32;
y += text_height + spacing as i64;
}
}
@@ -390,6 +422,7 @@ fn apply_image_watermark(
opacity: f32,
scale: f32,
rotation: Option<super::WatermarkRotation>,
margin: u32,
) -> Result<DynamicImage> {
let watermark = image::open(watermark_path).map_err(|e| PixstripError::Processing {
operation: "watermark".into(),
@@ -423,7 +456,6 @@ fn apply_image_watermark(
height: watermark.height(),
};
let margin = 10;
let (x, y) = calculate_position(position, image_dims, wm_dims, margin);
let mut base = img.into_rgba8();