1use crate::writer::fmt_num;
6
7#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
8pub struct Rgb(pub u8, pub u8, pub u8);
9
10#[derive(Clone, Copy, PartialEq, Debug)]
14pub struct TextRotation {
15 pub cx: f32,
17 pub cy: f32,
19 pub angle_deg: f32,
21 pub half_width: f32,
23}
24
25fn color_component(c: u8) -> String {
26 fmt_num(c as f32 / 255.0)
27}
28
29fn color_op(color: Rgb, op: &str) -> String {
33 format!(
34 "{} {} {} {}",
35 color_component(color.0),
36 color_component(color.1),
37 color_component(color.2),
38 op
39 )
40}
41
42pub struct ContentBuilder {
43 buf: Vec<u8>,
44}
45
46impl Default for ContentBuilder {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52impl ContentBuilder {
53 pub fn new() -> Self {
54 ContentBuilder { buf: Vec::new() }
55 }
56
57 fn op(&mut self, s: &str) {
58 self.buf.extend_from_slice(s.as_bytes());
59 self.buf.push(b'\n');
60 }
61
62 pub fn save(&mut self) {
64 self.op("q");
65 }
66
67 pub fn restore(&mut self) {
69 self.op("Q");
70 }
71
72 pub fn begin_marked_content(&mut self, tag: &str, mcid: u32) {
76 self.op(&format!("/{tag} << /MCID {mcid} >> BDC"));
77 }
78
79 pub fn begin_artifact(&mut self) {
89 self.op("/Artifact BMC");
90 }
91
92 pub fn end_marked_content(&mut self) {
94 self.op("EMC");
95 }
96
97 fn rect_operands(x: f32, y: f32, w: f32, h: f32) -> String {
102 format!("{} {} {} {}", fmt_num(x), fmt_num(y), fmt_num(w), fmt_num(h))
103 }
104
105 pub fn clip_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
109 self.op(&format!("{} re W n", Self::rect_operands(x, y, w, h)));
110 }
111
112 fn rect_op(&mut self, rect_prefix: &str, x: f32, y: f32, w: f32, h: f32, op: &str) {
117 self.op(&format!("{} {} re {}", rect_prefix, Self::rect_operands(x, y, w, h), op));
118 }
119
120 pub fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Rgb) {
121 self.rect_op(&color_op(color, "rg"), x, y, w, h, "f");
122 }
123
124 pub fn stroke_rect(&mut self, x: f32, y: f32, w: f32, h: f32, line_width: f32, color: Rgb) {
125 let prefix = format!("{} {} w", color_op(color, "RG"), fmt_num(line_width));
126 self.rect_op(&prefix, x, y, w, h, "S");
127 }
128
129 pub fn set_dash(&mut self, dash: f32, gap: f32) {
130 self.op(&format!("[{} {}] 0 d", fmt_num(dash), fmt_num(gap)));
131 }
132
133 pub fn reset_dash(&mut self) {
134 self.op("[] 0 d");
135 }
136
137 #[allow(clippy::too_many_arguments)]
138 pub fn draw_rounded_rect(
139 &mut self,
140 x: f32,
141 y: f32,
142 w: f32,
143 h: f32,
144 radius: f32,
145 fill: Option<Rgb>,
146 stroke: Option<(f32, Rgb)>,
147 dash: Option<(f32, f32)>,
148 ) {
149 let r = radius.min(w / 2.0).min(h / 2.0);
150 let k = r * 0.552_284_8;
151 let mut path = String::new();
152
153 path.push_str(&format!("{} {} m\n", fmt_num(x + r), fmt_num(y)));
154 path.push_str(&format!("{} {} l\n", fmt_num(x + w - r), fmt_num(y)));
155 path.push_str(&format!(
156 "{} {} {} {} {} {} c\n",
157 fmt_num(x + w - r + k),
158 fmt_num(y),
159 fmt_num(x + w),
160 fmt_num(y + r - k),
161 fmt_num(x + w),
162 fmt_num(y + r)
163 ));
164 path.push_str(&format!("{} {} l\n", fmt_num(x + w), fmt_num(y + h - r)));
165 path.push_str(&format!(
166 "{} {} {} {} {} {} c\n",
167 fmt_num(x + w),
168 fmt_num(y + h - r + k),
169 fmt_num(x + w - r + k),
170 fmt_num(y + h),
171 fmt_num(x + w - r),
172 fmt_num(y + h)
173 ));
174 path.push_str(&format!("{} {} l\n", fmt_num(x + r), fmt_num(y + h)));
175 path.push_str(&format!(
176 "{} {} {} {} {} {} c\n",
177 fmt_num(x + r - k),
178 fmt_num(y + h),
179 fmt_num(x),
180 fmt_num(y + h - r + k),
181 fmt_num(x),
182 fmt_num(y + h - r)
183 ));
184 path.push_str(&format!("{} {} l\n", fmt_num(x), fmt_num(y + r)));
185 path.push_str(&format!(
186 "{} {} {} {} {} {} c\nh",
187 fmt_num(x),
188 fmt_num(y + r - k),
189 fmt_num(x + r - k),
190 fmt_num(y),
191 fmt_num(x + r),
192 fmt_num(y)
193 ));
194
195 if let Some((dash_len, gap_len)) = dash {
196 self.set_dash(dash_len, gap_len);
197 }
198
199 match (fill, stroke) {
200 (Some(f), Some((sw, s))) => {
201 let fill_str = color_op(f, "rg");
202 let stroke_str = color_op(s, "RG");
203 self.op(&format!("{} {} {} w\n{} b", fill_str, stroke_str, fmt_num(sw), path));
204 }
205 (Some(f), None) => {
206 let fill_str = color_op(f, "rg");
207 self.op(&format!("{}\n{} f", fill_str, path));
208 }
209 (None, Some((sw, s))) => {
210 let stroke_str = color_op(s, "RG");
211 self.op(&format!("{} {} w\n{} S", stroke_str, fmt_num(sw), path));
212 }
213 (None, None) => {}
214 }
215
216 if dash.is_some() {
217 self.reset_dash();
218 }
219 }
220
221 pub fn line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, line_width: f32, color: Rgb) {
222 self.op(&format!(
223 "{} {} w {} {} m {} {} l S",
224 color_op(color, "RG"),
225 fmt_num(line_width),
226 fmt_num(x1),
227 fmt_num(y1),
228 fmt_num(x2),
229 fmt_num(y2)
230 ));
231 }
232
233 pub fn text(&mut self, font_resource: &str, size: f32, x: f32, y: f32, color: Rgb, encoded_bytes: &[u8]) {
240 self.text_block(font_resource, size, color, x, y, encoded_bytes);
241 self.buf.push(b'\n');
242 }
243
244 fn write_hex_string(&mut self, encoded_bytes: &[u8]) {
248 for b in encoded_bytes {
249 self.buf.extend_from_slice(format!("{b:02X}").as_bytes());
250 }
251 }
252
253 fn text_block(&mut self, font_resource: &str, size: f32, color: Rgb, tx: f32, ty: f32, encoded_bytes: &[u8]) {
259 self.buf.extend_from_slice(
260 format!(
261 "BT /{} {} Tf {} {} {} Td <",
262 font_resource,
263 fmt_num(size),
264 color_op(color, "rg"),
265 fmt_num(tx),
266 fmt_num(ty),
267 )
268 .as_bytes(),
269 );
270 self.write_hex_string(encoded_bytes);
271 self.buf.extend_from_slice(b"> Tj ET");
272 }
273
274 pub fn text_rotated(&mut self, font_resource: &str, size: f32, rotation: TextRotation, color: Rgb, encoded_bytes: &[u8]) {
283 let rad = rotation.angle_deg.to_radians();
284 let cos = rad.cos();
285 let sin = rad.sin();
286 self.buf.extend_from_slice(
287 format!(
288 "q {} {} {} {} {} {} cm ",
289 fmt_num(cos),
290 fmt_num(sin),
291 fmt_num(-sin),
292 fmt_num(cos),
293 fmt_num(rotation.cx),
294 fmt_num(rotation.cy),
295 )
296 .as_bytes(),
297 );
298 self.text_block(font_resource, size, color, -rotation.half_width, 0.0, encoded_bytes);
299 self.buf.extend_from_slice(b" Q\n");
300 }
301
302 pub fn draw_image(&mut self, image_resource: &str, x: f32, y: f32, w: f32, h: f32) {
307 self.op(&format!(
308 "q {} 0 0 {} {} {} cm /{} Do Q",
309 fmt_num(w),
310 fmt_num(h),
311 fmt_num(x),
312 fmt_num(y),
313 image_resource
314 ));
315 }
316
317 pub fn into_bytes(self) -> Vec<u8> {
318 self.buf
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn builds_expected_operators() {
328 let mut c = ContentBuilder::new();
329 c.save();
330 c.clip_rect(0.0, 0.0, 100.0, 50.0);
331 c.text("F1", 12.0, 10.0, 20.0, Rgb(0, 0, 0), &[0x00, 0x01, 0x00, 0x02, 0x00, 0x03]);
333 c.restore();
334 let bytes = c.into_bytes();
335 let s = String::from_utf8(bytes).unwrap();
336 assert!(s.contains("q\n"));
337 assert!(s.contains("0 0 100 50 re W n"));
338 assert!(s.contains("<000100020003> Tj"));
339 assert!(s.trim_end().ends_with('Q'));
340 }
341
342 #[test]
343 fn draw_image_emits_the_cm_do_idiom() {
344 let mut c = ContentBuilder::new();
345 c.draw_image("Im1", 10.0, 20.0, 100.0, 50.0);
346 let s = String::from_utf8(c.into_bytes()).unwrap();
347 assert!(s.contains("100 0 0 50 10 20 cm /Im1 Do"));
348 }
349
350 #[test]
351 fn text_rotated_emits_a_rotation_matrix_and_centers_horizontally() {
352 let mut c = ContentBuilder::new();
353 c.text_rotated(
354 "F1",
355 72.0,
356 TextRotation {
357 cx: 300.0,
358 cy: 400.0,
359 angle_deg: 45.0,
360 half_width: 50.0,
361 },
362 Rgb(210, 210, 210),
363 &[0x00, 0x01],
364 );
365 let s = String::from_utf8(c.into_bytes()).unwrap();
366 assert!(s.contains("0.707 0.707 -0.707 0.707 300 400 cm"));
368 assert!(
369 s.contains("-50 0 Td"),
370 "text must be horizontally centered via a -half_width offset"
371 );
372 assert!(s.trim_end().ends_with("Q"));
373 }
374}