use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
}
impl Color {
pub fn from_hex(hex: &str) -> Option<Self> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return None;
}
let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.0;
let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.0;
let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.0;
Some(Color { r, g, b })
}
#[allow(dead_code)]
pub fn to_pdf_array(&self) -> Vec<lopdf::Object> {
vec![
lopdf::Object::Real(self.r),
lopdf::Object::Real(self.g),
lopdf::Object::Real(self.b),
]
}
}
impl Default for Color {
fn default() -> Self {
Color { r: 1.0, g: 1.0, b: 0.0 } }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rect {
pub left: f64,
pub bottom: f64,
pub right: f64,
pub top: f64,
}
impl Rect {
pub fn from_string(s: &str) -> Option<Self> {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() != 4 {
return None;
}
Some(Rect {
left: parts[0].trim().parse().ok()?,
bottom: parts[1].trim().parse().ok()?,
right: parts[2].trim().parse().ok()?,
top: parts[3].trim().parse().ok()?,
})
}
#[allow(dead_code)]
pub fn to_pdf_array(&self) -> Vec<lopdf::Object> {
vec![
lopdf::Object::Real(self.left as f32),
lopdf::Object::Real(self.bottom as f32),
lopdf::Object::Real(self.right as f32),
lopdf::Object::Real(self.top as f32),
]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnnotationBase {
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default)]
pub page: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub rect: Option<Rect>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contents: Option<String>,
#[serde(rename = "creationdate", skip_serializing_if = "Option::is_none")]
pub creation_date: Option<String>,
#[serde(rename = "date", skip_serializing_if = "Option::is_none")]
pub modification_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(default = "default_opacity")]
pub opacity: f32,
#[serde(default)]
pub flags: u32,
#[serde(flatten)]
pub extra: HashMap<String, String>,
}
fn default_opacity() -> f32 { 1.0 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(default)]
pub open: bool,
#[serde(rename = "icon", default = "default_icon")]
pub icon_type: String,
}
fn default_icon() -> String { "Note".to_string() }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HighlightAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(rename = "coords", skip_serializing_if = "Option::is_none")]
pub coords: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnderlineAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(rename = "coords", skip_serializing_if = "Option::is_none")]
pub coords: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrikeOutAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(rename = "coords", skip_serializing_if = "Option::is_none")]
pub coords: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SquigglyAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(rename = "coords", skip_serializing_if = "Option::is_none")]
pub coords: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FreeTextAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(rename = "defaultstyle", skip_serializing_if = "Option::is_none")]
pub default_style: Option<String>,
#[serde(rename = "defaultappearance", skip_serializing_if = "Option::is_none")]
pub default_appearance: Option<String>,
#[serde(rename = "TextColor", skip_serializing_if = "Option::is_none")]
pub text_color: Option<String>,
#[serde(default)]
pub align: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SquareAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(default)]
pub width: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CircleAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(default)]
pub width: f32,
#[serde(rename = "interiorcolor", skip_serializing_if = "Option::is_none")]
pub interior_color: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end: Option<String>,
#[serde(rename = "head", default)]
pub head_style: String,
#[serde(rename = "tail", default)]
pub tail_style: String,
#[serde(default = "default_line_width")]
pub width: f32,
}
fn default_line_width() -> f32 { 1.0 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolygonAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(skip_serializing_if = "Option::is_none")]
pub vertices: Option<String>,
#[serde(default = "default_polygon_closed")]
pub is_closed: bool,
}
fn default_polygon_closed() -> bool { true }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InkAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(rename = "inklist", default)]
pub ink_list: Vec<String>,
#[serde(default = "default_line_width")]
pub width: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StampAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(default)]
pub icon: String,
#[serde(rename = "imagedata", skip_serializing_if = "Option::is_none")]
pub image_data: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PopupAnnotation {
#[serde(flatten)]
pub base: AnnotationBase,
#[serde(default)]
pub open: bool,
#[serde(rename = "parent", skip_serializing_if = "Option::is_none")]
pub parent_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Annotation {
Text(TextAnnotation),
Highlight(HighlightAnnotation),
Underline(UnderlineAnnotation),
StrikeOut(StrikeOutAnnotation),
Squiggly(SquigglyAnnotation),
FreeText(FreeTextAnnotation),
Square(SquareAnnotation),
Circle(CircleAnnotation),
Line(LineAnnotation),
Polygon(PolygonAnnotation),
Ink(InkAnnotation),
Stamp(StampAnnotation),
Popup(PopupAnnotation),
}
impl Annotation {
pub fn annotation_type(&self) -> &'static str {
match self {
Annotation::Text(_) => "Text",
Annotation::Highlight(_) => "Highlight",
Annotation::Underline(_) => "Underline",
Annotation::StrikeOut(_) => "StrikeOut",
Annotation::Squiggly(_) => "Squiggly",
Annotation::FreeText(_) => "FreeText",
Annotation::Square(_) => "Square",
Annotation::Circle(_) => "Circle",
Annotation::Line(_) => "Line",
Annotation::Polygon(_) => "Polygon",
Annotation::Ink(_) => "Ink",
Annotation::Stamp(_) => "Stamp",
Annotation::Popup(_) => "Popup",
}
}
pub fn base(&self) -> &AnnotationBase {
match self {
Annotation::Text(a) => &a.base,
Annotation::Highlight(a) => &a.base,
Annotation::Underline(a) => &a.base,
Annotation::StrikeOut(a) => &a.base,
Annotation::Squiggly(a) => &a.base,
Annotation::FreeText(a) => &a.base,
Annotation::Square(a) => &a.base,
Annotation::Circle(a) => &a.base,
Annotation::Line(a) => &a.base,
Annotation::Polygon(a) => &a.base,
Annotation::Ink(a) => &a.base,
Annotation::Stamp(a) => &a.base,
Annotation::Popup(a) => &a.base,
}
}
pub fn page(&self) -> usize {
self.base().page
}
#[allow(dead_code)]
pub fn rect(&self) -> Option<&Rect> {
self.base().rect.as_ref()
}
}