use anyhow::{Context, Result};
use crate::cli::args::AnnotateArgs;
use crate::cli::utils::{parse_colon_spec, parse_color_hex, parse_rect_from_parts};
pub fn annotate(args: &AnnotateArgs) -> Result<()> {
use printwell::annotations::{AnnotationType, Color, add_annotations};
let pdf_data = std::fs::read(&args.input)
.with_context(|| format!("Failed to read input file: {}", args.input))?;
if args.list {
return list_annotations_cmd(&pdf_data, args);
}
if args.remove {
return remove_annotations_cmd(&pdf_data, args);
}
let mut annotations_to_add = Vec::new();
let default_color = Color::from_hex(&args.color).unwrap_or(Color::yellow());
for spec in &args.highlight {
let annot = parse_annotation_spec(spec, AnnotationType::Highlight, default_color)
.with_context(|| format!("Invalid highlight spec: {spec}"))?;
annotations_to_add.push(annot);
}
for spec in &args.underline {
let annot = parse_annotation_spec(spec, AnnotationType::Underline, default_color)
.with_context(|| format!("Invalid underline spec: {spec}"))?;
annotations_to_add.push(annot);
}
for spec in &args.strikeout {
let annot = parse_annotation_spec(spec, AnnotationType::Strikeout, default_color)
.with_context(|| format!("Invalid strikeout spec: {spec}"))?;
annotations_to_add.push(annot);
}
for spec in &args.square {
let annot = parse_annotation_spec(spec, AnnotationType::Square, default_color)
.with_context(|| format!("Invalid square spec: {spec}"))?;
annotations_to_add.push(annot);
}
for spec in &args.note {
let annot = parse_note_spec(spec, default_color)
.with_context(|| format!("Invalid note spec: {spec}"))?;
annotations_to_add.push(annot);
}
if annotations_to_add.is_empty() {
anyhow::bail!(
"No annotations specified. Use --highlight, --note, --underline, --strikeout, or --square"
);
}
let output_path = args
.output
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--output is required when adding annotations"))?;
let result = add_annotations(&pdf_data, &annotations_to_add)
.context("Failed to add annotations to PDF")?;
std::fs::write(output_path, &result)
.with_context(|| format!("Failed to write output file: {output_path}"))?;
eprintln!(
"Added {} annotations, written to: {}",
annotations_to_add.len(),
output_path
);
Ok(())
}
fn list_annotations_cmd(pdf_data: &[u8], args: &AnnotateArgs) -> Result<()> {
use printwell::annotations::list_annotations;
let annotations = list_annotations(pdf_data).context("Failed to list annotations")?;
match args.format.as_str() {
"json" => {
let json = serde_json::to_string_pretty(
&annotations
.iter()
.map(|a| {
serde_json::json!({
"type": format!("{:?}", a.annotation_type),
"page": a.page,
"rect": {
"x": a.rect.x,
"y": a.rect.y,
"width": a.rect.width,
"height": a.rect.height,
},
"contents": a.contents,
"author": a.author,
})
})
.collect::<Vec<_>>(),
)
.context("Failed to serialize annotations")?;
println!("{json}");
}
_ => {
if annotations.is_empty() {
println!("No annotations found in document.");
} else {
println!("Found {} annotation(s):\n", annotations.len());
for (i, a) in annotations.iter().enumerate() {
println!("#{}: {:?} on page {}", i + 1, a.annotation_type, a.page);
println!(
" Rect: ({:.1}, {:.1}) {}x{}",
a.rect.x, a.rect.y, a.rect.width, a.rect.height
);
if let Some(ref contents) = a.contents {
println!(" Contents: {contents}");
}
if let Some(ref author) = a.author {
println!(" Author: {author}");
}
println!();
}
}
}
}
Ok(())
}
fn remove_annotations_cmd(pdf_data: &[u8], args: &AnnotateArgs) -> Result<()> {
use printwell::annotations::remove_annotations;
let output_path = args
.output
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--output is required when removing annotations"))?;
let result = remove_annotations(pdf_data, args.page, None)
.context("Failed to remove annotations from PDF")?;
std::fs::write(output_path, &result)
.with_context(|| format!("Failed to write output file: {output_path}"))?;
eprintln!("Removed annotations, written to: {output_path}");
Ok(())
}
fn parse_annotation_spec(
spec: &str,
annotation_type: printwell::annotations::AnnotationType,
default_color: printwell::annotations::Color,
) -> Result<printwell::annotations::Annotation> {
use printwell::annotations::{Annotation, Color, Rect};
let parts = parse_colon_spec(spec, 5, "page:x:y:width:height[:color]")?;
let page: u32 = parts[0]
.parse()
.with_context(|| format!("Invalid page number: {}", parts[0]))?;
let (pos_x, pos_y, width, height) = parse_rect_from_parts(&parts, 1)?;
let color = if parts.len() > 5 {
let (red, green, blue) = parse_color_hex(parts[5]).unwrap_or((
default_color.r,
default_color.g,
default_color.b,
));
Color::new(red, green, blue, 255)
} else {
default_color
};
Ok(Annotation::builder()
.annotation_type(annotation_type)
.page(page)
.rect(Rect::new(pos_x, pos_y, width, height))
.color(color)
.build())
}
fn parse_note_spec(
spec: &str,
default_color: printwell::annotations::Color,
) -> Result<printwell::annotations::Annotation> {
use printwell::annotations::{Annotation, AnnotationType, Rect};
let parts = parse_colon_spec(spec, 4, "page:x:y:contents")?;
let page: u32 = parts[0]
.parse()
.with_context(|| format!("Invalid page number: {}", parts[0]))?;
let pos_x: f32 = parts[1]
.parse()
.with_context(|| format!("Invalid x coordinate: {}", parts[1]))?;
let pos_y: f32 = parts[2]
.parse()
.with_context(|| format!("Invalid y coordinate: {}", parts[2]))?;
let contents = parts[3..].join(":");
Ok(Annotation::builder()
.annotation_type(AnnotationType::Text)
.page(page)
.rect(Rect::new(pos_x, pos_y, 24.0, 24.0))
.color(default_color)
.contents(contents)
.build())
}