use super::acroform::AcroFormBuilder;
use super::annotation_builder::{AnnotationBuilder, LinkAnnotation};
use super::content_stream::ContentStreamBuilder;
use super::form_fields::{
CheckboxWidget, ComboBoxWidget, FormFieldEntry, ListBoxWidget, PushButtonWidget,
RadioButtonGroup, TextFieldWidget,
};
use super::freetext::FreeTextAnnotation;
use super::ink::InkAnnotation;
use super::object_serializer::ObjectSerializer;
use super::shape_annotations::{LineAnnotation, PolygonAnnotation, ShapeAnnotation};
use super::special_annotations::{
CaretAnnotation, FileAttachmentAnnotation, FileAttachmentIcon, PopupAnnotation,
RedactAnnotation,
};
use super::stamp::{StampAnnotation, StampType};
use super::text_annotations::TextAnnotation;
use super::text_markup::TextMarkupAnnotation;
use crate::annotation_types::{LineEndingStyle, TextAlignment, TextAnnotationIcon, TextMarkupType};
use crate::elements::ContentElement;
use crate::error::Result;
use crate::geometry::Rect;
use crate::object::{Object, ObjectRef};
use std::collections::HashMap;
use std::io::Write;
#[derive(Debug, Clone)]
pub struct PdfWriterConfig {
pub version: String,
pub title: Option<String>,
pub author: Option<String>,
pub subject: Option<String>,
pub keywords: Option<String>,
pub creator: Option<String>,
pub compress: bool,
}
impl Default for PdfWriterConfig {
fn default() -> Self {
Self {
version: "1.7".to_string(),
title: None,
author: None,
subject: None,
keywords: None,
creator: Some("pdf_oxide".to_string()),
compress: false, }
}
}
impl PdfWriterConfig {
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
pub fn with_compress(mut self, compress: bool) -> Self {
self.compress = compress;
self
}
}
fn compress_data(data: &[u8]) -> std::io::Result<Vec<u8>> {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(data)?;
encoder.finish()
}
pub struct PageBuilder<'a> {
writer: &'a mut PdfWriter,
page_index: usize,
}
impl<'a> PageBuilder<'a> {
pub fn add_text(
&mut self,
text: &str,
x: f32,
y: f32,
font_name: &str,
font_size: f32,
) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.content_builder
.begin_text()
.set_font(font_name, font_size)
.text(text, x, y);
self
}
pub fn add_element(&mut self, element: &ContentElement) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.content_builder.add_element(element);
self
}
pub fn add_elements(&mut self, elements: &[ContentElement]) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.content_builder.add_elements(elements);
self
}
pub fn draw_rect(&mut self, x: f32, y: f32, width: f32, height: f32) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.content_builder.end_text();
page.content_builder.rect(x, y, width, height).stroke();
self
}
pub fn add_link(&mut self, link: LinkAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_link(link);
self
}
pub fn link(&mut self, rect: Rect, uri: impl Into<String>) -> &mut Self {
self.add_link(LinkAnnotation::uri(rect, uri))
}
pub fn internal_link(&mut self, rect: Rect, page: usize) -> &mut Self {
self.add_link(LinkAnnotation::goto_page(rect, page))
}
pub fn add_text_markup(&mut self, markup: TextMarkupAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_text_markup(markup);
self
}
pub fn highlight(&mut self, rect: Rect, quad_points: Vec<[f64; 8]>) -> &mut Self {
self.add_text_markup(TextMarkupAnnotation::highlight(rect, quad_points))
}
pub fn highlight_rect(&mut self, rect: Rect) -> &mut Self {
self.add_text_markup(TextMarkupAnnotation::from_rect(TextMarkupType::Highlight, rect))
}
pub fn underline(&mut self, rect: Rect, quad_points: Vec<[f64; 8]>) -> &mut Self {
self.add_text_markup(TextMarkupAnnotation::underline(rect, quad_points))
}
pub fn underline_rect(&mut self, rect: Rect) -> &mut Self {
self.add_text_markup(TextMarkupAnnotation::from_rect(TextMarkupType::Underline, rect))
}
pub fn strikeout(&mut self, rect: Rect, quad_points: Vec<[f64; 8]>) -> &mut Self {
self.add_text_markup(TextMarkupAnnotation::strikeout(rect, quad_points))
}
pub fn strikeout_rect(&mut self, rect: Rect) -> &mut Self {
self.add_text_markup(TextMarkupAnnotation::from_rect(TextMarkupType::StrikeOut, rect))
}
pub fn squiggly(&mut self, rect: Rect, quad_points: Vec<[f64; 8]>) -> &mut Self {
self.add_text_markup(TextMarkupAnnotation::squiggly(rect, quad_points))
}
pub fn squiggly_rect(&mut self, rect: Rect) -> &mut Self {
self.add_text_markup(TextMarkupAnnotation::from_rect(TextMarkupType::Squiggly, rect))
}
pub fn add_text_note(&mut self, note: TextAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_text_note(note);
self
}
pub fn sticky_note(&mut self, rect: Rect, contents: impl Into<String>) -> &mut Self {
self.add_text_note(TextAnnotation::note(rect, contents))
}
pub fn comment(&mut self, rect: Rect, contents: impl Into<String>) -> &mut Self {
self.add_text_note(TextAnnotation::comment(rect, contents))
}
pub fn text_note_with_icon(
&mut self,
rect: Rect,
contents: impl Into<String>,
icon: TextAnnotationIcon,
) -> &mut Self {
self.add_text_note(TextAnnotation::new(rect, contents).with_icon(icon))
}
pub fn add_freetext(&mut self, freetext: FreeTextAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_freetext(freetext);
self
}
pub fn textbox(&mut self, rect: Rect, contents: impl Into<String>) -> &mut Self {
self.add_freetext(FreeTextAnnotation::new(rect, contents))
}
pub fn textbox_styled(
&mut self,
rect: Rect,
contents: impl Into<String>,
font: &str,
size: f32,
) -> &mut Self {
self.add_freetext(FreeTextAnnotation::new(rect, contents).with_font(font, size))
}
pub fn textbox_centered(&mut self, rect: Rect, contents: impl Into<String>) -> &mut Self {
self.add_freetext(
FreeTextAnnotation::new(rect, contents).with_alignment(TextAlignment::Center),
)
}
pub fn callout(
&mut self,
rect: Rect,
contents: impl Into<String>,
callout_points: Vec<f64>,
) -> &mut Self {
self.add_freetext(FreeTextAnnotation::callout(rect, contents, callout_points))
}
pub fn typewriter(&mut self, rect: Rect, contents: impl Into<String>) -> &mut Self {
self.add_freetext(FreeTextAnnotation::typewriter(rect, contents))
}
pub fn add_line(&mut self, line: LineAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_line(line);
self
}
pub fn line(&mut self, start: (f64, f64), end: (f64, f64)) -> &mut Self {
self.add_line(LineAnnotation::new(start, end))
}
pub fn arrow(&mut self, start: (f64, f64), end: (f64, f64)) -> &mut Self {
self.add_line(LineAnnotation::arrow(start, end))
}
pub fn double_arrow(&mut self, start: (f64, f64), end: (f64, f64)) -> &mut Self {
self.add_line(LineAnnotation::double_arrow(start, end))
}
pub fn line_with_endings(
&mut self,
start: (f64, f64),
end: (f64, f64),
start_ending: LineEndingStyle,
end_ending: LineEndingStyle,
) -> &mut Self {
self.add_line(LineAnnotation::new(start, end).with_line_endings(start_ending, end_ending))
}
pub fn add_shape(&mut self, shape: ShapeAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_shape(shape);
self
}
pub fn rectangle(&mut self, rect: Rect) -> &mut Self {
self.add_shape(ShapeAnnotation::square(rect))
}
pub fn rectangle_filled(
&mut self,
rect: Rect,
stroke: (f32, f32, f32),
fill: (f32, f32, f32),
) -> &mut Self {
self.add_shape(
ShapeAnnotation::square(rect)
.with_stroke_color(stroke.0, stroke.1, stroke.2)
.with_fill_color(fill.0, fill.1, fill.2),
)
}
pub fn circle(&mut self, rect: Rect) -> &mut Self {
self.add_shape(ShapeAnnotation::circle(rect))
}
pub fn circle_filled(
&mut self,
rect: Rect,
stroke: (f32, f32, f32),
fill: (f32, f32, f32),
) -> &mut Self {
self.add_shape(
ShapeAnnotation::circle(rect)
.with_stroke_color(stroke.0, stroke.1, stroke.2)
.with_fill_color(fill.0, fill.1, fill.2),
)
}
pub fn add_polygon(&mut self, polygon: PolygonAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_polygon(polygon);
self
}
pub fn polygon(&mut self, vertices: Vec<(f64, f64)>) -> &mut Self {
self.add_polygon(PolygonAnnotation::polygon(vertices))
}
pub fn polygon_filled(
&mut self,
vertices: Vec<(f64, f64)>,
stroke: (f32, f32, f32),
fill: (f32, f32, f32),
) -> &mut Self {
self.add_polygon(
PolygonAnnotation::polygon(vertices)
.with_stroke_color(stroke.0, stroke.1, stroke.2)
.with_fill_color(fill.0, fill.1, fill.2),
)
}
pub fn polyline(&mut self, vertices: Vec<(f64, f64)>) -> &mut Self {
self.add_polygon(PolygonAnnotation::polyline(vertices))
}
pub fn add_ink(&mut self, ink: InkAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_ink(ink);
self
}
pub fn ink(&mut self, stroke: Vec<(f64, f64)>) -> &mut Self {
self.add_ink(InkAnnotation::with_stroke(stroke))
}
pub fn freehand(&mut self, strokes: Vec<Vec<(f64, f64)>>) -> &mut Self {
self.add_ink(InkAnnotation::with_strokes(strokes))
}
pub fn ink_styled(
&mut self,
stroke: Vec<(f64, f64)>,
color: (f32, f32, f32),
line_width: f32,
) -> &mut Self {
self.add_ink(
InkAnnotation::with_stroke(stroke)
.with_stroke_color(color.0, color.1, color.2)
.with_line_width(line_width),
)
}
pub fn add_stamp(&mut self, stamp: StampAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_stamp(stamp);
self
}
pub fn stamp(&mut self, rect: Rect, stamp_type: StampType) -> &mut Self {
self.add_stamp(StampAnnotation::new(rect, stamp_type))
}
pub fn stamp_approved(&mut self, rect: Rect) -> &mut Self {
self.add_stamp(StampAnnotation::approved(rect))
}
pub fn stamp_draft(&mut self, rect: Rect) -> &mut Self {
self.add_stamp(StampAnnotation::draft(rect))
}
pub fn stamp_confidential(&mut self, rect: Rect) -> &mut Self {
self.add_stamp(StampAnnotation::confidential(rect))
}
pub fn stamp_final(&mut self, rect: Rect) -> &mut Self {
self.add_stamp(StampAnnotation::final_stamp(rect))
}
pub fn stamp_not_approved(&mut self, rect: Rect) -> &mut Self {
self.add_stamp(StampAnnotation::not_approved(rect))
}
pub fn stamp_for_comment(&mut self, rect: Rect) -> &mut Self {
self.add_stamp(StampAnnotation::for_comment(rect))
}
pub fn stamp_custom(&mut self, rect: Rect, name: impl Into<String>) -> &mut Self {
self.add_stamp(StampAnnotation::custom(rect, name))
}
pub fn add_popup(&mut self, popup: PopupAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_popup(popup);
self
}
pub fn popup(&mut self, rect: Rect, open: bool) -> &mut Self {
self.add_popup(PopupAnnotation::new(rect).with_open(open))
}
pub fn add_caret(&mut self, caret: CaretAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_caret(caret);
self
}
pub fn caret(&mut self, rect: Rect) -> &mut Self {
self.add_caret(CaretAnnotation::new(rect))
}
pub fn caret_paragraph(&mut self, rect: Rect) -> &mut Self {
self.add_caret(CaretAnnotation::paragraph(rect))
}
pub fn caret_with_comment(&mut self, rect: Rect, comment: impl Into<String>) -> &mut Self {
self.add_caret(CaretAnnotation::new(rect).with_contents(comment))
}
pub fn add_file_attachment(&mut self, file: FileAttachmentAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_file_attachment(file);
self
}
pub fn file_attachment(&mut self, rect: Rect, file_name: impl Into<String>) -> &mut Self {
self.add_file_attachment(FileAttachmentAnnotation::new(rect, file_name))
}
pub fn file_attachment_paperclip(
&mut self,
rect: Rect,
file_name: impl Into<String>,
) -> &mut Self {
self.add_file_attachment(
FileAttachmentAnnotation::new(rect, file_name).with_icon(FileAttachmentIcon::Paperclip),
)
}
pub fn add_redact(&mut self, redact: RedactAnnotation) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_redact(redact);
self
}
pub fn redact(&mut self, rect: Rect) -> &mut Self {
self.add_redact(RedactAnnotation::new(rect))
}
pub fn redact_with_text(&mut self, rect: Rect, overlay_text: impl Into<String>) -> &mut Self {
self.add_redact(RedactAnnotation::new(rect).with_overlay_text(overlay_text))
}
pub fn add_text_field(&mut self, field: TextFieldWidget) -> &mut Self {
let page_ref = ObjectRef::new(0, 0); let entry = field.build_entry(page_ref);
let page = &mut self.writer.pages[self.page_index];
page.form_fields.push(entry);
self
}
pub fn text_field(&mut self, name: impl Into<String>, rect: Rect) -> &mut Self {
self.add_text_field(TextFieldWidget::new(name, rect))
}
pub fn add_checkbox(&mut self, checkbox: CheckboxWidget) -> &mut Self {
let page_ref = ObjectRef::new(0, 0);
let entry = checkbox.build_entry(page_ref);
let page = &mut self.writer.pages[self.page_index];
page.form_fields.push(entry);
self
}
pub fn checkbox(&mut self, name: impl Into<String>, rect: Rect) -> &mut Self {
self.add_checkbox(CheckboxWidget::new(name, rect))
}
pub fn add_radio_group(&mut self, group: RadioButtonGroup) -> &mut Self {
let page_ref = ObjectRef::new(0, 0);
let (parent_dict, entries) = group.build_entries(page_ref);
let page = &mut self.writer.pages[self.page_index];
let parent_entry = FormFieldEntry {
widget_dict: HashMap::new(), field_dict: parent_dict,
name: group.name().to_string(),
rect: Rect::new(0.0, 0.0, 0.0, 0.0), field_type: "Btn".to_string(),
};
page.form_fields.push(parent_entry);
for entry in entries {
page.form_fields.push(entry);
}
self
}
pub fn add_combo_box(&mut self, combo: ComboBoxWidget) -> &mut Self {
let page_ref = ObjectRef::new(0, 0);
let entry = combo.build_entry(page_ref);
let page = &mut self.writer.pages[self.page_index];
page.form_fields.push(entry);
self
}
pub fn add_list_box(&mut self, list: ListBoxWidget) -> &mut Self {
let page_ref = ObjectRef::new(0, 0);
let entry = list.build_entry(page_ref);
let page = &mut self.writer.pages[self.page_index];
page.form_fields.push(entry);
self
}
pub fn add_push_button(&mut self, button: PushButtonWidget) -> &mut Self {
let page_ref = ObjectRef::new(0, 0);
let entry = button.build_entry(page_ref);
let page = &mut self.writer.pages[self.page_index];
page.form_fields.push(entry);
self
}
pub fn add_annotation<A: Into<super::annotation_builder::Annotation>>(
&mut self,
annotation: A,
) -> &mut Self {
let page = &mut self.writer.pages[self.page_index];
page.annotations.add_annotation(annotation);
self
}
pub fn finish(self) -> &'a mut PdfWriter {
let page = &mut self.writer.pages[self.page_index];
page.content_builder.end_text();
self.writer
}
}
struct PageData {
width: f32,
height: f32,
content_builder: ContentStreamBuilder,
annotations: AnnotationBuilder,
form_fields: Vec<FormFieldEntry>,
}
pub struct PdfWriter {
config: PdfWriterConfig,
pages: Vec<PageData>,
next_obj_id: u32,
objects: HashMap<u32, Object>,
fonts: HashMap<String, ObjectRef>,
acroform: Option<AcroFormBuilder>,
}
impl PdfWriter {
pub fn new() -> Self {
Self::with_config(PdfWriterConfig::default())
}
pub fn with_config(config: PdfWriterConfig) -> Self {
Self {
config,
pages: Vec::new(),
next_obj_id: 1,
objects: HashMap::new(),
fonts: HashMap::new(),
acroform: None,
}
}
fn alloc_obj_id(&mut self) -> u32 {
let id = self.next_obj_id;
self.next_obj_id += 1;
id
}
pub fn add_page(&mut self, width: f32, height: f32) -> PageBuilder<'_> {
let page_index = self.pages.len();
self.pages.push(PageData {
width,
height,
content_builder: ContentStreamBuilder::new(),
annotations: AnnotationBuilder::new(),
form_fields: Vec::new(),
});
PageBuilder {
writer: self,
page_index,
}
}
pub fn add_letter_page(&mut self) -> PageBuilder<'_> {
self.add_page(612.0, 792.0)
}
pub fn add_a4_page(&mut self) -> PageBuilder<'_> {
self.add_page(595.0, 842.0)
}
fn get_font_ref(&mut self, font_name: &str) -> ObjectRef {
if let Some(font_ref) = self.fonts.get(font_name) {
return *font_ref;
}
let font_id = self.alloc_obj_id();
let font_obj = ObjectSerializer::dict(vec![
("Type", ObjectSerializer::name("Font")),
("Subtype", ObjectSerializer::name("Type1")),
("BaseFont", ObjectSerializer::name(font_name)),
("Encoding", ObjectSerializer::name("WinAnsiEncoding")),
]);
self.objects.insert(font_id, font_obj);
let font_ref = ObjectRef::new(font_id, 0);
self.fonts.insert(font_name.to_string(), font_ref);
font_ref
}
pub fn finish(mut self) -> Result<Vec<u8>> {
let serializer = ObjectSerializer::compact();
let mut output = Vec::new();
let mut xref_offsets: Vec<(u32, usize)> = Vec::new();
writeln!(output, "%PDF-{}", self.config.version)?;
output.extend_from_slice(b"%\xE2\xE3\xCF\xD3\n");
let font_names: Vec<String> = vec![
"Helvetica".to_string(),
"Helvetica-Bold".to_string(),
"Times-Roman".to_string(),
"Times-Bold".to_string(),
"Courier".to_string(),
"Courier-Bold".to_string(),
];
for font_name in &font_names {
self.get_font_ref(font_name);
}
let font_resources: HashMap<String, Object> = self
.fonts
.iter()
.map(|(name, obj_ref)| {
let simple_name = name.replace('-', "");
(simple_name, Object::Reference(*obj_ref))
})
.collect();
let catalog_id = self.alloc_obj_id();
let pages_id = self.alloc_obj_id();
let page_count = self.pages.len();
let mut page_ids: Vec<(u32, u32)> = Vec::with_capacity(page_count);
for _ in 0..page_count {
let page_id = self.alloc_obj_id();
let content_id = self.alloc_obj_id();
page_ids.push((page_id, content_id));
}
let annot_counts: Vec<usize> = self.pages.iter().map(|p| p.annotations.len()).collect();
let mut annot_ids: Vec<Vec<u32>> = Vec::with_capacity(page_count);
for count in annot_counts {
let mut page_annot_ids = Vec::with_capacity(count);
for _ in 0..count {
page_annot_ids.push(self.alloc_obj_id());
}
annot_ids.push(page_annot_ids);
}
let form_field_counts: Vec<usize> =
self.pages.iter().map(|p| p.form_fields.len()).collect();
let mut form_field_ids: Vec<Vec<u32>> = Vec::with_capacity(page_count);
for count in form_field_counts {
let mut page_field_ids = Vec::with_capacity(count);
for _ in 0..count {
page_field_ids.push(self.alloc_obj_id());
}
form_field_ids.push(page_field_ids);
}
let page_obj_refs: Vec<ObjectRef> = page_ids
.iter()
.map(|(page_id, _)| ObjectRef::new(*page_id, 0))
.collect();
let mut page_refs: Vec<Object> = Vec::new();
let mut page_objects: Vec<(u32, Object, Vec<u8>)> = Vec::new();
let mut annotation_objects: Vec<(u32, Object)> = Vec::new();
let mut form_field_objects: Vec<(u32, Object)> = Vec::new();
let mut all_field_refs: Vec<ObjectRef> = Vec::new();
for (i, page_data) in self.pages.iter().enumerate() {
let (page_id, content_id) = page_ids[i];
let page_ref = ObjectRef::new(page_id, 0);
let raw_content = page_data.content_builder.build()?;
let (content_bytes, is_compressed) = if self.config.compress {
match compress_data(&raw_content) {
Ok(compressed) => (compressed, true),
Err(_) => (raw_content, false), }
} else {
(raw_content, false)
};
let mut content_dict = HashMap::new();
content_dict.insert("Length".to_string(), Object::Integer(content_bytes.len() as i64));
if is_compressed {
content_dict.insert("Filter".to_string(), Object::Name("FlateDecode".to_string()));
}
let mut annot_refs: Vec<Object> = Vec::new();
if !page_data.annotations.is_empty() {
let annot_dicts = page_data.annotations.build(&page_obj_refs);
for (j, annot_dict) in annot_dicts.into_iter().enumerate() {
let annot_id = annot_ids[i][j];
annotation_objects.push((annot_id, Object::Dictionary(annot_dict)));
annot_refs.push(Object::Reference(ObjectRef::new(annot_id, 0)));
}
}
for (j, field_entry) in page_data.form_fields.iter().enumerate() {
let field_id = form_field_ids[i][j];
let field_ref = ObjectRef::new(field_id, 0);
all_field_refs.push(field_ref);
let mut field_dict = field_entry.field_dict.clone();
let mut widget_dict = field_entry.widget_dict.clone();
widget_dict.insert("P".to_string(), Object::Reference(page_ref));
for (key, value) in widget_dict {
field_dict.insert(key, value);
}
form_field_objects.push((field_id, Object::Dictionary(field_dict)));
annot_refs.push(Object::Reference(field_ref));
}
let mut page_entries: Vec<(&str, Object)> = vec![
("Type", ObjectSerializer::name("Page")),
("Parent", ObjectSerializer::reference(pages_id, 0)),
(
"MediaBox",
ObjectSerializer::rect(
0.0,
0.0,
page_data.width as f64,
page_data.height as f64,
),
),
("Contents", ObjectSerializer::reference(content_id, 0)),
(
"Resources",
ObjectSerializer::dict(vec![(
"Font",
Object::Dictionary(font_resources.clone()),
)]),
),
];
if !annot_refs.is_empty() {
page_entries.push(("Annots", Object::Array(annot_refs)));
}
let page_obj = ObjectSerializer::dict(page_entries);
page_refs.push(Object::Reference(ObjectRef::new(page_id, 0)));
page_objects.push((page_id, page_obj, Vec::new()));
page_objects.push((
content_id,
Object::Stream {
dict: content_dict,
data: bytes::Bytes::from(content_bytes),
},
Vec::new(),
));
}
let pages_obj = ObjectSerializer::dict(vec![
("Type", ObjectSerializer::name("Pages")),
("Kids", Object::Array(page_refs)),
("Count", ObjectSerializer::integer(self.pages.len() as i64)),
]);
let acroform_id = if !all_field_refs.is_empty() {
let id = self.alloc_obj_id();
let mut acroform = self.acroform.take().unwrap_or_default();
acroform.add_fields(all_field_refs);
let acroform_dict = acroform.build_with_resources();
self.objects.insert(id, Object::Dictionary(acroform_dict));
Some(id)
} else {
None
};
let mut catalog_entries = vec![
("Type", ObjectSerializer::name("Catalog")),
("Pages", ObjectSerializer::reference(pages_id, 0)),
];
if let Some(acroform_id) = acroform_id {
catalog_entries.push(("AcroForm", ObjectSerializer::reference(acroform_id, 0)));
}
let catalog_obj = ObjectSerializer::dict(catalog_entries);
let info_id = self.alloc_obj_id();
let mut info_entries = Vec::new();
if let Some(title) = &self.config.title {
info_entries.push(("Title", ObjectSerializer::string(title)));
}
if let Some(author) = &self.config.author {
info_entries.push(("Author", ObjectSerializer::string(author)));
}
if let Some(subject) = &self.config.subject {
info_entries.push(("Subject", ObjectSerializer::string(subject)));
}
if let Some(creator) = &self.config.creator {
info_entries.push(("Creator", ObjectSerializer::string(creator)));
}
let info_obj = ObjectSerializer::dict(info_entries);
xref_offsets.push((catalog_id, output.len()));
output.extend_from_slice(&serializer.serialize_indirect(catalog_id, 0, &catalog_obj));
xref_offsets.push((pages_id, output.len()));
output.extend_from_slice(&serializer.serialize_indirect(pages_id, 0, &pages_obj));
for font_ref in self.fonts.values() {
if let Some(font_obj) = self.objects.get(&font_ref.id) {
xref_offsets.push((font_ref.id, output.len()));
output.extend_from_slice(&serializer.serialize_indirect(font_ref.id, 0, font_obj));
}
}
for (obj_id, obj, _) in &page_objects {
xref_offsets.push((*obj_id, output.len()));
output.extend_from_slice(&serializer.serialize_indirect(*obj_id, 0, obj));
}
for (annot_id, annot_obj) in &annotation_objects {
xref_offsets.push((*annot_id, output.len()));
output.extend_from_slice(&serializer.serialize_indirect(*annot_id, 0, annot_obj));
}
for (field_id, field_obj) in &form_field_objects {
xref_offsets.push((*field_id, output.len()));
output.extend_from_slice(&serializer.serialize_indirect(*field_id, 0, field_obj));
}
if let Some(acroform_id) = acroform_id {
if let Some(acroform_obj) = self.objects.get(&acroform_id) {
xref_offsets.push((acroform_id, output.len()));
output.extend_from_slice(&serializer.serialize_indirect(
acroform_id,
0,
acroform_obj,
));
}
}
xref_offsets.push((info_id, output.len()));
output.extend_from_slice(&serializer.serialize_indirect(info_id, 0, &info_obj));
let xref_start = output.len();
writeln!(output, "xref")?;
writeln!(output, "0 {}", self.next_obj_id)?;
writeln!(output, "0000000000 65535 f ")?;
xref_offsets.sort_by_key(|(id, _)| *id);
for (_, offset) in &xref_offsets {
writeln!(output, "{:010} 00000 n ", offset)?;
}
let trailer = ObjectSerializer::dict(vec![
("Size", ObjectSerializer::integer(self.next_obj_id as i64)),
("Root", ObjectSerializer::reference(catalog_id, 0)),
("Info", ObjectSerializer::reference(info_id, 0)),
]);
writeln!(output, "trailer")?;
output.extend_from_slice(&serializer.serialize(&trailer));
writeln!(output)?;
writeln!(output, "startxref")?;
writeln!(output, "{}", xref_start)?;
write!(output, "%%EOF")?;
Ok(output)
}
pub fn save(self, path: impl AsRef<std::path::Path>) -> Result<()> {
let bytes = self.finish()?;
std::fs::write(path, bytes)?;
Ok(())
}
}
impl Default for PdfWriter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_empty_pdf() {
let writer = PdfWriter::new();
let mut writer = writer;
writer.add_letter_page().finish();
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.starts_with("%PDF-1.7"));
assert!(content.contains("/Type /Catalog"));
assert!(content.contains("/Type /Pages"));
assert!(content.contains("/Type /Page"));
assert!(content.contains("%%EOF"));
}
#[test]
fn test_pdf_with_text() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.add_text("Hello, World!", 72.0, 720.0, "Helvetica", 12.0);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Type /Font"));
assert!(content.contains("/BaseFont /Helvetica"));
assert!(content.contains("BT"));
assert!(content.contains("(Hello, World!) Tj"));
assert!(content.contains("ET"));
}
#[test]
fn test_pdf_with_metadata() {
let config = PdfWriterConfig::default()
.with_title("Test Document")
.with_author("Test Author");
let mut writer = PdfWriter::with_config(config);
writer.add_letter_page().finish();
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Title (Test Document)"));
assert!(content.contains("/Author (Test Author)"));
}
#[test]
fn test_multiple_pages() {
let mut writer = PdfWriter::new();
writer.add_letter_page().finish();
writer.add_a4_page().finish();
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Count 2"));
assert!(content.contains("[0 0 612 792]")); assert!(content.contains("[0 0 595 842]")); }
#[test]
fn test_page_builder() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.add_text("Line 1", 72.0, 720.0, "Helvetica", 12.0);
page.add_text("Line 2", 72.0, 700.0, "Helvetica", 12.0);
page.finish();
}
let bytes = writer.finish().unwrap();
assert!(!bytes.is_empty());
}
#[test]
fn test_pdf_with_link_annotation() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.add_text("Click here to visit Rust", 72.0, 720.0, "Helvetica", 12.0);
page.link(Rect::new(72.0, 720.0, 150.0, 12.0), "https://www.rust-lang.org");
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Type /Annot"));
assert!(content.contains("/Subtype /Link"));
assert!(content.contains("/Annots"));
assert!(content.contains("rust-lang.org"));
}
#[test]
fn test_pdf_with_internal_link() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.add_text("Go to page 2", 72.0, 720.0, "Helvetica", 12.0);
page.internal_link(Rect::new(72.0, 720.0, 100.0, 12.0), 1);
page.finish();
}
{
let mut page = writer.add_letter_page();
page.add_text("This is page 2", 72.0, 720.0, "Helvetica", 12.0);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Type /Annot"));
assert!(content.contains("/Subtype /Link"));
assert!(content.contains("/Dest")); assert!(content.contains("/Fit")); }
#[test]
fn test_pdf_with_multiple_annotations() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.link(Rect::new(72.0, 720.0, 100.0, 12.0), "https://example1.com");
page.link(Rect::new(72.0, 700.0, 100.0, 12.0), "https://example2.com");
page.link(Rect::new(72.0, 680.0, 100.0, 12.0), "https://example3.com");
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
let annot_count = content.matches("/Type /Annot").count();
assert_eq!(annot_count, 3, "Expected 3 annotations");
}
#[test]
fn test_pdf_with_highlight() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.add_text("Important text to highlight", 72.0, 720.0, "Helvetica", 12.0);
page.highlight_rect(Rect::new(72.0, 720.0, 150.0, 12.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Type /Annot"));
assert!(content.contains("/Subtype /Highlight"));
assert!(content.contains("/QuadPoints"));
assert!(content.contains("/Annots"));
}
#[test]
fn test_pdf_with_all_text_markup_types() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.highlight_rect(Rect::new(72.0, 720.0, 100.0, 12.0));
page.underline_rect(Rect::new(72.0, 700.0, 100.0, 12.0));
page.strikeout_rect(Rect::new(72.0, 680.0, 100.0, 12.0));
page.squiggly_rect(Rect::new(72.0, 660.0, 100.0, 12.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Highlight"));
assert!(content.contains("/Subtype /Underline"));
assert!(content.contains("/Subtype /StrikeOut"));
assert!(content.contains("/Subtype /Squiggly"));
let annot_count = content.matches("/Type /Annot").count();
assert_eq!(annot_count, 4, "Expected 4 text markup annotations");
}
#[test]
fn test_pdf_with_mixed_annotations() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.link(Rect::new(72.0, 720.0, 100.0, 12.0), "https://example.com");
page.highlight_rect(Rect::new(72.0, 700.0, 100.0, 12.0));
page.underline_rect(Rect::new(72.0, 680.0, 100.0, 12.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
let annot_count = content.matches("/Type /Annot").count();
assert_eq!(annot_count, 3, "Expected 3 mixed annotations");
assert!(content.contains("/Subtype /Link"));
assert!(content.contains("/Subtype /Highlight"));
assert!(content.contains("/Subtype /Underline"));
}
#[test]
fn test_pdf_with_sticky_note() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.add_text("Document with a note", 72.0, 720.0, "Helvetica", 12.0);
page.sticky_note(Rect::new(72.0, 700.0, 24.0, 24.0), "This is an important note!");
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Type /Annot"));
assert!(content.contains("/Subtype /Text"));
assert!(content.contains("/Name /Note"));
assert!(content.contains("/Annots"));
assert!(content.contains("important note"));
}
#[test]
fn test_pdf_with_comment_annotation() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.comment(Rect::new(72.0, 720.0, 24.0, 24.0), "Review comment here");
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Type /Annot"));
assert!(content.contains("/Subtype /Text"));
assert!(content.contains("/Name /Comment"));
}
#[test]
fn test_pdf_with_text_note_icons() {
use crate::annotation_types::TextAnnotationIcon;
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.text_note_with_icon(
Rect::new(72.0, 720.0, 24.0, 24.0),
"Help note",
TextAnnotationIcon::Help,
);
page.text_note_with_icon(
Rect::new(100.0, 720.0, 24.0, 24.0),
"Key note",
TextAnnotationIcon::Key,
);
page.text_note_with_icon(
Rect::new(128.0, 720.0, 24.0, 24.0),
"Insert note",
TextAnnotationIcon::Insert,
);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Name /Help"));
assert!(content.contains("/Name /Key"));
assert!(content.contains("/Name /Insert"));
let annot_count = content.matches("/Subtype /Text").count();
assert_eq!(annot_count, 3, "Expected 3 text annotations with different icons");
}
#[test]
fn test_pdf_with_all_annotation_types() {
use crate::annotation_types::TextAnnotationIcon;
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.add_text("Comprehensive annotation test", 72.0, 750.0, "Helvetica", 14.0);
page.link(Rect::new(72.0, 720.0, 100.0, 12.0), "https://example.com");
page.highlight_rect(Rect::new(72.0, 700.0, 100.0, 12.0));
page.underline_rect(Rect::new(72.0, 680.0, 100.0, 12.0));
page.strikeout_rect(Rect::new(72.0, 660.0, 100.0, 12.0));
page.squiggly_rect(Rect::new(72.0, 640.0, 100.0, 12.0));
page.sticky_note(Rect::new(200.0, 720.0, 24.0, 24.0), "A sticky note");
page.comment(Rect::new(200.0, 680.0, 24.0, 24.0), "A comment");
page.text_note_with_icon(
Rect::new(200.0, 640.0, 24.0, 24.0),
"Help text",
TextAnnotationIcon::Help,
);
page.internal_link(Rect::new(72.0, 600.0, 100.0, 12.0), 1);
page.finish();
}
{
let mut page = writer.add_letter_page();
page.add_text("Page 2", 72.0, 720.0, "Helvetica", 12.0);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Link"));
assert!(content.contains("/Subtype /Highlight"));
assert!(content.contains("/Subtype /Underline"));
assert!(content.contains("/Subtype /StrikeOut"));
assert!(content.contains("/Subtype /Squiggly"));
assert!(content.contains("/Subtype /Text"));
let annot_count = content.matches("/Type /Annot").count();
assert_eq!(annot_count, 9, "Expected 9 annotations total");
}
#[test]
fn test_pdf_with_textbox() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.add_text("Document with text box", 72.0, 750.0, "Helvetica", 14.0);
page.textbox(Rect::new(72.0, 650.0, 200.0, 80.0), "This is a text box annotation");
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Type /Annot"));
assert!(content.contains("/Subtype /FreeText"));
assert!(content.contains("/DA")); assert!(content.contains("/Annots"));
}
#[test]
fn test_pdf_with_styled_textbox() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.textbox_styled(
Rect::new(72.0, 600.0, 250.0, 60.0),
"Styled text content",
"Courier",
14.0,
);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /FreeText"));
assert!(content.contains("/Cour")); assert!(content.contains("14")); }
#[test]
fn test_pdf_with_centered_textbox() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.textbox_centered(Rect::new(100.0, 500.0, 200.0, 40.0), "Centered text");
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /FreeText"));
assert!(content.contains("/Q 1")); }
#[test]
fn test_pdf_with_callout() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.callout(
Rect::new(72.0, 600.0, 150.0, 50.0),
"Callout annotation",
vec![50.0, 550.0, 72.0, 600.0],
);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /FreeText"));
assert!(content.contains("/IT /FreeTextCallout")); assert!(content.contains("/CL")); }
#[test]
fn test_pdf_with_typewriter() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.typewriter(Rect::new(72.0, 500.0, 300.0, 20.0), "Typewriter text");
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /FreeText"));
assert!(content.contains("/IT /FreeTextTypeWriter")); }
#[test]
fn test_pdf_with_multiple_freetext_types() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.textbox(Rect::new(72.0, 700.0, 150.0, 40.0), "Basic text box");
page.textbox_centered(Rect::new(72.0, 640.0, 150.0, 40.0), "Centered box");
page.typewriter(Rect::new(72.0, 580.0, 200.0, 20.0), "Typewriter");
page.callout(
Rect::new(300.0, 700.0, 150.0, 40.0),
"Callout",
vec![250.0, 680.0, 300.0, 720.0],
);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
let freetext_count = content.matches("/Subtype /FreeText").count();
assert_eq!(freetext_count, 4, "Expected 4 FreeText annotations");
}
#[test]
fn test_pdf_with_line_annotation() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.line((100.0, 100.0), (300.0, 100.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Line"));
assert!(content.contains("/L ")); }
#[test]
fn test_pdf_with_arrow_annotation() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.arrow((100.0, 200.0), (300.0, 200.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Line"));
assert!(content.contains("/LE")); assert!(content.contains("/OpenArrow"));
}
#[test]
fn test_pdf_with_rectangle_annotation() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.rectangle(Rect::new(100.0, 400.0, 150.0, 100.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Square"));
}
#[test]
fn test_pdf_with_circle_annotation() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.circle(Rect::new(300.0, 400.0, 100.0, 100.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Circle"));
}
#[test]
fn test_pdf_with_filled_shapes() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.rectangle_filled(
Rect::new(100.0, 300.0, 100.0, 80.0),
(0.0, 0.0, 1.0), (0.8, 0.8, 1.0), );
page.circle_filled(
Rect::new(250.0, 300.0, 80.0, 80.0),
(1.0, 0.0, 0.0), (1.0, 0.8, 0.8), );
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Square"));
assert!(content.contains("/Subtype /Circle"));
assert!(content.contains("/IC")); }
#[test]
fn test_pdf_with_polygon() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.polygon(vec![(100.0, 100.0), (150.0, 200.0), (50.0, 200.0)]);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Polygon"));
assert!(content.contains("/Vertices"));
}
#[test]
fn test_pdf_with_polyline() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.polyline(vec![
(100.0, 500.0),
(200.0, 550.0),
(300.0, 500.0),
(400.0, 550.0),
]);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /PolyLine"));
assert!(content.contains("/Vertices"));
}
#[test]
fn test_pdf_with_all_shape_types() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.line((72.0, 750.0), (200.0, 750.0));
page.arrow((72.0, 700.0), (200.0, 700.0));
page.rectangle(Rect::new(72.0, 600.0, 100.0, 50.0));
page.circle(Rect::new(200.0, 600.0, 50.0, 50.0));
page.polygon(vec![(300.0, 600.0), (350.0, 650.0), (250.0, 650.0)]);
page.polyline(vec![(72.0, 500.0), (150.0, 550.0), (250.0, 500.0)]);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Line"));
assert!(content.contains("/Subtype /Square"));
assert!(content.contains("/Subtype /Circle"));
assert!(content.contains("/Subtype /Polygon"));
assert!(content.contains("/Subtype /PolyLine"));
let annot_count = content.matches("/Type /Annot").count();
assert_eq!(annot_count, 6, "Expected 6 shape annotations");
}
#[test]
fn test_pdf_with_ink_annotation() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.ink(vec![(100.0, 100.0), (150.0, 120.0), (200.0, 100.0)]);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Type /Annot"));
assert!(content.contains("/Subtype /Ink"));
assert!(content.contains("/InkList"));
}
#[test]
fn test_pdf_with_freehand_multiple_strokes() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.freehand(vec![
vec![(100.0, 100.0), (150.0, 120.0), (200.0, 100.0)],
vec![(100.0, 200.0), (200.0, 200.0)],
vec![(150.0, 150.0), (150.0, 250.0)],
]);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Ink"));
assert!(content.contains("/InkList"));
let ink_count = content.matches("/Subtype /Ink").count();
assert_eq!(ink_count, 1, "Expected 1 Ink annotation");
}
#[test]
fn test_pdf_with_styled_ink() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.ink_styled(
vec![(100.0, 300.0), (200.0, 350.0), (300.0, 300.0)],
(1.0, 0.0, 0.0), 3.0, );
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Ink"));
assert!(content.contains("/C")); assert!(content.contains("/BS")); }
#[test]
fn test_pdf_with_multiple_ink_annotations() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.ink(vec![(100.0, 100.0), (150.0, 120.0)]);
page.ink(vec![(200.0, 100.0), (250.0, 120.0)]);
page.ink_styled(
vec![(300.0, 100.0), (350.0, 120.0)],
(0.0, 0.0, 1.0), 2.0,
);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
let ink_count = content.matches("/Subtype /Ink").count();
assert_eq!(ink_count, 3, "Expected 3 Ink annotations");
}
#[test]
fn test_pdf_with_ink_and_other_annotations() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.ink(vec![(100.0, 100.0), (200.0, 150.0)]);
page.highlight_rect(Rect::new(72.0, 700.0, 100.0, 12.0));
page.sticky_note(Rect::new(300.0, 700.0, 24.0, 24.0), "Note");
page.line((72.0, 600.0), (200.0, 600.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Ink"));
assert!(content.contains("/Subtype /Highlight"));
assert!(content.contains("/Subtype /Text"));
assert!(content.contains("/Subtype /Line"));
let annot_count = content.matches("/Type /Annot").count();
assert_eq!(annot_count, 4, "Expected 4 mixed annotations");
}
#[test]
fn test_pdf_with_approved_stamp() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.stamp_approved(Rect::new(400.0, 700.0, 150.0, 50.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Type /Annot"));
assert!(content.contains("/Subtype /Stamp"));
assert!(content.contains("/Name /Approved"));
}
#[test]
fn test_pdf_with_draft_stamp() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.stamp_draft(Rect::new(400.0, 650.0, 120.0, 40.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Stamp"));
assert!(content.contains("/Name /Draft"));
}
#[test]
fn test_pdf_with_confidential_stamp() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.stamp_confidential(Rect::new(400.0, 600.0, 150.0, 50.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Stamp"));
assert!(content.contains("/Name /Confidential"));
}
#[test]
fn test_pdf_with_custom_stamp() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.stamp_custom(Rect::new(400.0, 550.0, 150.0, 50.0), "ReviewPending");
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Stamp"));
assert!(content.contains("/Name /ReviewPending"));
}
#[test]
fn test_pdf_with_multiple_stamps() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.stamp_approved(Rect::new(400.0, 700.0, 100.0, 40.0));
page.stamp_draft(Rect::new(400.0, 650.0, 100.0, 40.0));
page.stamp_final(Rect::new(400.0, 600.0, 100.0, 40.0));
page.stamp_for_comment(Rect::new(400.0, 550.0, 100.0, 40.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
let stamp_count = content.matches("/Subtype /Stamp").count();
assert_eq!(stamp_count, 4, "Expected 4 Stamp annotations");
assert!(content.contains("/Name /Approved"));
assert!(content.contains("/Name /Draft"));
assert!(content.contains("/Name /Final"));
assert!(content.contains("/Name /ForComment"));
}
#[test]
fn test_pdf_with_stamp_and_other_annotations() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.stamp_approved(Rect::new(400.0, 700.0, 150.0, 50.0));
page.highlight_rect(Rect::new(72.0, 700.0, 100.0, 12.0));
page.sticky_note(Rect::new(200.0, 700.0, 24.0, 24.0), "Note");
page.line((72.0, 600.0), (200.0, 600.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Stamp"));
assert!(content.contains("/Subtype /Highlight"));
assert!(content.contains("/Subtype /Text"));
assert!(content.contains("/Subtype /Line"));
let annot_count = content.matches("/Type /Annot").count();
assert_eq!(annot_count, 4, "Expected 4 mixed annotations");
}
#[test]
fn test_pdf_with_popup_annotation() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.popup(Rect::new(200.0, 600.0, 200.0, 100.0), true);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Type /Annot"));
assert!(content.contains("/Subtype /Popup"));
assert!(content.contains("/Rect"));
assert!(content.contains("/Open true"));
}
#[test]
fn test_pdf_with_caret_annotation() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.caret(Rect::new(100.0, 700.0, 20.0, 20.0));
page.caret_paragraph(Rect::new(100.0, 650.0, 20.0, 20.0));
page.caret_with_comment(
Rect::new(100.0, 600.0, 20.0, 20.0),
"Insert new paragraph here",
);
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
let caret_count = content.matches("/Subtype /Caret").count();
assert_eq!(caret_count, 3, "Expected 3 Caret annotations");
assert!(content.contains("/Sy /None"));
assert!(content.contains("/Sy /P"));
assert!(content.contains("Insert new paragraph here"));
}
#[test]
fn test_pdf_with_file_attachment_annotation() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.file_attachment(Rect::new(50.0, 700.0, 24.0, 24.0), "document.pdf");
page.file_attachment_paperclip(Rect::new(50.0, 650.0, 24.0, 24.0), "notes.txt");
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
let attach_count = content.matches("/Subtype /FileAttachment").count();
assert_eq!(attach_count, 2, "Expected 2 FileAttachment annotations");
assert!(content.contains("/Name /PushPin"));
assert!(content.contains("/Name /Paperclip"));
assert!(content.contains("document.pdf"));
assert!(content.contains("notes.txt"));
}
#[test]
fn test_pdf_with_redact_annotation() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.redact(Rect::new(100.0, 700.0, 200.0, 20.0));
page.redact_with_text(Rect::new(100.0, 650.0, 200.0, 20.0), "REDACTED");
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
let redact_count = content.matches("/Subtype /Redact").count();
assert_eq!(redact_count, 2, "Expected 2 Redact annotations");
assert!(content.contains("REDACTED"));
}
#[test]
fn test_pdf_with_mixed_special_annotations() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.popup(Rect::new(200.0, 700.0, 150.0, 80.0), false);
page.caret(Rect::new(100.0, 650.0, 20.0, 20.0));
page.file_attachment(Rect::new(50.0, 600.0, 24.0, 24.0), "report.pdf");
page.redact(Rect::new(100.0, 550.0, 200.0, 20.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Popup"));
assert!(content.contains("/Subtype /Caret"));
assert!(content.contains("/Subtype /FileAttachment"));
assert!(content.contains("/Subtype /Redact"));
let annot_count = content.matches("/Type /Annot").count();
assert_eq!(annot_count, 4, "Expected 4 special annotations");
}
#[test]
fn test_pdf_with_complete_annotation_coverage() {
let mut writer = PdfWriter::new();
{
let mut page = writer.add_letter_page();
page.link(Rect::new(72.0, 750.0, 100.0, 20.0), "https://example.com");
page.highlight_rect(Rect::new(72.0, 720.0, 100.0, 12.0));
page.underline_rect(Rect::new(72.0, 700.0, 100.0, 12.0));
page.sticky_note(Rect::new(200.0, 720.0, 24.0, 24.0), "Note");
page.textbox(Rect::new(72.0, 660.0, 150.0, 30.0), "Comment here");
page.line((72.0, 620.0), (200.0, 620.0));
page.rectangle(Rect::new(72.0, 570.0, 50.0, 50.0));
page.circle(Rect::new(140.0, 570.0, 50.0, 50.0));
page.ink(vec![(72.0, 520.0), (100.0, 540.0), (130.0, 520.0)]);
page.stamp_approved(Rect::new(400.0, 700.0, 100.0, 40.0));
page.popup(Rect::new(400.0, 600.0, 150.0, 80.0), false);
page.caret(Rect::new(400.0, 550.0, 20.0, 20.0));
page.file_attachment(Rect::new(400.0, 500.0, 24.0, 24.0), "data.xlsx");
page.redact(Rect::new(400.0, 450.0, 150.0, 20.0));
page.finish();
}
let bytes = writer.finish().unwrap();
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("/Subtype /Link"));
assert!(content.contains("/Subtype /Highlight"));
assert!(content.contains("/Subtype /Underline"));
assert!(content.contains("/Subtype /Text"));
assert!(content.contains("/Subtype /FreeText"));
assert!(content.contains("/Subtype /Line"));
assert!(content.contains("/Subtype /Square"));
assert!(content.contains("/Subtype /Circle"));
assert!(content.contains("/Subtype /Ink"));
assert!(content.contains("/Subtype /Stamp"));
assert!(content.contains("/Subtype /Popup"));
assert!(content.contains("/Subtype /Caret"));
assert!(content.contains("/Subtype /FileAttachment"));
assert!(content.contains("/Subtype /Redact"));
let annot_count = content.matches("/Type /Annot").count();
assert_eq!(annot_count, 14, "Expected 14 different annotation types");
}
}