1use crate::object::{IndirectRef, PdfDict, PdfObject};
2use crate::writer::encode::make_stream;
3use crate::writer::PdfWriter;
4
5pub struct PageBuilder {
7 width: f64,
8 height: f64,
9 content: Vec<u8>,
10 font_names: Vec<(String, String)>,
12 image_names: Vec<(String, IndirectRef)>,
14 font_refs: Vec<(String, IndirectRef)>,
17}
18
19impl PageBuilder {
20 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 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 pub fn begin_text(&mut self) {
41 self.content.extend_from_slice(b"BT\n");
42 }
43
44 pub fn end_text(&mut self) {
46 self.content.extend_from_slice(b"ET\n");
47 }
48
49 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 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 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 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 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 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 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 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 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 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 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 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 pub fn build(self, writer: &mut PdfWriter, pages_ref: &IndirectRef) -> IndirectRef {
155 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 let mut resources = PdfDict::new();
165
166 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 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 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 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 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 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]; 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}