use crate::Result;
use typed_builder::TypedBuilder;
#[derive(Debug, Clone, Copy, Default)]
pub enum Position {
#[default]
Center,
TopLeft,
TopCenter,
TopRight,
MiddleLeft,
MiddleRight,
BottomLeft,
BottomCenter,
BottomRight,
Custom(f32, f32),
}
impl Position {
#[must_use]
pub fn to_coords(
&self,
page_width: f32,
page_height: f32,
watermark_width: f32,
watermark_height: f32,
) -> (f32, f32) {
let margin = 36.0; match self {
Self::Center => (
(page_width - watermark_width) / 2.0,
(page_height - watermark_height) / 2.0,
),
Self::TopLeft => (margin, page_height - watermark_height - margin),
Self::TopCenter => (
(page_width - watermark_width) / 2.0,
page_height - watermark_height - margin,
),
Self::TopRight => (
page_width - watermark_width - margin,
page_height - watermark_height - margin,
),
Self::MiddleLeft => (margin, (page_height - watermark_height) / 2.0),
Self::MiddleRight => (
page_width - watermark_width - margin,
(page_height - watermark_height) / 2.0,
),
Self::BottomLeft => (margin, margin),
Self::BottomCenter => ((page_width - watermark_width) / 2.0, margin),
Self::BottomRight => (page_width - watermark_width - margin, margin),
Self::Custom(x, y) => (*x, *y),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum Layer {
#[default]
Background,
Foreground,
}
#[derive(Debug, Clone, Default)]
pub enum PageSelection {
#[default]
All,
Pages(Vec<u32>),
Range(u32, u32),
Odd,
Even,
First,
Last,
}
impl PageSelection {
#[must_use]
pub fn includes(&self, page: u32, total_pages: u32) -> bool {
match self {
Self::All => true,
Self::Pages(pages) => pages.contains(&page),
Self::Range(start, end) => page >= *start && page <= *end,
Self::Odd => page % 2 == 1,
Self::Even => page.is_multiple_of(2),
Self::First => page == 1,
Self::Last => page == total_pages,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Color {
#[must_use]
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
#[must_use]
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b, a: 255 }
}
#[must_use]
pub fn from_hex(hex: &str) -> Option<Self> {
let hex = hex.trim_start_matches('#');
if hex.len() == 6 {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some(Self::rgb(r, g, b))
} else if hex.len() == 8 {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
Some(Self::new(r, g, b, a))
} else {
None
}
}
#[must_use]
pub const fn red() -> Self {
Self::rgb(255, 0, 0)
}
#[must_use]
pub const fn green() -> Self {
Self::rgb(0, 128, 0)
}
#[must_use]
pub const fn blue() -> Self {
Self::rgb(0, 0, 255)
}
#[must_use]
pub const fn black() -> Self {
Self::rgb(0, 0, 0)
}
#[must_use]
pub const fn white() -> Self {
Self::rgb(255, 255, 255)
}
#[must_use]
pub const fn gray() -> Self {
Self::rgb(128, 128, 128)
}
#[must_use]
pub const fn to_argb(&self) -> u32 {
((self.a as u32) << 24) | ((self.r as u32) << 16) | ((self.g as u32) << 8) | (self.b as u32)
}
}
impl Default for Color {
fn default() -> Self {
Self::gray()
}
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct Watermark {
#[builder(default, setter(into, strip_option))]
pub text: Option<String>,
#[builder(default, setter(strip_option))]
pub image: Option<Vec<u8>>,
#[builder(default)]
pub position: Position,
#[builder(default = 0.0)]
pub rotation: f32,
#[builder(default = 0.5)]
pub opacity: f32,
#[builder(default = 72.0)]
pub font_size: f32,
#[builder(default = "Helvetica".into(), setter(into))]
pub font_name: String,
#[builder(default)]
pub color: Color,
#[builder(default)]
pub layer: Layer,
#[builder(default)]
pub pages: PageSelection,
#[builder(default = 1.0)]
pub scale: f32,
}
impl Watermark {
#[must_use]
pub const fn is_text(&self) -> bool {
self.text.is_some()
}
#[must_use]
pub const fn is_image(&self) -> bool {
self.image.is_some()
}
}
pub fn add_watermark(pdf_data: &[u8], watermark: &Watermark) -> Result<Vec<u8>> {
if watermark.text.is_none() && watermark.image.is_none() {
return Err(crate::WatermarkError::NoContent.into());
}
if watermark.text.is_some() && watermark.image.is_some() {
return Err(crate::WatermarkError::BothContentTypes.into());
}
if watermark.opacity < 0.0 || watermark.opacity > 1.0 {
return Err(crate::WatermarkError::InvalidOpacity {
value: watermark.opacity,
}
.into());
}
let wm_def = printwell_sys::WatermarkDef {
text: watermark.text.clone().unwrap_or_default(),
image: watermark.image.clone().unwrap_or_default(),
x: 0.0, y: 0.0,
rotation: watermark.rotation,
opacity: watermark.opacity,
font_size: watermark.font_size,
font_name: watermark.font_name.clone(),
color: watermark.color.to_argb(),
behind_content: matches!(watermark.layer, Layer::Background),
pages: match &watermark.pages {
PageSelection::All => vec![],
PageSelection::Pages(pages) => pages
.iter()
.map(|&p| i32::try_from(p).unwrap_or(i32::MAX))
.collect(),
PageSelection::Range(start, end) => (*start..=*end)
.map(|p| i32::try_from(p).unwrap_or(i32::MAX))
.collect(),
PageSelection::Odd => vec![-1], PageSelection::Even => vec![-2],
PageSelection::First => vec![-3],
PageSelection::Last => vec![-4],
},
position_type: match watermark.position {
Position::Center => 0,
Position::TopLeft => 1,
Position::TopCenter => 2,
Position::TopRight => 3,
Position::MiddleLeft => 4,
Position::MiddleRight => 5,
Position::BottomLeft => 6,
Position::BottomCenter => 7,
Position::BottomRight => 8,
Position::Custom(_, _) => 9,
},
custom_x: match watermark.position {
Position::Custom(x, _) => x,
_ => 0.0,
},
custom_y: match watermark.position {
Position::Custom(_, y) => y,
_ => 0.0,
},
scale: watermark.scale,
};
let result = printwell_sys::ffi::pdf_add_watermark(pdf_data, &wm_def)
.map_err(|e| crate::WatermarkError::AddFailed(e.to_string()))?;
Ok(result)
}
pub fn add_watermarks(pdf_data: &[u8], watermarks: &[Watermark]) -> Result<Vec<u8>> {
let mut result = pdf_data.to_vec();
for watermark in watermarks {
result = add_watermark(&result, watermark)?;
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_position_to_coords() {
let pos = Position::Center;
let (x, y) = pos.to_coords(612.0, 792.0, 100.0, 50.0);
assert!((x - 256.0).abs() < f32::EPSILON);
assert!((y - 371.0).abs() < f32::EPSILON);
}
#[test]
fn test_color_from_hex() {
let color = Color::from_hex("#FF0000").unwrap();
assert_eq!(color.r, 255);
assert_eq!(color.g, 0);
assert_eq!(color.b, 0);
let color = Color::from_hex("00FF00").unwrap();
assert_eq!(color.r, 0);
assert_eq!(color.g, 255);
assert_eq!(color.b, 0);
}
#[test]
fn test_page_selection() {
assert!(PageSelection::All.includes(1, 10));
assert!(PageSelection::All.includes(10, 10));
assert!(PageSelection::Odd.includes(1, 10));
assert!(!PageSelection::Odd.includes(2, 10));
assert!(!PageSelection::Even.includes(1, 10));
assert!(PageSelection::Even.includes(2, 10));
assert!(PageSelection::First.includes(1, 10));
assert!(!PageSelection::First.includes(2, 10));
assert!(!PageSelection::Last.includes(1, 10));
assert!(PageSelection::Last.includes(10, 10));
assert!(PageSelection::Range(3, 5).includes(3, 10));
assert!(PageSelection::Range(3, 5).includes(4, 10));
assert!(PageSelection::Range(3, 5).includes(5, 10));
assert!(!PageSelection::Range(3, 5).includes(2, 10));
assert!(!PageSelection::Range(3, 5).includes(6, 10));
}
}