Add watermark tiling, rotation types, margin/scale controls
This commit is contained in:
@@ -314,6 +314,9 @@ fn cmd_process(args: CmdProcessArgs) {
|
|||||||
opacity: args.watermark_opacity,
|
opacity: args.watermark_opacity,
|
||||||
color: [255, 255, 255, 255],
|
color: [255, 255, 255, 255],
|
||||||
font_family: None,
|
font_family: None,
|
||||||
|
rotation: None,
|
||||||
|
tiled: false,
|
||||||
|
margin: 10,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if args.rename_prefix.is_some() || args.rename_suffix.is_some() || args.rename_template.is_some() {
|
if args.rename_prefix.is_some() || args.rename_suffix.is_some() || args.rename_template.is_some() {
|
||||||
|
|||||||
@@ -204,15 +204,28 @@ pub enum WatermarkConfig {
|
|||||||
opacity: f32,
|
opacity: f32,
|
||||||
color: [u8; 4],
|
color: [u8; 4],
|
||||||
font_family: Option<String>,
|
font_family: Option<String>,
|
||||||
|
rotation: Option<WatermarkRotation>,
|
||||||
|
tiled: bool,
|
||||||
|
margin: u32,
|
||||||
},
|
},
|
||||||
Image {
|
Image {
|
||||||
path: std::path::PathBuf,
|
path: std::path::PathBuf,
|
||||||
position: WatermarkPosition,
|
position: WatermarkPosition,
|
||||||
opacity: f32,
|
opacity: f32,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
|
rotation: Option<WatermarkRotation>,
|
||||||
|
tiled: bool,
|
||||||
|
margin: u32,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub enum WatermarkRotation {
|
||||||
|
Degrees45,
|
||||||
|
DegreesNeg45,
|
||||||
|
Degrees90,
|
||||||
|
}
|
||||||
|
|
||||||
// --- Adjustments ---
|
// --- Adjustments ---
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -47,13 +47,31 @@ pub fn apply_watermark(
|
|||||||
opacity,
|
opacity,
|
||||||
color,
|
color,
|
||||||
font_family,
|
font_family,
|
||||||
} => apply_text_watermark(img, text, *position, *font_size, *opacity, *color, font_family.as_deref()),
|
rotation,
|
||||||
|
tiled,
|
||||||
|
margin,
|
||||||
|
} => {
|
||||||
|
if *tiled {
|
||||||
|
apply_tiled_text_watermark(img, text, *font_size, *opacity, *color, font_family.as_deref(), *rotation, *margin)
|
||||||
|
} else {
|
||||||
|
apply_text_watermark(img, text, *position, *font_size, *opacity, *color, font_family.as_deref(), *rotation, *margin)
|
||||||
|
}
|
||||||
|
}
|
||||||
WatermarkConfig::Image {
|
WatermarkConfig::Image {
|
||||||
path,
|
path,
|
||||||
position,
|
position,
|
||||||
opacity,
|
opacity,
|
||||||
scale,
|
scale,
|
||||||
} => apply_image_watermark(img, path, *position, *opacity, *scale),
|
rotation: _,
|
||||||
|
tiled,
|
||||||
|
margin,
|
||||||
|
} => {
|
||||||
|
if *tiled {
|
||||||
|
apply_tiled_image_watermark(img, path, *opacity, *scale, *margin)
|
||||||
|
} else {
|
||||||
|
apply_image_watermark(img, path, *position, *opacity, *scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +155,8 @@ 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>,
|
||||||
|
margin_px: u32,
|
||||||
) -> Result<DynamicImage> {
|
) -> Result<DynamicImage> {
|
||||||
let font_data = find_system_font(font_family)?;
|
let font_data = find_system_font(font_family)?;
|
||||||
let font = ab_glyph::FontArc::try_from_vec(font_data).map_err(|_| {
|
let font = ab_glyph::FontArc::try_from_vec(font_data).map_err(|_| {
|
||||||
@@ -161,7 +181,7 @@ fn apply_text_watermark(
|
|||||||
height: img.height(),
|
height: img.height(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let margin = (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);
|
||||||
|
|
||||||
let alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8;
|
let alpha = (opacity * 255.0).clamp(0.0, 255.0) as u8;
|
||||||
@@ -173,6 +193,107 @@ fn apply_text_watermark(
|
|||||||
Ok(DynamicImage::ImageRgba8(rgba))
|
Ok(DynamicImage::ImageRgba8(rgba))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_tiled_text_watermark(
|
||||||
|
img: DynamicImage,
|
||||||
|
text: &str,
|
||||||
|
font_size: f32,
|
||||||
|
opacity: f32,
|
||||||
|
color: [u8; 4],
|
||||||
|
font_family: Option<&str>,
|
||||||
|
_rotation: Option<super::WatermarkRotation>,
|
||||||
|
margin: u32,
|
||||||
|
) -> Result<DynamicImage> {
|
||||||
|
let font_data = find_system_font(font_family)?;
|
||||||
|
let font = ab_glyph::FontArc::try_from_vec(font_data).map_err(|_| {
|
||||||
|
PixstripError::Processing {
|
||||||
|
operation: "watermark".into(),
|
||||||
|
reason: "Failed to load font".into(),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
y += text_height as i32 + spacing as i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DynamicImage::ImageRgba8(rgba))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_tiled_image_watermark(
|
||||||
|
img: DynamicImage,
|
||||||
|
watermark_path: &std::path::Path,
|
||||||
|
opacity: f32,
|
||||||
|
scale: f32,
|
||||||
|
margin: u32,
|
||||||
|
) -> Result<DynamicImage> {
|
||||||
|
let watermark = image::open(watermark_path).map_err(|e| PixstripError::Processing {
|
||||||
|
operation: "watermark".into(),
|
||||||
|
reason: format!("Failed to load watermark image: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let wm_width = (watermark.width() as f32 * scale) as u32;
|
||||||
|
let wm_height = (watermark.height() as f32 * scale) as u32;
|
||||||
|
|
||||||
|
if wm_width == 0 || wm_height == 0 {
|
||||||
|
return Ok(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
let watermark = watermark.resize_exact(wm_width, wm_height, image::imageops::FilterType::Lanczos3);
|
||||||
|
let overlay = watermark.to_rgba8();
|
||||||
|
let alpha_factor = opacity.clamp(0.0, 1.0);
|
||||||
|
let spacing = margin.max(10);
|
||||||
|
|
||||||
|
let mut base = img.into_rgba8();
|
||||||
|
let (iw, ih) = (base.width(), base.height());
|
||||||
|
|
||||||
|
let mut ty = spacing;
|
||||||
|
while ty < ih {
|
||||||
|
let mut tx = spacing;
|
||||||
|
while tx < iw {
|
||||||
|
for oy in 0..wm_height {
|
||||||
|
for ox in 0..wm_width {
|
||||||
|
let px = tx + ox;
|
||||||
|
let py = ty + oy;
|
||||||
|
if px < iw && py < ih {
|
||||||
|
let wm_pixel = overlay.get_pixel(ox, oy);
|
||||||
|
let base_pixel = base.get_pixel(px, py);
|
||||||
|
let wm_alpha = (wm_pixel[3] as f32 / 255.0) * alpha_factor;
|
||||||
|
let base_alpha = base_pixel[3] as f32 / 255.0;
|
||||||
|
let out_alpha = wm_alpha + base_alpha * (1.0 - wm_alpha);
|
||||||
|
if out_alpha > 0.0 {
|
||||||
|
let r = ((wm_pixel[0] as f32 * wm_alpha + base_pixel[0] as f32 * base_alpha * (1.0 - wm_alpha)) / out_alpha) as u8;
|
||||||
|
let g = ((wm_pixel[1] as f32 * wm_alpha + base_pixel[1] as f32 * base_alpha * (1.0 - wm_alpha)) / out_alpha) as u8;
|
||||||
|
let b = ((wm_pixel[2] as f32 * wm_alpha + base_pixel[2] as f32 * base_alpha * (1.0 - wm_alpha)) / out_alpha) as u8;
|
||||||
|
let a = (out_alpha * 255.0) as u8;
|
||||||
|
base.put_pixel(px, py, Rgba([r, g, b, a]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx += wm_width + spacing;
|
||||||
|
}
|
||||||
|
ty += wm_height + spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DynamicImage::ImageRgba8(base))
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_image_watermark(
|
fn apply_image_watermark(
|
||||||
img: DynamicImage,
|
img: DynamicImage,
|
||||||
watermark_path: &std::path::Path,
|
watermark_path: &std::path::Path,
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ pub struct JobConfig {
|
|||||||
pub watermark_color: [u8; 4],
|
pub watermark_color: [u8; 4],
|
||||||
pub watermark_font_family: String,
|
pub watermark_font_family: String,
|
||||||
pub watermark_use_image: bool,
|
pub watermark_use_image: bool,
|
||||||
|
pub watermark_tiled: bool,
|
||||||
|
pub watermark_margin: u32,
|
||||||
|
pub watermark_scale: f32,
|
||||||
// Rename
|
// Rename
|
||||||
pub rename_enabled: bool,
|
pub rename_enabled: bool,
|
||||||
pub rename_prefix: String,
|
pub rename_prefix: String,
|
||||||
@@ -363,6 +366,9 @@ fn build_ui(app: &adw::Application) {
|
|||||||
watermark_color: [255, 255, 255, 255],
|
watermark_color: [255, 255, 255, 255],
|
||||||
watermark_font_family: String::new(),
|
watermark_font_family: String::new(),
|
||||||
watermark_use_image: false,
|
watermark_use_image: false,
|
||||||
|
watermark_tiled: false,
|
||||||
|
watermark_margin: 10,
|
||||||
|
watermark_scale: 20.0,
|
||||||
rename_enabled: if remember { sess_state.rename_enabled.unwrap_or(false) } else { false },
|
rename_enabled: if remember { sess_state.rename_enabled.unwrap_or(false) } else { false },
|
||||||
rename_prefix: String::new(),
|
rename_prefix: String::new(),
|
||||||
rename_suffix: String::new(),
|
rename_suffix: String::new(),
|
||||||
@@ -1683,7 +1689,10 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
|||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
position,
|
position,
|
||||||
opacity: cfg.watermark_opacity,
|
opacity: cfg.watermark_opacity,
|
||||||
scale: 0.2,
|
scale: cfg.watermark_scale / 100.0,
|
||||||
|
rotation: None,
|
||||||
|
tiled: cfg.watermark_tiled,
|
||||||
|
margin: cfg.watermark_margin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if !cfg.watermark_text.is_empty() {
|
} else if !cfg.watermark_text.is_empty() {
|
||||||
@@ -1694,6 +1703,9 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
|||||||
opacity: cfg.watermark_opacity,
|
opacity: cfg.watermark_opacity,
|
||||||
color: cfg.watermark_color,
|
color: cfg.watermark_color,
|
||||||
font_family: if cfg.watermark_font_family.is_empty() { None } else { Some(cfg.watermark_font_family.clone()) },
|
font_family: if cfg.watermark_font_family.is_empty() { None } else { Some(cfg.watermark_font_family.clone()) },
|
||||||
|
rotation: None,
|
||||||
|
tiled: cfg.watermark_tiled,
|
||||||
|
margin: cfg.watermark_margin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2613,7 +2625,10 @@ fn build_preset_from_config(cfg: &JobConfig, name: &str) -> pixstrip_core::prese
|
|||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
position,
|
position,
|
||||||
opacity: cfg.watermark_opacity,
|
opacity: cfg.watermark_opacity,
|
||||||
scale: 0.2,
|
scale: cfg.watermark_scale / 100.0,
|
||||||
|
rotation: None,
|
||||||
|
tiled: cfg.watermark_tiled,
|
||||||
|
margin: cfg.watermark_margin,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if !cfg.watermark_text.is_empty() {
|
} else if !cfg.watermark_text.is_empty() {
|
||||||
@@ -2624,6 +2639,9 @@ fn build_preset_from_config(cfg: &JobConfig, name: &str) -> pixstrip_core::prese
|
|||||||
opacity: cfg.watermark_opacity,
|
opacity: cfg.watermark_opacity,
|
||||||
color: cfg.watermark_color,
|
color: cfg.watermark_color,
|
||||||
font_family: if cfg.watermark_font_family.is_empty() { None } else { Some(cfg.watermark_font_family.clone()) },
|
font_family: if cfg.watermark_font_family.is_empty() { None } else { Some(cfg.watermark_font_family.clone()) },
|
||||||
|
rotation: None,
|
||||||
|
tiled: cfg.watermark_tiled,
|
||||||
|
margin: cfg.watermark_margin,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -496,6 +496,27 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Wire tiled toggle
|
||||||
|
{
|
||||||
|
let jc = state.job_config.clone();
|
||||||
|
tiled_row.connect_active_notify(move |row| {
|
||||||
|
jc.borrow_mut().watermark_tiled = row.is_active();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Wire margin spinner
|
||||||
|
{
|
||||||
|
let jc = state.job_config.clone();
|
||||||
|
margin_row.connect_value_notify(move |row| {
|
||||||
|
jc.borrow_mut().watermark_margin = row.value() as u32;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Wire scale spinner
|
||||||
|
{
|
||||||
|
let jc = state.job_config.clone();
|
||||||
|
scale_row.connect_value_notify(move |row| {
|
||||||
|
jc.borrow_mut().watermark_scale = row.value() as f32;
|
||||||
|
});
|
||||||
|
}
|
||||||
// Wire image chooser button
|
// Wire image chooser button
|
||||||
{
|
{
|
||||||
let jc = state.job_config.clone();
|
let jc = state.job_config.clone();
|
||||||
|
|||||||
Reference in New Issue
Block a user