Skip to main content

justpdf_core/writer/
page.rs

1use crate::object::{IndirectRef, PdfDict, PdfObject};
2use crate::writer::encode::make_stream;
3use crate::writer::PdfWriter;
4
5/// Builder for constructing a single PDF page with content streams.
6pub struct PageBuilder {
7    width: f64,
8    height: f64,
9    content: Vec<u8>,
10    /// Registered fonts: (resource_name like "F1", font_name like "Helvetica").
11    font_names: Vec<(String, String)>,
12    /// Registered images: (resource_name like "Im1", image_ref).
13    image_names: Vec<(String, IndirectRef)>,
14    /// Registered font references: (resource_name like "F1", font_ref).
15    /// Used for embedded fonts (TrueType etc.) that are indirect objects.
16    font_refs: Vec<(String, IndirectRef)>,
17}
18
19impl PageBuilder {
20    /// Create a new page builder with the given dimensions.
21    /// Default US Letter size is 612 x 792 points.
22    pub fn new(width: f64, height: f64) -> Self {
23        Self {
24            width,
25            height,
26            content: Vec::new(),
27            font_names: Vec::new(),
28            image_names: Vec::new(),
29            font_refs: Vec::new(),
30        }
31    }
32
33    /// Set the current font and size. Emits `BT /{name} {size} Tf`.
34    pub fn set_font(&mut self, resource_name: &str, size: f64) {
35        use std::io::Write;
36        write!(self.content, "/{} {} Tf\n", resource_name, size).unwrap();
37    }
38
39    /// Begin a text object: `BT`.
40    pub fn begin_text(&mut self) {
41        self.content.extend_from_slice(b"BT\n");
42    }
43
44    /// End a text object: `ET`.
45    pub fn end_text(&mut self) {
46        self.content.extend_from_slice(b"ET\n");
47    }
48
49    /// Move to position (x, y): `x y Td`.
50    pub fn move_to(&mut self, x: f64, y: f64) {
51        use std::io::Write;
52        write!(self.content, "{} {} Td\n", x, y).unwrap();
53    }
54
55    /// Show text string with PDF string escaping: `(text) Tj`.
56    pub fn show_text(&mut self, text: &str) {
57        self.content.push(b'(');
58        for &b in text.as_bytes() {
59            match b {
60                b'\\' => self.content.extend_from_slice(b"\\\\"),
61                b'(' => self.content.extend_from_slice(b"\\("),
62                b')' => self.content.extend_from_slice(b"\\)"),
63                _ => self.content.push(b),
64            }
65        }
66        self.content.extend_from_slice(b") Tj\n");
67    }
68
69    /// Set fill color in RGB: `r g b rg`.
70    pub fn set_fill_rgb(&mut self, r: f64, g: f64, b: f64) {
71        use std::io::Write;
72        write!(self.content, "{} {} {} rg\n", r, g, b).unwrap();
73    }
74
75    /// Set stroke color in RGB: `r g b RG`.
76    pub fn set_stroke_rgb(&mut self, r: f64, g: f64, b: f64) {
77        use std::io::Write;
78        write!(self.content, "{} {} {} RG\n", r, g, b).unwrap();
79    }
80
81    /// Draw a line from (x1,y1) to (x2,y2) and stroke: `x1 y1 m x2 y2 l S`.
82    pub fn draw_line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
83        use std::io::Write;
84        write!(self.content, "{} {} m {} {} l S\n", x1, y1, x2, y2).unwrap();
85    }
86
87    /// Draw a stroked rectangle: `x y w h re S`.
88    pub fn draw_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
89        use std::io::Write;
90        write!(self.content, "{} {} {} {} re S\n", x, y, w, h).unwrap();
91    }
92
93    /// Draw a filled rectangle: `x y w h re f`.
94    pub fn fill_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
95        use std::io::Write;
96        write!(self.content, "{} {} {} {} re f\n", x, y, w, h).unwrap();
97    }
98
99    /// Draw an image with transformation: `q w 0 0 h x y cm /name Do Q`.
100    pub fn draw_image(&mut self, name: &str, x: f64, y: f64, w: f64, h: f64) {
101        use std::io::Write;
102        write!(
103            self.content,
104            "q {} 0 0 {} {} {} cm /{} Do Q\n",
105            w, h, x, y, name
106        )
107        .unwrap();
108    }
109
110    /// Draw an inline image directly in the content stream.
111    ///
112    /// Writes `BI /W {width} /H {height} /BPC {bpc} /CS /{cs} ID {data} EI`.
113    pub fn draw_inline_image(
114        &mut self,
115        width: u32,
116        height: u32,
117        bpc: u8,
118        color_space: &str,
119        data: &[u8],
120    ) {
121        use std::io::Write;
122        write!(
123            self.content,
124            "BI /W {} /H {} /BPC {} /CS /{} ID ",
125            width, height, bpc, color_space
126        )
127        .unwrap();
128        self.content.extend_from_slice(data);
129        self.content.extend_from_slice(b" EI\n");
130    }
131
132    /// Register a font resource for this page.
133    pub fn add_font(&mut self, resource_name: &str, font_name: &str) {
134        self.font_names
135            .push((resource_name.to_string(), font_name.to_string()));
136    }
137
138    /// Register an embedded font resource by indirect reference.
139    /// Used for TrueType and other embedded fonts.
140    pub fn add_font_ref(&mut self, resource_name: &str, font_ref: IndirectRef) {
141        self.font_refs
142            .push((resource_name.to_string(), font_ref));
143    }
144
145    /// Register an image resource for this page.
146    pub fn add_image(&mut self, resource_name: &str, image_ref: IndirectRef) {
147        self.image_names
148            .push((resource_name.to_string(), image_ref));
149    }
150
151    /// Build the page object and add it (and its content stream) to the writer.
152    ///
153    /// Returns the indirect reference to the Page dictionary.
154    pub fn build(self, writer: &mut PdfWriter, pages_ref: &IndirectRef) -> IndirectRef {
155        // Create content stream
156        let (stream_dict, stream_data) = make_stream(&self.content, true);
157        let content_stream = PdfObject::Stream {
158            dict: stream_dict,
159            data: stream_data,
160        };
161        let content_ref = writer.add_object(content_stream);
162
163        // Build Resources dictionary
164        let mut resources = PdfDict::new();
165
166        // Font resources
167        if !self.font_names.is_empty() || !self.font_refs.is_empty() {
168            let mut font_dict = PdfDict::new();
169            for (res_name, _font_name) in &self.font_names {
170                // For standard fonts, we create inline font dicts.
171                let mut f = PdfDict::new();
172                f.insert(b"Type".to_vec(), PdfObject::Name(b"Font".to_vec()));
173                f.insert(b"Subtype".to_vec(), PdfObject::Name(b"Type1".to_vec()));
174                let base_font = self
175                    .font_names
176                    .iter()
177                    .find(|(n, _)| n == res_name)
178                    .map(|(_, bf)| bf.clone())
179                    .unwrap_or_default();
180                f.insert(
181                    b"BaseFont".to_vec(),
182                    PdfObject::Name(base_font.into_bytes()),
183                );
184                font_dict.insert(
185                    res_name.as_bytes().to_vec(),
186                    PdfObject::Dict(f),
187                );
188            }
189            // Add embedded font references
190            for (res_name, font_ref) in &self.font_refs {
191                font_dict.insert(
192                    res_name.as_bytes().to_vec(),
193                    PdfObject::Reference(font_ref.clone()),
194                );
195            }
196            resources.insert(b"Font".to_vec(), PdfObject::Dict(font_dict));
197        }
198
199        // Image / XObject resources
200        if !self.image_names.is_empty() {
201            let mut xobject_dict = PdfDict::new();
202            for (res_name, img_ref) in &self.image_names {
203                xobject_dict.insert(
204                    res_name.as_bytes().to_vec(),
205                    PdfObject::Reference(img_ref.clone()),
206                );
207            }
208            resources.insert(b"XObject".to_vec(), PdfObject::Dict(xobject_dict));
209        }
210
211        // Build Page dictionary
212        let mut page_dict = PdfDict::new();
213        page_dict.insert(b"Type".to_vec(), PdfObject::Name(b"Page".to_vec()));
214        page_dict.insert(
215            b"Parent".to_vec(),
216            PdfObject::Reference(pages_ref.clone()),
217        );
218        page_dict.insert(
219            b"MediaBox".to_vec(),
220            PdfObject::Array(vec![
221                PdfObject::Real(0.0),
222                PdfObject::Real(0.0),
223                PdfObject::Real(self.width),
224                PdfObject::Real(self.height),
225            ]),
226        );
227        page_dict.insert(
228            b"Contents".to_vec(),
229            PdfObject::Reference(content_ref),
230        );
231        page_dict.insert(b"Resources".to_vec(), PdfObject::Dict(resources));
232
233        writer.add_object(PdfObject::Dict(page_dict))
234    }
235}
236
237impl Default for PageBuilder {
238    fn default() -> Self {
239        Self::new(612.0, 792.0)
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_page_builder_content() {
249        let mut page = PageBuilder::new(612.0, 792.0);
250        page.begin_text();
251        page.set_font("F1", 12.0);
252        page.move_to(72.0, 720.0);
253        page.show_text("Hello");
254        page.end_text();
255
256        let content = String::from_utf8(page.content.clone()).unwrap();
257        assert!(content.contains("BT\n"));
258        assert!(content.contains("/F1 12 Tf\n"));
259        assert!(content.contains("72 720 Td\n"));
260        assert!(content.contains("(Hello) Tj\n"));
261        assert!(content.contains("ET\n"));
262    }
263
264    #[test]
265    fn test_page_builder_text_escaping() {
266        let mut page = PageBuilder::new(612.0, 792.0);
267        page.show_text("Hello (world) \\ end");
268
269        let content = String::from_utf8(page.content.clone()).unwrap();
270        assert!(content.contains("(Hello \\(world\\) \\\\ end) Tj"));
271    }
272
273    #[test]
274    fn test_page_builder_graphics() {
275        let mut page = PageBuilder::new(612.0, 792.0);
276        page.set_fill_rgb(1.0, 0.0, 0.0);
277        page.fill_rect(10.0, 10.0, 100.0, 50.0);
278        page.set_stroke_rgb(0.0, 0.0, 1.0);
279        page.draw_rect(10.0, 10.0, 100.0, 50.0);
280        page.draw_line(0.0, 0.0, 100.0, 100.0);
281
282        let content = String::from_utf8(page.content.clone()).unwrap();
283        assert!(content.contains("1 0 0 rg\n"));
284        assert!(content.contains("10 10 100 50 re f\n"));
285        assert!(content.contains("0 0 1 RG\n"));
286        assert!(content.contains("10 10 100 50 re S\n"));
287        assert!(content.contains("0 0 m 100 100 l S\n"));
288    }
289
290    #[test]
291    fn test_page_builder_image() {
292        let mut page = PageBuilder::new(612.0, 792.0);
293        page.draw_image("Im1", 0.0, 0.0, 200.0, 150.0);
294
295        let content = String::from_utf8(page.content.clone()).unwrap();
296        assert!(content.contains("q 200 0 0 150 0 0 cm /Im1 Do Q\n"));
297    }
298
299    #[test]
300    fn test_page_builder_build() {
301        let mut writer = PdfWriter::new();
302        let pages_ref = IndirectRef {
303            obj_num: 99,
304            gen_num: 0,
305        };
306
307        let mut page = PageBuilder::new(612.0, 792.0);
308        page.add_font("F1", "Helvetica");
309        page.begin_text();
310        page.set_font("F1", 12.0);
311        page.move_to(72.0, 720.0);
312        page.show_text("Test");
313        page.end_text();
314
315        let page_ref = page.build(&mut writer, &pages_ref);
316
317        // Should have added 2 objects: content stream + page dict
318        assert_eq!(writer.objects.len(), 2);
319        assert_eq!(page_ref.gen_num, 0);
320    }
321
322    #[test]
323    fn test_inline_image() {
324        let mut page = PageBuilder::new(612.0, 792.0);
325        let data = vec![0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00]; // 2x1 RGB
326        page.draw_inline_image(2, 1, 8, "DeviceRGB", &data);
327
328        let content = String::from_utf8_lossy(&page.content);
329        assert!(content.contains("BI /W 2 /H 1 /BPC 8 /CS /DeviceRGB ID "));
330        assert!(content.contains(" EI\n"));
331    }
332
333    #[test]
334    fn test_page_builder_font_ref() {
335        let mut writer = PdfWriter::new();
336        let pages_ref = IndirectRef {
337            obj_num: 99,
338            gen_num: 0,
339        };
340
341        let font_ref = IndirectRef {
342            obj_num: 50,
343            gen_num: 0,
344        };
345
346        let mut page = PageBuilder::new(612.0, 792.0);
347        page.add_font_ref("F1", font_ref);
348        page.begin_text();
349        page.set_font("F1", 12.0);
350        page.show_text("Hello");
351        page.end_text();
352
353        let page_ref = page.build(&mut writer, &pages_ref);
354        assert!(page_ref.obj_num > 0);
355    }
356}