Skip to main content

pdf_ast/transform/
builder.rs

1use crate::ast::{AstNode, NodeId, NodeType, PdfAstGraph};
2use crate::types::{PdfArray, PdfDictionary, PdfName, PdfString, PdfValue};
3
4/// Builder for creating PDF document structures
5pub struct DocumentBuilder {
6    graph: PdfAstGraph,
7    catalog_id: Option<NodeId>,
8    pages_root_id: Option<NodeId>,
9    current_page: Option<NodeId>,
10    info_id: Option<NodeId>,
11}
12
13impl DocumentBuilder {
14    pub fn new() -> Self {
15        Self {
16            graph: PdfAstGraph::new(),
17            catalog_id: None,
18            pages_root_id: None,
19            current_page: None,
20            info_id: None,
21        }
22    }
23
24    /// Create document catalog
25    pub fn with_catalog(&mut self) -> &mut Self {
26        let mut catalog_dict = PdfDictionary::new();
27        catalog_dict.insert("Type", PdfValue::Name(PdfName::new("Catalog")));
28
29        let catalog_node = AstNode::new(
30            NodeId(1),
31            NodeType::Catalog,
32            PdfValue::Dictionary(catalog_dict),
33        );
34
35        let catalog_id = self
36            .graph
37            .create_node(NodeType::Catalog, catalog_node.value);
38        self.graph.set_root(catalog_id);
39        self.catalog_id = Some(catalog_id);
40
41        self
42    }
43
44    /// Create pages tree
45    pub fn with_pages_tree(&mut self) -> &mut Self {
46        if self.catalog_id.is_none() {
47            self.with_catalog();
48        }
49
50        let mut pages_dict = PdfDictionary::new();
51        pages_dict.insert("Type", PdfValue::Name(PdfName::new("Pages")));
52        pages_dict.insert("Count", PdfValue::Integer(0));
53        pages_dict.insert("Kids", PdfValue::Array(PdfArray::new()));
54
55        let pages_id = self
56            .graph
57            .create_node(NodeType::Pages, PdfValue::Dictionary(pages_dict));
58
59        if let Some(catalog_id) = self.catalog_id {
60            self.graph
61                .add_edge(catalog_id, pages_id, crate::ast::EdgeType::Child);
62        }
63
64        self.pages_root_id = Some(pages_id);
65        self
66    }
67
68    /// Add a page
69    pub fn add_page(&mut self, width: f32, height: f32) -> &mut Self {
70        if self.pages_root_id.is_none() {
71            self.with_pages_tree();
72        }
73
74        let mut page_dict = PdfDictionary::new();
75        page_dict.insert("Type", PdfValue::Name(PdfName::new("Page")));
76
77        // MediaBox
78        let mut media_box = PdfArray::new();
79        media_box.push(PdfValue::Integer(0));
80        media_box.push(PdfValue::Integer(0));
81        media_box.push(PdfValue::Real(width as f64));
82        media_box.push(PdfValue::Real(height as f64));
83        page_dict.insert("MediaBox", PdfValue::Array(media_box));
84
85        // Resources
86        let resources_dict = PdfDictionary::new();
87        page_dict.insert("Resources", PdfValue::Dictionary(resources_dict));
88
89        let page_id = self
90            .graph
91            .create_node(NodeType::Page, PdfValue::Dictionary(page_dict));
92
93        if let Some(pages_id) = self.pages_root_id {
94            self.graph
95                .add_edge(pages_id, page_id, crate::ast::EdgeType::Child);
96
97            // Update page count in pages tree
98            if let Some(pages_node) = self.graph.get_node_mut(pages_id) {
99                if let PdfValue::Dictionary(dict) = &mut pages_node.value {
100                    if let Some(PdfValue::Integer(count)) = dict.get_mut("Count") {
101                        *count += 1;
102                    }
103
104                    // Add to Kids array
105                    if let Some(PdfValue::Array(kids)) = dict.get_mut("Kids") {
106                        kids.push(PdfValue::Reference(crate::types::PdfReference::new(
107                            page_id.0 as u32,
108                            0,
109                        )));
110                    }
111                }
112            }
113        }
114
115        self.current_page = Some(page_id);
116        self
117    }
118
119    /// Add content stream to current page
120    pub fn add_content_stream(&mut self, content: &str) -> &mut Self {
121        if let Some(page_id) = self.current_page {
122            let content_data = content.as_bytes().to_vec();
123            let stream = crate::types::PdfStream {
124                dict: {
125                    let mut dict = PdfDictionary::new();
126                    dict.insert("Length", PdfValue::Integer(content_data.len() as i64));
127                    dict
128                },
129                data: crate::types::StreamData::Raw(content_data),
130            };
131
132            let stream_id = self
133                .graph
134                .create_node(NodeType::ContentStream, PdfValue::Stream(stream));
135
136            self.graph
137                .add_edge(page_id, stream_id, crate::ast::EdgeType::Child);
138
139            // Update page dictionary to reference content stream
140            if let Some(page_node) = self.graph.get_node_mut(page_id) {
141                if let PdfValue::Dictionary(dict) = &mut page_node.value {
142                    dict.insert(
143                        "Contents",
144                        PdfValue::Reference(crate::types::PdfReference::new(stream_id.0 as u32, 0)),
145                    );
146                }
147            }
148        }
149
150        self
151    }
152
153    /// Add font to current page resources
154    pub fn add_font(&mut self, name: &str, font_type: FontType, base_font: &str) -> &mut Self {
155        if let Some(page_id) = self.current_page {
156            let mut font_dict = PdfDictionary::new();
157            font_dict.insert("Type", PdfValue::Name(PdfName::new("Font")));
158            font_dict.insert("Subtype", PdfValue::Name(PdfName::new(font_type.as_str())));
159            font_dict.insert("BaseFont", PdfValue::Name(PdfName::new(base_font)));
160
161            let font_id = self.graph.create_node(
162                match font_type {
163                    FontType::Type1 => NodeType::Type1Font,
164                    FontType::TrueType => NodeType::TrueTypeFont,
165                    FontType::Type3 => NodeType::Type3Font,
166                },
167                PdfValue::Dictionary(font_dict),
168            );
169
170            self.graph
171                .add_edge(page_id, font_id, crate::ast::EdgeType::Child);
172
173            // Update page resources
174            if let Some(page_node) = self.graph.get_node_mut(page_id) {
175                if let PdfValue::Dictionary(page_dict) = &mut page_node.value {
176                    if let Some(PdfValue::Dictionary(resources)) = page_dict.get_mut("Resources") {
177                        // Get or create Font dictionary
178                        let font_dict = resources
179                            .entry("Font")
180                            .or_insert_with(|| PdfValue::Dictionary(PdfDictionary::new()));
181
182                        if let PdfValue::Dictionary(fonts) = font_dict {
183                            fonts.insert(
184                                name,
185                                PdfValue::Reference(crate::types::PdfReference::new(
186                                    font_id.0 as u32,
187                                    0,
188                                )),
189                            );
190                        }
191                    }
192                }
193            }
194        }
195
196        self
197    }
198
199    /// Add document info
200    pub fn with_info(
201        &mut self,
202        title: Option<&str>,
203        author: Option<&str>,
204        creator: Option<&str>,
205    ) -> &mut Self {
206        let mut info_dict = PdfDictionary::new();
207
208        if let Some(title) = title {
209            info_dict.insert(
210                "Title",
211                PdfValue::String(PdfString::new_literal(title.as_bytes())),
212            );
213        }
214
215        if let Some(author) = author {
216            info_dict.insert(
217                "Author",
218                PdfValue::String(PdfString::new_literal(author.as_bytes())),
219            );
220        }
221
222        if let Some(creator) = creator {
223            info_dict.insert(
224                "Creator",
225                PdfValue::String(PdfString::new_literal(creator.as_bytes())),
226            );
227        }
228
229        // Add creation date
230        let now = chrono::Utc::now().format("D:%Y%m%d%H%M%S%z").to_string();
231        info_dict.insert(
232            "CreationDate",
233            PdfValue::String(PdfString::new_literal(now.as_bytes())),
234        );
235
236        let info_id = self
237            .graph
238            .create_node(NodeType::Metadata, PdfValue::Dictionary(info_dict));
239
240        if let Some(catalog_id) = self.catalog_id {
241            self.graph
242                .add_edge(catalog_id, info_id, crate::ast::EdgeType::Child);
243        }
244
245        self.info_id = Some(info_id);
246        self
247    }
248
249    /// Add annotation to current page
250    pub fn add_annotation(&mut self, annotation_type: AnnotationType, rect: [f32; 4]) -> &mut Self {
251        if let Some(page_id) = self.current_page {
252            let mut annot_dict = PdfDictionary::new();
253            annot_dict.insert("Type", PdfValue::Name(PdfName::new("Annot")));
254            annot_dict.insert(
255                "Subtype",
256                PdfValue::Name(PdfName::new(annotation_type.as_str())),
257            );
258
259            // Rect
260            let mut rect_array = PdfArray::new();
261            for &coord in &rect {
262                rect_array.push(PdfValue::Real(coord as f64));
263            }
264            annot_dict.insert("Rect", PdfValue::Array(rect_array));
265
266            let annot_id = self
267                .graph
268                .create_node(NodeType::Annotation, PdfValue::Dictionary(annot_dict));
269
270            self.graph
271                .add_edge(page_id, annot_id, crate::ast::EdgeType::Child);
272        }
273
274        self
275    }
276
277    /// Build the final document
278    pub fn build(self) -> PdfAstGraph {
279        self.graph
280    }
281
282    /// Get the current graph reference
283    pub fn graph(&self) -> &PdfAstGraph {
284        &self.graph
285    }
286
287    /// Get mutable graph reference
288    pub fn graph_mut(&mut self) -> &mut PdfAstGraph {
289        &mut self.graph
290    }
291}
292
293impl Default for DocumentBuilder {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299#[derive(Debug, Clone, Copy)]
300pub enum FontType {
301    Type1,
302    TrueType,
303    Type3,
304}
305
306impl FontType {
307    fn as_str(&self) -> &'static str {
308        match self {
309            FontType::Type1 => "Type1",
310            FontType::TrueType => "TrueType",
311            FontType::Type3 => "Type3",
312        }
313    }
314}
315
316#[derive(Debug, Clone, Copy)]
317pub enum AnnotationType {
318    Text,
319    Link,
320    FreeText,
321    Line,
322    Square,
323    Circle,
324    Highlight,
325    Underline,
326    Squiggly,
327    StrikeOut,
328    Stamp,
329    Caret,
330    Ink,
331    Popup,
332    FileAttachment,
333    Sound,
334    Movie,
335    Widget,
336    Screen,
337    PrinterMark,
338    TrapNet,
339    Watermark,
340    ThreeD,
341    Redact,
342}
343
344impl AnnotationType {
345    fn as_str(&self) -> &'static str {
346        match self {
347            AnnotationType::Text => "Text",
348            AnnotationType::Link => "Link",
349            AnnotationType::FreeText => "FreeText",
350            AnnotationType::Line => "Line",
351            AnnotationType::Square => "Square",
352            AnnotationType::Circle => "Circle",
353            AnnotationType::Highlight => "Highlight",
354            AnnotationType::Underline => "Underline",
355            AnnotationType::Squiggly => "Squiggly",
356            AnnotationType::StrikeOut => "StrikeOut",
357            AnnotationType::Stamp => "Stamp",
358            AnnotationType::Caret => "Caret",
359            AnnotationType::Ink => "Ink",
360            AnnotationType::Popup => "Popup",
361            AnnotationType::FileAttachment => "FileAttachment",
362            AnnotationType::Sound => "Sound",
363            AnnotationType::Movie => "Movie",
364            AnnotationType::Widget => "Widget",
365            AnnotationType::Screen => "Screen",
366            AnnotationType::PrinterMark => "PrinterMark",
367            AnnotationType::TrapNet => "TrapNet",
368            AnnotationType::Watermark => "Watermark",
369            AnnotationType::ThreeD => "3D",
370            AnnotationType::Redact => "Redact",
371        }
372    }
373}