use crate::processing::options::Watermark;
use crate::processing::transform::{resize_with_algorithm, TransformError};
use bytes::Bytes;
use libvips::{ops, VipsImage};
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum WatermarkError {
#[error(transparent)]
Transform(#[from] TransformError),
#[error("{operation}: {source}")]
Vips {
operation: &'static str,
#[source]
source: libvips::error::Error,
},
}
fn vips(operation: &'static str) -> impl FnOnce(libvips::error::Error) -> WatermarkError {
move |source| WatermarkError::Vips { operation, source }
}
#[derive(Clone)]
pub struct PreparedWatermark {
bytes: Bytes,
width: i32,
height: i32,
bands: i32,
format: ops::BandFormat,
interpretation: ops::Interpretation,
xres: f64,
yres: f64,
}
impl PreparedWatermark {
fn to_image(&self) -> Result<VipsImage, WatermarkError> {
let raw = VipsImage::new_from_memory(&self.bytes, self.width, self.height, self.bands, self.format)
.map_err(vips("Failed to load watermark from prepared bytes"))?;
ops::copy_with_opts(
&raw,
&ops::CopyOptions {
width: self.width,
height: self.height,
bands: self.bands,
format: self.format,
coding: ops::Coding::None,
interpretation: self.interpretation,
xres: self.xres,
yres: self.yres,
xoffset: 0,
yoffset: 0,
},
)
.map_err(vips("Failed to restore watermark image header"))
}
}
#[derive(Clone)]
pub struct CachedWatermark {
pub bytes: Bytes,
pub prepared_rgba: Option<PreparedWatermark>,
}
impl CachedWatermark {
pub fn from_bytes(bytes: Bytes) -> Self {
Self {
bytes,
prepared_rgba: None,
}
}
pub fn from_prepared(bytes: Bytes, prepared_rgba: PreparedWatermark) -> Self {
Self {
bytes,
prepared_rgba: Some(prepared_rgba),
}
}
}
pub fn load_watermark_image(watermark_bytes: &[u8]) -> Result<VipsImage, WatermarkError> {
let watermark_img =
VipsImage::new_from_buffer(watermark_bytes, "").map_err(vips("Failed to load watermark image from buffer"))?;
ensure_alpha_channel(watermark_img)
}
pub fn prepare_cached_watermark(bytes: Bytes) -> Result<CachedWatermark, WatermarkError> {
let watermark_img = load_watermark_image(bytes.as_ref())?;
let prepared_rgba = build_prepared_watermark_image(watermark_img)?;
Ok(CachedWatermark::from_prepared(bytes, prepared_rgba))
}
pub fn apply_watermark(
img: VipsImage,
watermark: &CachedWatermark,
watermark_opts: &Watermark,
resizing_algorithm: Option<&str>,
) -> Result<VipsImage, WatermarkError> {
let watermark_img = resolve_watermark_image(watermark)?;
let factor = (img.get_width() as f64 / 4.0) / watermark_img.get_width() as f64;
let watermark_resized = resize_with_algorithm(
&watermark_img,
factor,
None,
resizing_algorithm,
"Failed to resize watermark",
)?;
let watermark_with_alpha = ensure_alpha_channel(watermark_resized)?;
let multipliers = &mut [1.0, 1.0, 1.0, watermark_opts.opacity as f64];
let adders = &mut [0.0, 0.0, 0.0, 0.0];
let watermark_with_opacity = ops::linear(&watermark_with_alpha, multipliers, adders)
.map_err(vips("Failed to apply opacity to watermark"))?;
let (x, y) = calculate_watermark_position(&img, &watermark_with_opacity, &watermark_opts.position);
let bg = &mut [0.0, 0.0, 0.0, 0.0]; let options = ops::EmbedOptions {
extend: ops::Extend::Background,
background: bg.to_vec(),
};
let watermark_on_canvas = ops::embed_with_opts(
&watermark_with_opacity,
x as i32,
y as i32,
img.get_width(),
img.get_height(),
&options,
)
.map_err(vips("Failed to embed watermark on canvas"))?;
ops::composite_2(&img, &watermark_on_canvas, ops::BlendMode::Over).map_err(vips("Failed to composite watermark"))
}
fn resolve_watermark_image(watermark: &CachedWatermark) -> Result<VipsImage, WatermarkError> {
if let Some(prepared_rgba) = &watermark.prepared_rgba {
return prepared_rgba.to_image();
}
load_watermark_image(watermark.bytes.as_ref())
}
fn ensure_alpha_channel(watermark_img: VipsImage) -> Result<VipsImage, WatermarkError> {
if watermark_img.get_bands() == 4 || watermark_img.get_bands() == 2 {
return Ok(watermark_img);
}
ops::bandjoin_const(&watermark_img, &mut [255.0]).map_err(vips("Failed to add alpha to watermark"))
}
fn build_prepared_watermark_image(watermark_img: VipsImage) -> Result<PreparedWatermark, WatermarkError> {
let format = watermark_img
.get_format()
.map_err(vips("Failed to determine watermark format"))?;
let interpretation = watermark_img
.get_interpretation()
.map_err(vips("Failed to determine watermark interpretation"))?;
let prepared = PreparedWatermark {
bytes: Bytes::from(watermark_img.image_write_to_memory()),
width: watermark_img.get_width(),
height: watermark_img.get_height(),
bands: watermark_img.get_bands(),
format,
interpretation,
xres: watermark_img.get_xres(),
yres: watermark_img.get_yres(),
};
Ok(prepared)
}
fn calculate_watermark_position(main_img: &VipsImage, watermark_img: &VipsImage, position: &str) -> (u32, u32) {
let main_w = main_img.get_width() as u32;
let main_h = main_img.get_height() as u32;
let wm_w = watermark_img.get_width() as u32;
let wm_h = watermark_img.get_height() as u32;
let margin = (main_w.min(main_h) as f32 * 0.05).round() as u32;
match position {
"no" => ((main_w - wm_w) / 2, margin),
"so" => ((main_w - wm_w) / 2, main_h - wm_h - margin),
"ea" => (main_w - wm_w - margin, (main_h - wm_h) / 2),
"we" => (margin, (main_h - wm_h) / 2),
"nowe" => (margin, margin),
"noea" => (main_w - wm_w - margin, margin),
"sowe" => (margin, main_h - wm_h - margin),
"soea" => (main_w - wm_w - margin, main_h - wm_h - margin),
"ce" => ((main_w - wm_w) / 2, (main_h - wm_h) / 2),
_ => ((main_w - wm_w) / 2, (main_h - wm_h) / 2),
}
}