Skip to main content

rust_pdf_box/
lib.rs

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        // 1. Watermark under content on all pages
168        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        // 2. Rubber stamp over content on page 1
175        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        // 3. Header and Footer
191        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        // 1. Generate in-memory self-signed signing key in pure Rust
218        let signing_key = SigningKey::generate_self_signed("CN=Techwasti, O=Techwasti Infotech")
219            .expect("key generation failed");
220
221        // 2. Configure signer with visual stamp
222        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        // 3. Sign the PDF
234        let signed_bytes = doc.sign(signer).expect("signing failed");
235        assert!(signed_bytes.len() > 1000);
236
237        // 4. Verify signature in the generated PDF
238        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        // Fluent watermark
294        doc.watermark("PAID").expect("watermark failed");
295
296        // Simple text drawing
297        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        // Verify serialization
307        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}