Skip to main content

pdfluent_lopdf/
creator.rs

1use crate::Result;
2use crate::{Dictionary, Document, FontData, Object, ObjectId, Stream};
3
4impl Document {
5    /// Create new PDF document with version.
6    pub fn with_version<S: Into<String>>(version: S) -> Document {
7        let mut document = Self::new();
8        document.version = version.into();
9        document
10    }
11
12    /// Create an object ID.
13    pub fn new_object_id(&mut self) -> ObjectId {
14        self.max_id += 1;
15        (self.max_id, 0)
16    }
17
18    /// Add PDF object into document's object list.
19    pub fn add_object<T: Into<Object>>(&mut self, object: T) -> ObjectId {
20        self.max_id += 1;
21        let id = (self.max_id, 0);
22        self.objects.insert(id, object.into());
23        id
24    }
25
26    pub fn set_object<T: Into<Object>>(&mut self, id: ObjectId, object: T) {
27        self.objects.insert(id, object.into());
28    }
29
30    /// Remove PDF object from document's object list.
31    ///
32    /// Other objects may still hold references to this object! Therefore, removing the object might
33    /// lead to dangling references.
34    pub fn remove_object(&mut self, object_id: &ObjectId) -> Result<()> {
35        self.objects.remove(object_id);
36        Ok(())
37    }
38
39    /// Remove annotation from the document.
40    ///
41    /// References to this annotation are removed from the pages' lists of annotations. Finally, the
42    /// annotation object itself is removed.
43    pub fn remove_annot(&mut self, object_id: &ObjectId) -> Result<()> {
44        for (_, page_id) in self.get_pages() {
45            let page = self.get_object_mut(page_id)?.as_dict_mut()?;
46            let annots = page.get_mut(b"Annots")?.as_array_mut()?;
47
48            annots.retain(|object| {
49                if let Ok(id) = object.as_reference() {
50                    return id != *object_id;
51                }
52
53                true
54            });
55        }
56
57        self.remove_object(object_id)?;
58
59        Ok(())
60    }
61
62    /// Get the page's resource dictionary.
63    ///
64    /// Get Object that has the key "Resources".
65    pub fn get_or_create_resources(&mut self, page_id: ObjectId) -> Result<&mut Object> {
66        let resources_id = {
67            let page = self.get_object(page_id).and_then(Object::as_dict)?;
68            if page.has(b"Resources") {
69                page.get(b"Resources").and_then(Object::as_reference).ok()
70            } else {
71                None
72            }
73        };
74        if let Some(res_id) = resources_id {
75            return self.get_object_mut(res_id);
76        }
77        let page = self.get_object_mut(page_id).and_then(Object::as_dict_mut)?;
78        if !page.has(b"Resources") {
79            page.set(b"Resources", Dictionary::new());
80        }
81        page.get_mut(b"Resources")
82    }
83
84    /// Add XObject to a page.
85    ///
86    /// Get Object that has the key `Resources -> XObject`.
87    pub fn add_xobject<N: Into<Vec<u8>>>(
88        &mut self,
89        page_id: ObjectId,
90        xobject_name: N,
91        xobject_id: ObjectId,
92    ) -> Result<()> {
93        if let Ok(resources) = self
94            .get_or_create_resources(page_id)
95            .and_then(Object::as_dict_mut)
96        {
97            if !resources.has(b"XObject") {
98                resources.set("XObject", Dictionary::new());
99            }
100            let mut xobjects = resources.get_mut(b"XObject")?;
101            if let Object::Reference(xobjects_ref_id) = xobjects {
102                let mut xobjects_id = *xobjects_ref_id;
103                while let Object::Reference(id) = self.get_object(xobjects_id)? {
104                    xobjects_id = *id;
105                }
106                xobjects = self.get_object_mut(xobjects_id)?;
107            }
108            let xobjects = Object::as_dict_mut(xobjects)?;
109            xobjects.set(xobject_name, Object::Reference(xobject_id));
110        }
111        Ok(())
112    }
113
114    /// Add Graphics State to a page.
115    ///
116    /// Get Object that has the key `Resources -> ExtGState`.
117    pub fn add_graphics_state<N: Into<Vec<u8>>>(
118        &mut self,
119        page_id: ObjectId,
120        gs_name: N,
121        gs_id: ObjectId,
122    ) -> Result<()> {
123        if let Ok(resources) = self
124            .get_or_create_resources(page_id)
125            .and_then(Object::as_dict_mut)
126        {
127            if !resources.has(b"ExtGState") {
128                resources.set("ExtGState", Dictionary::new());
129            }
130            let states = resources
131                .get_mut(b"ExtGState")
132                .and_then(Object::as_dict_mut)?;
133            states.set(gs_name, Object::Reference(gs_id));
134        }
135        Ok(())
136    }
137
138    /// Add font to a page.
139    /// # Examples
140    ///
141    /// ```no_run
142    /// // Assuming you have a font file at "./SomeFont.ttf"
143    /// use pdfluent_lopdf::dictionary;
144    ///
145    /// let font_file = std::fs::read("./SomeFont.ttf").unwrap();
146    ///
147    /// // Create a new FontData instance with the font file.
148    /// let font_name = "SomeFont".to_string();
149    /// let mut font_data = pdfluent_lopdf::FontData::new(&font_file, font_name.clone());
150    ///
151    /// // Customize the font data if needed.
152    /// font_data
153    ///     .set_italic_angle(10)
154    ///     .set_encoding("WinAnsiEncoding".to_string());
155    ///
156    ///
157    /// // Create a new PDF document.
158    /// let mut doc = pdfluent_lopdf::Document::with_version("1.5");
159    ///
160    /// // Add the font to the document.
161    /// let font_id = doc.add_font(font_data).unwrap();
162    ///
163    /// // Now you can use `font_id` to reference the font in your PDF content.
164    /// // For example:
165    /// let resources_id = doc.add_object(dictionary! {
166    ///  "Font" => dictionary! {
167    ///         font_name => font_id,
168    ///     },
169    /// });
170    /// ```
171    pub fn add_font(&mut self, font_data: FontData) -> Result<ObjectId> {
172        // Create embedded font stream
173        let font_stream = Stream::new(
174            dictionary! {
175                "Length1" => Object::Integer(font_data.bytes().len() as i64),
176            },
177            font_data.bytes(),
178        );
179        let font_file_id = self.add_object(font_stream);
180        let font_name = font_data.font_name.clone();
181
182        // Create font descriptor dictionary
183        let font_descriptor_id = self.add_object(dictionary! {
184            "Type" => "FontDescriptor",
185            "FontName" => Object::Name(font_name.clone().into_bytes()),
186            "Flags" => Object::Integer(font_data.flags),
187            "FontBBox" => Object::Array(vec![
188                Object::Integer(font_data.font_bbox.0),
189                Object::Integer(font_data.font_bbox.1),
190                Object::Integer(font_data.font_bbox.2),
191                Object::Integer(font_data.font_bbox.3),
192            ]),
193            "ItalicAngle" => Object::Integer(font_data.italic_angle),
194            "Ascent" => Object::Integer(font_data.ascent),
195            "Descent" => Object::Integer(font_data.descent),
196            "CapHeight" => Object::Integer(font_data.cap_height),
197            "StemV" => Object::Integer(font_data.stem_v),
198            "FontFile2" => Object::Reference(font_file_id),
199        });
200
201        // Create font dictionary
202        let font_id = self.add_object(dictionary! {
203            "Type" => "Font",
204            "Subtype" => "TrueType",
205            "BaseFont" => Object::Name(font_name.clone().into_bytes()),
206            "FontDescriptor" => Object::Reference(font_descriptor_id),
207            "Encoding" => Object::Name(font_data.encoding.into_bytes()),
208        });
209
210        Ok(font_id)
211    }
212}
213
214#[cfg(test)]
215pub mod tests {
216    use std::path::PathBuf;
217
218    use crate::content::*;
219    use crate::{Document, FontData, Object, Stream};
220
221    #[cfg(not(feature = "time"))]
222    pub fn get_timestamp() -> Object {
223        Object::string_literal("D:19700101000000Z")
224    }
225
226    #[cfg(feature = "time")]
227    pub fn get_timestamp() -> Object {
228        time::OffsetDateTime::now_utc().into()
229    }
230
231    /// Create and return a document for testing
232    pub fn create_document() -> Document {
233        create_document_with_texts(&["Hello World!"])
234    }
235
236    pub fn create_document_with_texts(texts_for_pages: &[&str]) -> Document {
237        let mut doc = Document::with_version("1.5");
238        let info_id = doc.add_object(dictionary! {
239            "Title" => Object::string_literal("Create PDF document example"),
240            "Creator" => Object::string_literal("https://crates.io/crates/lopdf"),
241            "CreationDate" => get_timestamp(),
242        });
243        let pages_id = doc.new_object_id();
244        let font_id = doc.add_object(dictionary! {
245            "Type" => "Font",
246            "Subtype" => "Type1",
247            "BaseFont" => "Courier",
248        });
249        let resources_id = doc.add_object(dictionary! {
250            "Font" => dictionary! {
251                "F1" => font_id,
252            },
253        });
254        let contents = texts_for_pages.iter().map(|text| Content {
255            operations: vec![
256                Operation::new("BT", vec![]),
257                Operation::new("Tf", vec!["F1".into(), 48.into()]),
258                Operation::new("Td", vec![100.into(), 600.into()]),
259                Operation::new("Tj", vec![Object::string_literal(*text)]),
260                Operation::new("ET", vec![]),
261            ],
262        });
263
264        let pages = contents.map(|content| {
265            let content_id = doc.add_object(Stream::new(dictionary! {}, content.encode().unwrap()));
266            let page = doc.add_object(dictionary! {
267                "Type" => "Page",
268                "Parent" => pages_id,
269                "Contents" => content_id,
270            });
271            page.into()
272        });
273
274        let pages = dictionary! {
275            "Type" => "Pages",
276            "Kids" => pages.collect::<Vec<Object>>(),
277            "Count" => 1,
278            "Resources" => resources_id,
279            "MediaBox" => vec![0.into(), 0.into(), 595.into(), 842.into()],
280        };
281        doc.objects.insert(pages_id, Object::Dictionary(pages));
282        let catalog_id = doc.add_object(dictionary! {
283            "Type" => "Catalog",
284            "Pages" => pages_id,
285        });
286        doc.trailer.set("Root", catalog_id);
287        doc.trailer.set("Info", info_id);
288        doc.trailer.set(
289            "ID",
290            Object::Array(vec![
291                Object::string_literal(b"ABC"),
292                Object::string_literal(b"DEF"),
293            ]),
294        );
295        doc.compress();
296        doc
297    }
298
299    /// Save a document
300    pub fn save_document(file_path: &PathBuf, doc: &mut Document) {
301        let res = doc.save(file_path);
302
303        assert!(match res {
304            Ok(_file) => true,
305            Err(_e) => false,
306        });
307    }
308
309    #[test]
310    fn save_created_document() {
311        // Create temporary folder to store file.
312        let temp_dir = tempfile::tempdir().unwrap();
313        let file_path = temp_dir.path().join("test_1_create.pdf");
314
315        let mut doc = create_document();
316        // Save to file
317        save_document(&file_path, &mut doc);
318        assert!(file_path.exists());
319    }
320
321    #[test]
322    #[ignore] // Requires test font file not included in fork
323    fn test_add_font_embeds_font_correctly() {
324        // Create a dummy TTF font in memory (fake content, just to test structure)
325        let font_file = std::fs::read("./tests/resources/fonts/Montserrat-Regular.ttf").unwrap();
326
327        // Construct FontData manually
328        let mut font_data = FontData::new(&font_file, "MyFont".to_string());
329        font_data
330            .set_flags(32)
331            .set_font_bbox((0, -200, 1000, 800))
332            .set_italic_angle(0)
333            .set_ascent(750)
334            .set_descent(-250)
335            .set_cap_height(700)
336            .set_stem_v(80)
337            .set_encoding("WinAnsiEncoding".to_string());
338
339        // Create PDF document
340        let mut doc = Document::with_version("1.5");
341
342        // Create dummy page
343        let page_id = doc.new_object_id();
344        doc.set_object(page_id, dictionary! {});
345
346        // Add font
347        let font_id = doc.add_font(font_data.clone()).unwrap();
348
349        // Font dictionary must exist
350        let font_obj = doc.get_object(font_id).unwrap();
351        let font_dict = font_obj.as_dict().unwrap();
352
353        // Check base font name
354        assert_eq!(
355            font_dict.get(b"BaseFont").unwrap(),
356            &Object::Name(b"MyFont".to_vec())
357        );
358
359        // Check encoding
360        assert_eq!(
361            font_dict.get(b"Encoding").unwrap(),
362            &Object::Name(b"WinAnsiEncoding".to_vec())
363        );
364
365        // Check font descriptor exists and is referenced
366        let descriptor_ref = font_dict
367            .get(b"FontDescriptor")
368            .unwrap()
369            .as_reference()
370            .unwrap();
371        let descriptor_obj = doc.get_object(descriptor_ref).unwrap().as_dict().unwrap();
372        assert_eq!(
373            descriptor_obj.get(b"FontName").unwrap(),
374            &Object::Name(b"MyFont".to_vec())
375        );
376
377        // Check font file is embedded
378        let font_file_ref = descriptor_obj
379            .get(b"FontFile2")
380            .unwrap()
381            .as_reference()
382            .unwrap();
383        let font_stream = doc.get_object(font_file_ref).unwrap().as_stream().unwrap();
384        assert_eq!(font_stream.content, font_file);
385    }
386}