use crate::{
DriverCallback, DriverCategory, DriverContext, DriverError, DriverResult, file_exists,
types::{Driver, DriverParameter},
};
use image::GenericImageView;
use serde_json::{Value, json};
use std::collections::HashMap;
use tracing::{debug, info};
#[derive(Debug)]
pub struct ImageWatermarkDriver;
#[async_trait::async_trait]
impl Driver for ImageWatermarkDriver {
fn name(&self) -> &str {
return "image_watermark";
}
fn description(&self) -> &str {
return "Add text or image watermark to an image";
}
fn usage_hint(&self) -> &str {
return "Use this skill to add watermarks to images. \
You can add text watermarks or overlay another image as a watermark.";
}
fn parameters(&self) -> Vec<DriverParameter> {
return vec![
DriverParameter {
name: "source".to_string(),
param_type: "string".to_string(),
description: "Source image file path".to_string(),
required: true,
default: None,
example: Some(Value::String("/path/to/input.jpg".to_string())),
enum_values: None,
},
DriverParameter {
name: "destination".to_string(),
param_type: "string".to_string(),
description: "Destination file path".to_string(),
required: true,
default: None,
example: Some(Value::String("/path/to/output.jpg".to_string())),
enum_values: None,
},
DriverParameter {
name: "text".to_string(),
param_type: "string".to_string(),
description: "Text to use as watermark (use with watermark_type=text)".to_string(),
required: false,
default: None,
example: Some(Value::String("© 2024".to_string())),
enum_values: None,
},
DriverParameter {
name: "watermark_image".to_string(),
param_type: "string".to_string(),
description: "Path to watermark image (use with watermark_type=image)".to_string(),
required: false,
default: None,
example: Some(Value::String("/path/to/logo.png".to_string())),
enum_values: None,
},
DriverParameter {
name: "watermark_type".to_string(),
param_type: "string".to_string(),
description: "Type of watermark: 'text' or 'image'".to_string(),
required: true,
default: None,
example: Some(Value::String("text".to_string())),
enum_values: Some(vec!["text".to_string(), "image".to_string()]),
},
DriverParameter {
name: "position".to_string(),
param_type: "string".to_string(),
description: "Position: top-left, top-right, bottom-left, bottom-right, center".to_string(),
required: false,
default: Some(Value::String("bottom-right".to_string())),
example: Some(Value::String("center".to_string())),
enum_values: Some(vec![
"top-left".to_string(),
"top-right".to_string(),
"bottom-left".to_string(),
"bottom-right".to_string(),
"center".to_string(),
]),
},
DriverParameter {
name: "opacity".to_string(),
param_type: "number".to_string(),
description: "Opacity of watermark (0.0 to 1.0)".to_string(),
required: false,
default: Some(json!(0.5)),
example: Some(json!(0.7)),
enum_values: None,
},
];
}
fn example_call(&self) -> DriverResult<Value> {
return Ok(json!({
"action": "image_watermark",
"parameters": {
"source": "/photos/photo.jpg",
"destination": "/photos/watermarked.jpg",
"text": "© 2024",
"watermark_type": "text",
"position": "bottom-right",
"opacity": 0.7
}
}));
}
fn example_output(&self) -> String {
return "Added text watermark to image".to_string();
}
fn category(&self) -> DriverCategory {
return DriverCategory::Media;
}
async fn execute(
&self,
parameters: &HashMap<String, Value>,
callback: Option<&dyn DriverCallback>,
context: Option<&DriverContext>,
) -> DriverResult<String> {
debug!("Executing image_watermark driver");
let task_id = context.as_ref().and_then(|c| c.task_id()).map(String::from);
let driver_index = context.as_ref().and_then(|c| c.driver_index());
let step_name = context.as_ref().and_then(|c| c.driver_name()).map(String::from);
if let Some(cb) = callback {
cb.on_start(task_id.clone(), driver_index, step_name);
cb.on_log(task_id.clone(), driver_index, Some("Starting image watermark operation".to_string()));
cb.on_progress(task_id.clone(), driver_index, Some(10), None);
}
let source = parameters.get("source").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("source"))?;
let destination = parameters.get("destination").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("destination"))?;
let watermark_type =
parameters.get("watermark_type").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("watermark_type"))?;
let position = parameters.get("position").and_then(|v| v.as_str()).unwrap_or("bottom-right");
let opacity = parameters.get("opacity").and_then(|v| v.as_f64()).unwrap_or(0.5).clamp(0.0, 1.0);
debug!("Adding watermark: source={}, type={}, position={}, opacity={}", source, watermark_type, position, opacity);
if let Some(cb) = callback {
cb.on_log(
task_id.clone(),
driver_index,
Some(format!(
"Source: {}, destination: {}, type: {}, position: {}, opacity: {}",
source, destination, watermark_type, position, opacity
)),
);
cb.on_progress(task_id.clone(), driver_index, Some(20), None);
}
if !file_exists(source) {
return Err(DriverError::execution(format!("Source image not found: {}", source)));
}
if let Some(cb) = callback {
cb.on_log(task_id.clone(), driver_index, Some(format!("Source file verified: {}", source)));
cb.on_progress(task_id.clone(), driver_index, Some(30), None);
}
let img = image::open(source).map_err(|e| DriverError::execution(format!("Failed to open image '{}': {}", source, e)))?;
if let Some(cb) = callback {
cb.on_log(task_id.clone(), driver_index, Some(format!("Adding {} watermark...", watermark_type)));
cb.on_progress(task_id.clone(), driver_index, Some(50), None);
}
let result = match watermark_type {
"text" => {
let text = parameters.get("text").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("text"))?;
add_text_watermark(&img, text, position, opacity)?
}
"image" => {
let watermark_path =
parameters.get("watermark_image").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("watermark_image"))?;
if !file_exists(watermark_path) {
return Err(DriverError::execution(format!("Watermark image not found: {}", watermark_path)));
}
let watermark = image::open(watermark_path).map_err(|e| DriverError::execution(format!("Failed to open watermark: {}", e)))?;
add_image_watermark(&img, &watermark, position, opacity)?
}
_ => return Err(DriverError::execution(format!("Unknown watermark type: {}", watermark_type))),
};
if let Some(cb) = callback {
cb.on_log(task_id.clone(), driver_index, Some("Saving watermarked image...".to_string()));
cb.on_progress(task_id.clone(), driver_index, Some(80), None);
}
result.save(destination).map_err(|e| DriverError::execution(format!("Failed to save watermarked image: {}", e)))?;
let result_msg = format!("Added {} watermark to image", watermark_type);
if let Some(cb) = callback {
cb.on_log(task_id.clone(), driver_index, Some(format!("Result: {}", result_msg)));
cb.on_progress(task_id.clone(), driver_index, Some(100), None);
cb.on_complete(task_id.clone(), driver_index, Some("image_watermark".to_string()), Some(result_msg.clone()));
}
info!("Image watermark applied: {}", watermark_type);
return Ok(result_msg);
}
fn validate(&self, parameters: &HashMap<String, Value>) -> DriverResult<()> {
parameters.get("source").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("source"))?;
parameters.get("destination").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("destination"))?;
parameters.get("watermark_type").and_then(|v| v.as_str()).ok_or_else(|| DriverError::missing_parameter("watermark_type"))?;
return Ok(());
}
}
fn add_text_watermark(img: &image::DynamicImage, text: &str, position: &str, opacity: f64) -> DriverResult<image::DynamicImage> {
use image::{ImageBuffer, Rgba};
let (w, h) = img.dimensions();
let mut output: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::new(w, h);
for y in 0..h {
for x in 0..w {
let px = img.get_pixel(x, y);
output.put_pixel(x, y, px);
}
}
let (text_x, text_y) = calculate_position(w, h, 200, 50, position);
let opacity_byte = (opacity * 255.0) as u8;
for dy in 0..60 {
for dx in 0..(text.len() as u32 * 12 + 20) {
let x = text_x + dx;
let y = text_y + dy;
if x < w && y < h {
let px = output.get_pixel_mut(x, y);
px[0] = px[0].saturating_add((255 - px[0]) * opacity_byte / 255);
px[1] = px[1].saturating_add((255 - px[1]) * opacity_byte / 255);
px[2] = px[2].saturating_add((255 - px[2]) * opacity_byte / 255);
}
}
}
return Ok(image::DynamicImage::ImageRgba8(output));
}
fn add_image_watermark(
img: &image::DynamicImage,
watermark: &image::DynamicImage,
position: &str,
opacity: f64,
) -> DriverResult<image::DynamicImage> {
use image::{ImageBuffer, Rgba};
let (w, h) = img.dimensions();
let (ww, wh) = watermark.dimensions();
let mut output: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::new(w, h);
for y in 0..h {
for x in 0..w {
let px = img.get_pixel(x, y);
output.put_pixel(x, y, px);
}
}
let (pos_x, pos_y) = calculate_position(w, h, ww, wh, position);
for dy in 0..wh.min(h - pos_y) {
for dx in 0..ww.min(w - pos_x) {
let wx = pos_x + dx;
let wy = pos_y + dy;
let wp = watermark.get_pixel(dx, dy);
let op = output.get_pixel_mut(wx, wy);
if wp[3] > 0 {
let alpha = wp[3] as f32 / 255.0 * opacity as f32;
op[0] = (op[0] as f32 * (1.0 - alpha) + wp[0] as f32 * alpha) as u8;
op[1] = (op[1] as f32 * (1.0 - alpha) + wp[1] as f32 * alpha) as u8;
op[2] = (op[2] as f32 * (1.0 - alpha) + wp[2] as f32 * alpha) as u8;
}
}
}
return Ok(image::DynamicImage::ImageRgba8(output));
}
fn calculate_position(total_w: u32, total_h: u32, item_w: u32, item_h: u32, position: &str) -> (u32, u32) {
let margin = 20;
match position {
"top-left" => (margin, margin),
"top-right" => (total_w - item_w - margin, margin),
"bottom-left" => (margin, total_h - item_h - margin),
"center" => ((total_w - item_w) / 2, (total_h - item_h) / 2),
_ => (total_w - item_w - margin, total_h - item_h - margin),
}
}