use crate::Result;
use typed_builder::TypedBuilder;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnnotationType {
Highlight,
Underline,
Strikeout,
Squiggly,
Text,
FreeText,
Line,
Square,
Circle,
Ink,
Stamp,
Link,
}
impl AnnotationType {
#[must_use]
pub const fn to_pdfium_type(&self) -> i32 {
match self {
Self::Highlight => 9, Self::Underline => 10, Self::Strikeout => 11, Self::Squiggly => 12, Self::Text => 1, Self::FreeText => 3, Self::Line => 4, Self::Square => 5, Self::Circle => 6, Self::Ink => 15, Self::Stamp => 13, Self::Link => 2, }
}
#[must_use]
pub const fn from_pdfium_type(t: i32) -> Option<Self> {
match t {
9 => Some(Self::Highlight),
10 => Some(Self::Underline),
11 => Some(Self::Strikeout),
12 => Some(Self::Squiggly),
1 => Some(Self::Text),
3 => Some(Self::FreeText),
4 => Some(Self::Line),
5 => Some(Self::Square),
6 => Some(Self::Circle),
15 => Some(Self::Ink),
13 => Some(Self::Stamp),
2 => Some(Self::Link),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, TypedBuilder)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl Rect {
#[must_use]
pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
x,
y,
width,
height,
}
}
}
#[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::new(r, g, b, 255)
}
#[must_use]
pub const fn yellow() -> Self {
Self::rgb(255, 255, 0)
}
#[must_use]
pub const fn red() -> Self {
Self::rgb(255, 0, 0)
}
#[must_use]
pub const fn green() -> Self {
Self::rgb(0, 255, 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 to_rgba(&self) -> u32 {
((self.r as u32) << 24) | ((self.g as u32) << 16) | ((self.b as u32) << 8) | (self.a as u32)
}
#[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
}
}
}
impl Default for Color {
fn default() -> Self {
Self::yellow()
}
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct Annotation {
pub annotation_type: AnnotationType,
pub page: u32,
pub rect: Rect,
#[builder(default)]
pub color: Color,
#[builder(default = 1.0)]
pub opacity: f32,
#[builder(default, setter(into, strip_option))]
pub contents: Option<String>,
#[builder(default, setter(into, strip_option))]
pub author: Option<String>,
#[builder(default, setter(into, strip_option))]
pub subject: Option<String>,
#[builder(default, setter(into, strip_option))]
pub text: Option<String>,
#[builder(default = 12.0)]
pub font_size: f32,
#[builder(default)]
pub points: Vec<(f32, f32)>,
}
#[derive(Debug, Clone)]
pub struct AnnotationInfo {
pub annotation_type: AnnotationType,
pub page: u32,
pub rect: Rect,
pub color: Color,
pub opacity: f32,
pub contents: Option<String>,
pub author: Option<String>,
}
pub fn add_annotations(pdf_data: &[u8], annotations: &[Annotation]) -> Result<Vec<u8>> {
if annotations.is_empty() {
return Ok(pdf_data.to_vec());
}
let annotation_defs: Vec<printwell_sys::AnnotationDef> = annotations
.iter()
.map(|a| printwell_sys::AnnotationDef {
annotation_type: a.annotation_type.to_pdfium_type(),
page: i32::try_from(a.page).unwrap_or(i32::MAX),
x: a.rect.x,
y: a.rect.y,
width: a.rect.width,
height: a.rect.height,
color: a.color.to_rgba(),
opacity: a.opacity,
contents: a.contents.clone().unwrap_or_default(),
author: a.author.clone().unwrap_or_default(),
text: a.text.clone().unwrap_or_default(),
font_size: a.font_size,
})
.collect();
let result =
printwell_sys::ffi::pdf_add_annotations(pdf_data, &annotation_defs).map_err(|e| {
crate::AnnotationError::Operation(format!("Failed to add annotations: {e}"))
})?;
Ok(result)
}
pub fn list_annotations(pdf_data: &[u8]) -> Result<Vec<AnnotationInfo>> {
let annotation_defs = printwell_sys::ffi::pdf_list_annotations(pdf_data).map_err(|e| {
crate::AnnotationError::Operation(format!("Failed to list annotations: {e}"))
})?;
let annotations = annotation_defs
.iter()
.filter_map(|a| {
let annotation_type = AnnotationType::from_pdfium_type(a.annotation_type)?;
Some(AnnotationInfo {
annotation_type,
page: u32::try_from(a.page).unwrap_or(0),
rect: Rect::new(a.x, a.y, a.width, a.height),
color: Color::new(
((a.color >> 24) & 0xFF) as u8,
((a.color >> 16) & 0xFF) as u8,
((a.color >> 8) & 0xFF) as u8,
(a.color & 0xFF) as u8,
),
opacity: a.opacity,
contents: if a.contents.is_empty() {
None
} else {
Some(a.contents.clone())
},
author: if a.author.is_empty() {
None
} else {
Some(a.author.clone())
},
})
})
.collect();
Ok(annotations)
}
pub fn remove_annotations(
pdf_data: &[u8],
page: Option<u32>,
annotation_types: Option<&[AnnotationType]>,
) -> Result<Vec<u8>> {
let page_filter: i32 = page.map_or(-1, |p| i32::try_from(p).unwrap_or(i32::MAX));
let type_filter: Vec<i32> = annotation_types
.map(|types| types.iter().map(AnnotationType::to_pdfium_type).collect())
.unwrap_or_default();
let result = printwell_sys::ffi::pdf_remove_annotations(pdf_data, page_filter, &type_filter)
.map_err(|e| {
crate::AnnotationError::Operation(format!("Failed to remove annotations: {e}"))
})?;
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_from_hex() {
let c = Color::from_hex("#FF0000").unwrap();
assert_eq!(c.r, 255);
assert_eq!(c.g, 0);
assert_eq!(c.b, 0);
assert_eq!(c.a, 255);
let c = Color::from_hex("00FF00").unwrap();
assert_eq!(c.r, 0);
assert_eq!(c.g, 255);
assert_eq!(c.b, 0);
}
#[test]
fn test_annotation_type_conversion() {
let types = [
AnnotationType::Highlight,
AnnotationType::Underline,
AnnotationType::Text,
AnnotationType::FreeText,
];
for t in &types {
let pdfium = t.to_pdfium_type();
let back = AnnotationType::from_pdfium_type(pdfium);
assert_eq!(back, Some(*t));
}
}
}