Skip to main content

pdfboss_write/
canvas.rs

1//! The imperative painting surface. A `Canvas` accumulates
2//! `pdfboss_core::content::Op` values — the exact IR the reader parses —
3//! plus the fonts and images those operators reference. Nothing here
4//! serializes; `finish` hands the parts to document assembly.
5//!
6//! Resource naming contract: the font first used gets resource name `F1`,
7//! the next distinct font `F2`, …; image handles map to `Im1`, `Im2`, … in
8//! [`add_image`](Canvas::add_image) order. Assembly builds the matching
9//! `/Resources` dictionary from [`CanvasParts`].
10
11use pdfboss_core::content::Op;
12use pdfboss_core::{Matrix, Name, Point};
13
14use crate::color::Color;
15use crate::error::Result;
16use crate::font::Standard14;
17use crate::image::ImageData;
18
19#[allow(clippy::excessive_precision)] // the contract names this exact literal
20const KAPPA: f32 = 0.552284749831;
21
22/// Line cap style (ISO 32000 §8.4.3.3).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum LineCap {
25    /// Squared off at the endpoint.
26    #[default]
27    Butt,
28    /// Semicircle around the endpoint.
29    Round,
30    /// Square projecting half a width beyond the endpoint.
31    Square,
32}
33
34/// Line join style (ISO 32000 §8.4.3.4).
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum LineJoin {
37    /// Outer edges extended to a point.
38    #[default]
39    Miter,
40    /// Circular arc around the corner.
41    Round,
42    /// Cut off with a straight edge.
43    Bevel,
44}
45
46/// Handle to an image registered on a canvas.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct ImageHandle(pub(crate) usize);
49
50/// The accumulated output of a finished canvas.
51#[derive(Debug)]
52pub struct CanvasParts {
53    /// Operators, in paint order.
54    pub ops: Vec<Op>,
55    /// Distinct fonts in first-use order; index `i` is resource `F{i+1}`.
56    pub fonts: Vec<Standard14>,
57    /// Images in registration order; index `i` is resource `Im{i+1}`.
58    pub images: Vec<ImageData>,
59}
60
61/// An imperative painter producing content-stream operators.
62#[derive(Debug, Default)]
63pub struct Canvas {
64    ops: Vec<Op>,
65    fonts: Vec<Standard14>,
66    images: Vec<ImageData>,
67}
68
69impl Canvas {
70    /// Creates an empty canvas.
71    pub fn new() -> Canvas {
72        Canvas::default()
73    }
74
75    /// Pushes the graphics state (`q`).
76    pub fn save(&mut self) {
77        self.ops.push(Op::Save);
78    }
79
80    /// Pops the graphics state (`Q`).
81    pub fn restore(&mut self) {
82        self.ops.push(Op::Restore);
83    }
84
85    /// Concatenates `m` onto the current transformation matrix (`cm`).
86    pub fn transform(&mut self, m: Matrix) {
87        self.ops.push(Op::Concat(m));
88    }
89
90    /// Sets the stroke line width (`w`).
91    pub fn set_line_width(&mut self, width: f32) {
92        self.ops.push(Op::SetLineWidth(width));
93    }
94
95    /// Sets the line cap style (`J`).
96    pub fn set_line_cap(&mut self, cap: LineCap) {
97        self.ops.push(Op::SetLineCap(cap as i32));
98    }
99
100    /// Sets the line join style (`j`).
101    pub fn set_line_join(&mut self, join: LineJoin) {
102        self.ops.push(Op::SetLineJoin(join as i32));
103    }
104
105    /// Sets the miter limit (`M`).
106    pub fn set_miter_limit(&mut self, limit: f32) {
107        self.ops.push(Op::SetMiterLimit(limit));
108    }
109
110    /// Sets the dash pattern (`d`).
111    pub fn set_dash(&mut self, pattern: &[f32], phase: f32) {
112        self.ops.push(Op::SetDash(pattern.to_vec(), phase));
113    }
114
115    /// Sets the fill color (`g`/`rg`/`k`).
116    pub fn set_fill(&mut self, color: Color) {
117        self.ops.push(color.fill_op());
118    }
119
120    /// Sets the stroke color (`G`/`RG`/`K`).
121    pub fn set_stroke(&mut self, color: Color) {
122        self.ops.push(color.stroke_op());
123    }
124
125    /// Begins a new subpath at `(x, y)` (`m`).
126    pub fn move_to(&mut self, x: f32, y: f32) {
127        self.ops.push(Op::MoveTo(x, y));
128    }
129
130    /// Straight segment to `(x, y)` (`l`).
131    pub fn line_to(&mut self, x: f32, y: f32) {
132        self.ops.push(Op::LineTo(x, y));
133    }
134
135    /// Cubic Bézier with two control points (`c`).
136    #[allow(clippy::too_many_arguments)] // six coordinates is the operator's arity
137    pub fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
138        self.ops.push(Op::CurveTo(x1, y1, x2, y2, x3, y3));
139    }
140
141    /// Closes the current subpath (`h`).
142    pub fn close(&mut self) {
143        self.ops.push(Op::ClosePath);
144    }
145
146    /// Appends a rectangle subpath (`re`).
147    pub fn rect(&mut self, x: f32, y: f32, width: f32, height: f32) {
148        self.ops.push(Op::Rect(x, y, width, height));
149    }
150
151    /// Appends a circle as four Bézier arcs, starting at the rightmost
152    /// point `(cx + r, cy)` and running counter-clockwise.
153    pub fn circle(&mut self, cx: f32, cy: f32, r: f32) {
154        self.ellipse(cx, cy, r, r);
155    }
156
157    /// Appends an axis-aligned ellipse as four Bézier arcs, starting at the
158    /// rightmost point `(cx + rx, cy)` and running counter-clockwise.
159    pub fn ellipse(&mut self, cx: f32, cy: f32, rx: f32, ry: f32) {
160        let ox = KAPPA * rx;
161        let oy = KAPPA * ry;
162        self.ops.push(Op::MoveTo(cx + rx, cy));
163        self.ops
164            .push(Op::CurveTo(cx + rx, cy + oy, cx + ox, cy + ry, cx, cy + ry));
165        self.ops
166            .push(Op::CurveTo(cx - ox, cy + ry, cx - rx, cy + oy, cx - rx, cy));
167        self.ops
168            .push(Op::CurveTo(cx - rx, cy - oy, cx - ox, cy - ry, cx, cy - ry));
169        self.ops
170            .push(Op::CurveTo(cx + ox, cy - ry, cx + rx, cy - oy, cx + rx, cy));
171        self.ops.push(Op::ClosePath);
172    }
173
174    /// Appends a closed polygon through `points`. Fewer than three points
175    /// appends nothing.
176    pub fn polygon(&mut self, points: &[Point]) {
177        if points.len() < 3 {
178            return;
179        }
180        self.ops.push(Op::MoveTo(points[0].x, points[0].y));
181        for point in &points[1..] {
182            self.ops.push(Op::LineTo(point.x, point.y));
183        }
184        self.ops.push(Op::ClosePath);
185    }
186
187    /// Fills the current path, nonzero winding (`f`).
188    pub fn fill(&mut self) {
189        self.ops.push(Op::Fill);
190    }
191
192    /// Fills the current path, even-odd (`f*`).
193    pub fn fill_even_odd(&mut self) {
194        self.ops.push(Op::FillEvenOdd);
195    }
196
197    /// Strokes the current path (`S`).
198    pub fn stroke(&mut self) {
199        self.ops.push(Op::Stroke);
200    }
201
202    /// Closes and strokes the current path (`s`).
203    pub fn close_stroke(&mut self) {
204        self.ops.push(Op::CloseStroke);
205    }
206
207    /// Fills then strokes the current path (`B`).
208    pub fn fill_stroke(&mut self) {
209        self.ops.push(Op::FillStroke);
210    }
211
212    /// Intersects the clip with the current path, nonzero (`W n`). The
213    /// path is consumed: it is ended without painting and a fresh path
214    /// starts afterwards.
215    pub fn clip(&mut self) {
216        self.ops.push(Op::ClipNonZero);
217        self.ops.push(Op::EndPath);
218    }
219
220    /// Intersects the clip with the current path, even-odd (`W* n`). The
221    /// path is consumed: it is ended without painting and a fresh path
222    /// starts afterwards.
223    pub fn clip_even_odd(&mut self) {
224        self.ops.push(Op::ClipEvenOdd);
225        self.ops.push(Op::EndPath);
226    }
227
228    /// Ends the current path without painting (`n`).
229    pub fn end_path(&mut self) {
230        self.ops.push(Op::EndPath);
231    }
232
233    /// Shows one line of text with its baseline origin at `(x, y)`
234    /// (`BT`/`Tf`/`Td`/`Tj`/`ET`). Errors on unencodable characters
235    /// before any operator is pushed, leaving the canvas untouched.
236    pub fn text(&mut self, text: &str, x: f32, y: f32, font: Standard14, size: f32) -> Result<()> {
237        let encoded = font.encode(text)?;
238        let index = match self.fonts.iter().position(|face| *face == font) {
239            Some(index) => index,
240            None => {
241                self.fonts.push(font);
242                self.fonts.len() - 1
243            }
244        };
245        self.ops.push(Op::BeginText);
246        self.ops
247            .push(Op::SetFont(Name(format!("F{}", index + 1)), size));
248        self.ops.push(Op::TextMove(x, y));
249        self.ops.push(Op::ShowText(encoded));
250        self.ops.push(Op::EndText);
251        Ok(())
252    }
253
254    /// Registers an image for use with [`draw_image`](Canvas::draw_image).
255    pub fn add_image(&mut self, image: ImageData) -> ImageHandle {
256        let handle = ImageHandle(self.images.len());
257        self.images.push(image);
258        handle
259    }
260
261    /// Paints a registered image into the axis-aligned box at `(x, y)` with
262    /// the given size (`q cm Do Q`).
263    pub fn draw_image(&mut self, image: ImageHandle, x: f32, y: f32, width: f32, height: f32) {
264        self.ops.push(Op::Save);
265        self.ops.push(Op::Concat(Matrix {
266            a: width,
267            b: 0.0,
268            c: 0.0,
269            d: height,
270            e: x,
271            f: y,
272        }));
273        self.ops
274            .push(Op::XObject(Name(format!("Im{}", image.0 + 1))));
275        self.ops.push(Op::Restore);
276    }
277
278    /// Pushes a raw operator — the escape hatch for anything the methods
279    /// above don't cover. Resource-referencing operators are the caller's
280    /// responsibility to keep consistent with the naming contract.
281    pub fn op(&mut self, op: Op) {
282        self.ops.push(op);
283    }
284
285    /// The operators accumulated so far, in paint order.
286    pub fn ops(&self) -> &[Op] {
287        &self.ops
288    }
289
290    /// Consumes the canvas into its parts for document assembly.
291    pub(crate) fn into_parts(self) -> CanvasParts {
292        CanvasParts {
293            ops: self.ops,
294            fonts: self.fonts,
295            images: self.images,
296        }
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use pdfboss_core::Name;
303
304    use super::*;
305    use crate::error::Error;
306
307    fn name(text: &str) -> Name {
308        Name(text.into())
309    }
310
311    #[test]
312    fn state_ops_push_single_operators() {
313        let mut canvas = Canvas::new();
314        canvas.save();
315        canvas.restore();
316        canvas.transform(Matrix {
317            a: 1.0,
318            b: 2.0,
319            c: 3.0,
320            d: 4.0,
321            e: 5.0,
322            f: 6.0,
323        });
324        canvas.set_line_width(2.5);
325        canvas.set_miter_limit(4.0);
326        canvas.set_dash(&[3.0, 1.0], 0.5);
327        assert_eq!(
328            canvas.ops(),
329            [
330                Op::Save,
331                Op::Restore,
332                Op::Concat(Matrix {
333                    a: 1.0,
334                    b: 2.0,
335                    c: 3.0,
336                    d: 4.0,
337                    e: 5.0,
338                    f: 6.0,
339                }),
340                Op::SetLineWidth(2.5),
341                Op::SetMiterLimit(4.0),
342                Op::SetDash(vec![3.0, 1.0], 0.5),
343            ]
344        );
345    }
346
347    #[test]
348    fn line_cap_and_join_map_declaration_order() {
349        let mut canvas = Canvas::new();
350        canvas.set_line_cap(LineCap::Butt);
351        canvas.set_line_cap(LineCap::Round);
352        canvas.set_line_cap(LineCap::Square);
353        canvas.set_line_join(LineJoin::Miter);
354        canvas.set_line_join(LineJoin::Round);
355        canvas.set_line_join(LineJoin::Bevel);
356        assert_eq!(
357            canvas.ops(),
358            [
359                Op::SetLineCap(0),
360                Op::SetLineCap(1),
361                Op::SetLineCap(2),
362                Op::SetLineJoin(0),
363                Op::SetLineJoin(1),
364                Op::SetLineJoin(2),
365            ]
366        );
367    }
368
369    #[test]
370    fn fill_and_stroke_colors() {
371        let mut canvas = Canvas::new();
372        canvas.set_fill(Color::Gray(0.5));
373        canvas.set_fill(Color::Rgb(0.1, 0.2, 0.3));
374        canvas.set_fill(Color::Cmyk(0.1, 0.2, 0.3, 0.4));
375        canvas.set_stroke(Color::Gray(0.5));
376        canvas.set_stroke(Color::Rgb(0.1, 0.2, 0.3));
377        canvas.set_stroke(Color::Cmyk(0.1, 0.2, 0.3, 0.4));
378        assert_eq!(
379            canvas.ops(),
380            [
381                Op::SetFillGray(0.5),
382                Op::SetFillRGB(0.1, 0.2, 0.3),
383                Op::SetFillCMYK(0.1, 0.2, 0.3, 0.4),
384                Op::SetStrokeGray(0.5),
385                Op::SetStrokeRGB(0.1, 0.2, 0.3),
386                Op::SetStrokeCMYK(0.1, 0.2, 0.3, 0.4),
387            ]
388        );
389    }
390
391    #[test]
392    fn path_construction_ops() {
393        let mut canvas = Canvas::new();
394        canvas.move_to(1.0, 2.0);
395        canvas.line_to(3.0, 4.0);
396        canvas.curve_to(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
397        canvas.close();
398        canvas.rect(10.0, 20.0, 30.0, 40.0);
399        assert_eq!(
400            canvas.ops(),
401            [
402                Op::MoveTo(1.0, 2.0),
403                Op::LineTo(3.0, 4.0),
404                Op::CurveTo(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
405                Op::ClosePath,
406                Op::Rect(10.0, 20.0, 30.0, 40.0),
407            ]
408        );
409    }
410
411    #[test]
412    fn circle_appends_four_arcs_and_close() {
413        let mut canvas = Canvas::new();
414        canvas.circle(0.0, 0.0, 1.0);
415        assert_eq!(
416            canvas.ops(),
417            [
418                Op::MoveTo(1.0, 0.0),
419                Op::CurveTo(1.0, KAPPA, KAPPA, 1.0, 0.0, 1.0),
420                Op::CurveTo(-KAPPA, 1.0, -1.0, KAPPA, -1.0, 0.0),
421                Op::CurveTo(-1.0, -KAPPA, -KAPPA, -1.0, 0.0, -1.0),
422                Op::CurveTo(KAPPA, -1.0, 1.0, -KAPPA, 1.0, 0.0),
423                Op::ClosePath,
424            ]
425        );
426    }
427
428    #[test]
429    fn ellipse_appends_four_arcs_and_close() {
430        let (cx, cy, rx, ry) = (10.0f32, 20.0f32, 4.0f32, 2.0f32);
431        let ox = KAPPA * rx;
432        let oy = KAPPA * ry;
433        let mut canvas = Canvas::new();
434        canvas.ellipse(cx, cy, rx, ry);
435        assert_eq!(
436            canvas.ops(),
437            [
438                Op::MoveTo(cx + rx, cy),
439                Op::CurveTo(cx + rx, cy + oy, cx + ox, cy + ry, cx, cy + ry),
440                Op::CurveTo(cx - ox, cy + ry, cx - rx, cy + oy, cx - rx, cy),
441                Op::CurveTo(cx - rx, cy - oy, cx - ox, cy - ry, cx, cy - ry),
442                Op::CurveTo(cx + ox, cy - ry, cx + rx, cy - oy, cx + rx, cy),
443                Op::ClosePath,
444            ]
445        );
446    }
447
448    #[test]
449    fn polygon_appends_closed_path() {
450        let mut canvas = Canvas::new();
451        canvas.polygon(&[
452            Point::new(0.0, 0.0),
453            Point::new(10.0, 0.0),
454            Point::new(5.0, 8.0),
455        ]);
456        assert_eq!(
457            canvas.ops(),
458            [
459                Op::MoveTo(0.0, 0.0),
460                Op::LineTo(10.0, 0.0),
461                Op::LineTo(5.0, 8.0),
462                Op::ClosePath,
463            ]
464        );
465    }
466
467    #[test]
468    fn polygon_under_three_points_is_no_op() {
469        let mut canvas = Canvas::new();
470        canvas.polygon(&[]);
471        canvas.polygon(&[Point::new(1.0, 2.0)]);
472        canvas.polygon(&[Point::new(1.0, 2.0), Point::new(3.0, 4.0)]);
473        assert_eq!(canvas.ops(), []);
474    }
475
476    #[test]
477    fn paint_verbs_push_single_operators() {
478        let mut canvas = Canvas::new();
479        canvas.fill();
480        canvas.fill_even_odd();
481        canvas.stroke();
482        canvas.close_stroke();
483        canvas.fill_stroke();
484        canvas.end_path();
485        assert_eq!(
486            canvas.ops(),
487            [
488                Op::Fill,
489                Op::FillEvenOdd,
490                Op::Stroke,
491                Op::CloseStroke,
492                Op::FillStroke,
493                Op::EndPath,
494            ]
495        );
496    }
497
498    #[test]
499    fn clip_pairs_consume_the_path() {
500        let mut canvas = Canvas::new();
501        canvas.clip();
502        canvas.clip_even_odd();
503        assert_eq!(
504            canvas.ops(),
505            [Op::ClipNonZero, Op::EndPath, Op::ClipEvenOdd, Op::EndPath,]
506        );
507    }
508
509    #[test]
510    fn text_pushes_five_op_sequence() {
511        let mut canvas = Canvas::new();
512        canvas
513            .text("Hi", 72.0, 720.0, Standard14::Helvetica, 12.0)
514            .unwrap();
515        assert_eq!(
516            canvas.ops(),
517            [
518                Op::BeginText,
519                Op::SetFont(name("F1"), 12.0),
520                Op::TextMove(72.0, 720.0),
521                Op::ShowText(b"Hi".to_vec()),
522                Op::EndText,
523            ]
524        );
525        let parts = canvas.into_parts();
526        assert_eq!(parts.fonts, [Standard14::Helvetica]);
527    }
528
529    #[test]
530    fn texts_in_the_same_font_share_f1() {
531        let mut canvas = Canvas::new();
532        canvas
533            .text("one", 0.0, 0.0, Standard14::TimesRoman, 10.0)
534            .unwrap();
535        canvas
536            .text("two", 0.0, 20.0, Standard14::TimesRoman, 10.0)
537            .unwrap();
538        let font_names: Vec<&Op> = canvas
539            .ops()
540            .iter()
541            .filter(|op| matches!(op, Op::SetFont(..)))
542            .collect();
543        assert_eq!(
544            font_names,
545            [
546                &Op::SetFont(name("F1"), 10.0),
547                &Op::SetFont(name("F1"), 10.0),
548            ]
549        );
550        assert_eq!(canvas.into_parts().fonts, [Standard14::TimesRoman]);
551    }
552
553    #[test]
554    fn second_face_gets_f2() {
555        let mut canvas = Canvas::new();
556        canvas
557            .text("one", 0.0, 0.0, Standard14::Helvetica, 10.0)
558            .unwrap();
559        canvas
560            .text("two", 0.0, 20.0, Standard14::CourierBold, 10.0)
561            .unwrap();
562        canvas
563            .text("three", 0.0, 40.0, Standard14::Helvetica, 10.0)
564            .unwrap();
565        let font_names: Vec<&Op> = canvas
566            .ops()
567            .iter()
568            .filter(|op| matches!(op, Op::SetFont(..)))
569            .collect();
570        assert_eq!(
571            font_names,
572            [
573                &Op::SetFont(name("F1"), 10.0),
574                &Op::SetFont(name("F2"), 10.0),
575                &Op::SetFont(name("F1"), 10.0),
576            ]
577        );
578        assert_eq!(
579            canvas.into_parts().fonts,
580            [Standard14::Helvetica, Standard14::CourierBold]
581        );
582    }
583
584    #[test]
585    fn text_error_leaves_canvas_untouched() {
586        let mut canvas = Canvas::new();
587        canvas.save();
588        let result = canvas.text("\u{2318}", 0.0, 0.0, Standard14::Helvetica, 12.0);
589        assert!(matches!(
590            result,
591            Err(Error::Unencodable { ch: '\u{2318}', .. })
592        ));
593        assert_eq!(canvas.ops(), [Op::Save]);
594        assert!(canvas.into_parts().fonts.is_empty());
595    }
596
597    #[test]
598    fn draw_image_emits_save_concat_xobject_restore() {
599        let mut canvas = Canvas::new();
600        let first = canvas.add_image(ImageData::gray8(1, 1, vec![0]).unwrap());
601        let second = canvas.add_image(ImageData::gray8(1, 1, vec![255]).unwrap());
602        canvas.draw_image(first, 10.0, 20.0, 100.0, 50.0);
603        canvas.draw_image(second, 0.0, 0.0, 5.0, 5.0);
604        assert_eq!(
605            canvas.ops(),
606            [
607                Op::Save,
608                Op::Concat(Matrix {
609                    a: 100.0,
610                    b: 0.0,
611                    c: 0.0,
612                    d: 50.0,
613                    e: 10.0,
614                    f: 20.0,
615                }),
616                Op::XObject(name("Im1")),
617                Op::Restore,
618                Op::Save,
619                Op::Concat(Matrix {
620                    a: 5.0,
621                    b: 0.0,
622                    c: 0.0,
623                    d: 5.0,
624                    e: 0.0,
625                    f: 0.0,
626                }),
627                Op::XObject(name("Im2")),
628                Op::Restore,
629            ]
630        );
631        assert_eq!(canvas.into_parts().images.len(), 2);
632    }
633}