1#![allow(
2 clippy::collapsible_if,
3 clippy::collapsible_match,
4 clippy::manual_rem_euclid,
5 clippy::manual_is_multiple_of,
6 clippy::too_many_arguments,
7 clippy::type_complexity,
8 clippy::approx_constant,
9 clippy::new_without_default,
10 clippy::chunks_exact_to_as_chunks,
11 clippy::manual_is_ascii_check
12)]
13
14pub mod content;
15pub mod cos;
16pub mod crypto;
17pub mod error;
18pub mod facade;
19pub mod font;
20pub mod image;
21pub mod interactive;
22pub mod manipulation;
23pub mod pd;
24pub mod prelude;
25pub mod sign;
26pub mod stamp;
27pub mod text;
28
29pub use content::{AppendMode, Color, PDPageContentStream};
30pub use cos::{
31 COSArray, COSDictionary, COSDocument, COSName, COSObject, COSStream, COSString, ObjectId,
32};
33pub use crypto::{DerElement, SigningKey};
34pub use error::{PdfError, PdfResult};
35pub use facade::PdfDocument;
36pub use font::{PDTrueTypeFont, PDType1Font};
37pub use image::PDImageXObject;
38pub use interactive::annotation::{AnnotationType, PDAnnotation};
39pub use interactive::form::{FieldType, PDAcroForm, PDFormField};
40pub use manipulation::merger::PDFMerger;
41pub use manipulation::page_ops::PageOps;
42pub use manipulation::splitter::PDFSplitter;
43pub use pd::{
44 PDDocument, PDDocumentCatalog, PDDocumentInformation, PDPage, PDRectangle, PDResources,
45};
46pub use sign::{SignatureReport, SignatureVerifier, Signer, VisualSignature};
47pub use stamp::{PageRange, StampLayer, StampPosition, Stamper};
48pub use text::stripper::PDFTextStripper;
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use cos::Parser;
54 use cos::Writer;
55
56 #[test]
57 fn test_cos_roundtrip() {
58 let mut doc = COSDocument::new();
59 let cat_id = doc.allocate_id();
60 let mut cat = COSDictionary::new();
61 cat.set("Type", COSName::new("Catalog"));
62 cat.set("TestString", COSString::literal("Hello (World)!"));
63 cat.set("TestHex", COSString::hex(b"Techwasti"));
64 cat.set("TestNum", 42);
65 cat.set("TestReal", 3.1415);
66 doc.set_object(cat_id, cat);
67
68 doc.trailer.set("Root", cat_id);
69
70 let mut buf = Vec::new();
71 let mut writer = Writer::new(&mut buf);
72 writer.write_document(&doc).expect("write failed");
73
74 let pdf_bytes = buf;
75 assert!(pdf_bytes.starts_with(b"%PDF-1.7"));
76
77 let parser = Parser::new(&pdf_bytes);
78 let parsed_doc = parser.parse().expect("parse failed");
79
80 let root_ref = parsed_doc.trailer.get_reference("Root").expect("root ref");
81 assert_eq!(root_ref, cat_id);
82
83 let resolved_cat = parsed_doc.get_catalog().expect("catalog");
84 assert_eq!(resolved_cat.get_name("Type"), Some("Catalog"));
85 assert_eq!(resolved_cat.get_integer("TestNum"), Some(42));
86 assert_eq!(
87 resolved_cat
88 .get_string("TestString")
89 .unwrap()
90 .to_string_lossy(),
91 "Hello (World)!"
92 );
93 assert_eq!(
94 resolved_cat
95 .get_string("TestHex")
96 .unwrap()
97 .as_str()
98 .unwrap(),
99 "Techwasti"
100 );
101 }
102
103 #[test]
104 fn test_pd_document_lifecycle() {
105 let mut doc = PDDocument::new();
106 doc.info.set_title("Test Document");
107 doc.info.set_author("Techwasti Infotech");
108
109 let page1 = PDPage::new(PDRectangle::A4);
110 let page2 = PDPage::new(PDRectangle::LETTER);
111 doc.add_page(page1);
112 doc.add_page(page2);
113
114 assert_eq!(doc.page_count(), 2);
115 assert_eq!(doc.get_page(0).unwrap().media_box(), PDRectangle::A4);
116 assert_eq!(doc.get_page(1).unwrap().media_box(), PDRectangle::LETTER);
117
118 let mut buf = Vec::new();
119 doc.save_to_writer(&mut buf).expect("save failed");
120
121 let loaded = PDDocument::load_from_bytes(&buf).expect("load failed");
122 assert_eq!(loaded.page_count(), 2);
123 assert_eq!(loaded.info.title(), Some("Test Document".to_string()));
124 assert_eq!(loaded.info.author(), Some("Techwasti Infotech".to_string()));
125 assert_eq!(loaded.get_page(0).unwrap().media_box(), PDRectangle::A4);
126 assert_eq!(loaded.get_page(1).unwrap().media_box(), PDRectangle::LETTER);
127 }
128
129 #[test]
130 fn test_content_stream_rendering() {
131 let mut doc = PDDocument::new();
132 let page = PDPage::new(PDRectangle::A4);
133 doc.add_page(page);
134
135 let mut content = PDPageContentStream::new(AppendMode::Overwrite);
136 content.save_graphics_state();
137 content.set_stroke_color(Color::rgb(0.2, 0.4, 0.8));
138 content.set_fill_color(Color::rgb(0.9, 0.9, 0.95));
139 content.set_line_width(2.0);
140 content.rect(50.0, 700.0, 500.0, 80.0);
141 content.fill_and_stroke();
142
143 content.begin_text();
144 content.set_font_type1(&mut doc, 0, PDType1Font::HelveticaBold, 24.0);
145 content.set_fill_color(Color::rgb(0.1, 0.1, 0.2));
146 content.show_text_at("Techwasti Infotech - rust-pdf-box", 70.0, 730.0);
147 content.end_text();
148
149 content.restore_graphics_state();
150 content.finish(&mut doc, 0).expect("finish failed");
151
152 let mut buf = Vec::new();
153 doc.save_to_writer(&mut buf).expect("save failed");
154
155 let loaded = PDDocument::load_from_bytes(&buf).expect("reload failed");
156 assert_eq!(loaded.page_count(), 1);
157 let loaded_page = loaded.get_page(0).unwrap();
158 assert!(!loaded_page.content_stream_ids().is_empty());
159 }
160
161 #[test]
162 fn test_stamping_watermark_and_rubber_stamp() {
163 let mut doc = PDDocument::new();
164 doc.add_page(PDPage::new(PDRectangle::A4));
165 doc.add_page(PDPage::new(PDRectangle::A4));
166
167 doc.stamp()
169 .pages(PageRange::All)
170 .watermark("CONFIDENTIAL", 50.0, Color::rgb(0.9, 0.1, 0.1), 45.0, 0.2)
171 .apply(&mut doc)
172 .expect("watermark failed");
173
174 doc.stamp()
176 .pages(PageRange::Single(1))
177 .position(StampPosition::TopRight {
178 margin_x: 36.0,
179 margin_y: 36.0,
180 })
181 .rubber_stamp(
182 "APPROVED",
183 Some("Techwasti Infotech".to_string()),
184 Color::rgb(0.0, 0.6, 0.2),
185 -15.0,
186 )
187 .apply(&mut doc)
188 .expect("rubber stamp failed");
189
190 doc.stamp()
192 .pages(PageRange::All)
193 .header(
194 "Document - Page {page} of {total}",
195 StampPosition::TopCenter { margin_y: 20.0 },
196 10.0,
197 Color::GRAY,
198 )
199 .apply(&mut doc)
200 .expect("header failed");
201
202 let mut buf = Vec::new();
203 doc.save_to_writer(&mut buf).expect("save failed");
204
205 let loaded = PDDocument::load_from_bytes(&buf).expect("load failed");
206 assert_eq!(loaded.page_count(), 2);
207 assert!(loaded.get_page(0).unwrap().content_stream_ids().len() >= 3);
208 assert!(loaded.get_page(1).unwrap().content_stream_ids().len() >= 2);
209 }
210
211 #[test]
212 fn test_digital_signing_and_verification() {
213 let mut doc = PDDocument::new();
214 let page = PDPage::new(PDRectangle::A4);
215 doc.add_page(page);
216
217 let signing_key = SigningKey::generate_self_signed("CN=Techwasti, O=Techwasti Infotech")
219 .expect("key generation failed");
220
221 let signer = Signer::new(signing_key)
223 .with_name("Techwasti Infotech")
224 .with_reason("Contract Agreement Verification")
225 .with_location("India")
226 .with_visual_stamp(
227 VisualSignature::at_page(1)
228 .rect(50.0, 50.0, 260.0, 120.0)
229 .title("Digitally Signed by Techwasti")
230 .detail("Status: Validated\nDate: 2026-10-25\nAuthority: Techwasti CA"),
231 );
232
233 let signed_bytes = doc.sign(signer).expect("signing failed");
235 assert!(signed_bytes.len() > 1000);
236
237 let report = SignatureVerifier::verify(&signed_bytes).expect("verification failed");
239 assert!(report.is_valid);
240 assert_eq!(report.signer_name, Some("Techwasti Infotech".to_string()));
241 assert_eq!(
242 report.reason,
243 Some("Contract Agreement Verification".to_string())
244 );
245 assert_eq!(report.location, Some("India".to_string()));
246 }
247
248 #[test]
249 fn test_text_extraction() {
250 let mut doc = PdfDocument::create();
251 doc.add_blank_page(PDRectangle::A4);
252
253 doc.draw_on_page(0, AppendMode::Overwrite, |stream, doc_inner| {
254 stream.begin_text();
255 stream.set_font_type1(doc_inner, 0, PDType1Font::HelveticaBold, 16.0);
256 stream.show_text_at("Hello World from Apache PDFBox in Rust!", 50.0, 700.0);
257 stream.end_text();
258 Ok(())
259 })
260 .expect("draw failed");
261
262 let extracted = doc.extract_text().expect("extraction failed");
263 assert!(extracted.contains("Hello World from Apache PDFBox in Rust!"));
264 }
265
266 #[test]
267 fn test_pdf_merger_and_splitter() {
268 let mut doc1 = PdfDocument::create();
269 doc1.add_blank_page(PDRectangle::A4);
270 doc1.add_blank_page(PDRectangle::A4);
271
272 let mut doc2 = PdfDocument::create();
273 doc2.add_blank_page(PDRectangle::LETTER);
274
275 doc1.merge(&doc2).expect("merge failed");
276 assert_eq!(doc1.page_count(), 3);
277
278 let split_docs = doc1.split_single_pages().expect("split failed");
279 assert_eq!(split_docs.len(), 3);
280 assert_eq!(split_docs[0].page_count(), 1);
281 assert_eq!(split_docs[1].page_count(), 1);
282 assert_eq!(split_docs[2].page_count(), 1);
283 }
284
285 #[test]
286 fn test_fluent_facade_api() {
287 let mut doc = PdfDocument::create();
288 doc.set_title("Invoice #1042")
289 .set_author("Techwasti Infotech");
290
291 doc.add_blank_page(PDRectangle::A4);
292
293 doc.watermark("PAID").expect("watermark failed");
295
296 doc.draw_on_page(0, AppendMode::Append, |stream, doc_inner| {
298 stream.begin_text();
299 stream.set_font_type1(doc_inner, 0, PDType1Font::Helvetica, 14.0);
300 stream.show_text_at("Invoice Total: $1,250.00 USD", 50.0, 750.0);
301 stream.end_text();
302 Ok(())
303 })
304 .expect("draw failed");
305
306 let bytes = doc.to_bytes().expect("to_bytes failed");
308 let reloaded = PdfDocument::open_from_bytes(&bytes).expect("open failed");
309 assert_eq!(reloaded.page_count(), 1);
310 assert_eq!(
311 reloaded.inner.info.title(),
312 Some("Invoice #1042".to_string())
313 );
314 }
315}