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; groups registered with
9//! [`group`](Canvas::group) map to `Gp1`, `Gp2`, …; distinct `/ExtGState`
10//! entries pushed by the alpha/blend-mode setters map to `Gs1`, `Gs2`, ….
11//! Assembly builds the matching `/Resources` dictionary from
12//! [`CanvasParts`].
13
14use pdfboss_core::content::Op;
15use pdfboss_core::{Dict, Matrix, Name, Object, Point};
16
17use crate::color::Color;
18use crate::error::Result;
19use crate::font::Standard14;
20use crate::image::ImageData;
21
22#[allow(clippy::excessive_precision)] // the contract names this exact literal
23const KAPPA: f32 = 0.552284749831;
24
25/// Line cap style (ISO 32000 §8.4.3.3).
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum LineCap {
28    /// Squared off at the endpoint.
29    #[default]
30    Butt,
31    /// Semicircle around the endpoint.
32    Round,
33    /// Square projecting half a width beyond the endpoint.
34    Square,
35}
36
37/// Line join style (ISO 32000 §8.4.3.4).
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum LineJoin {
40    /// Outer edges extended to a point.
41    #[default]
42    Miter,
43    /// Circular arc around the corner.
44    Round,
45    /// Cut off with a straight edge.
46    Bevel,
47}
48
49/// Handle to an image registered on a canvas.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct ImageHandle(pub(crate) usize);
52
53/// Handle to a sub-canvas registered with [`Canvas::group`], painted with
54/// [`Canvas::draw_group`].
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct GroupHandle(pub(crate) usize);
57
58/// A separable blend mode (ISO 32000 §11.3.5, Table 136), set with
59/// [`Canvas::set_blend_mode`].
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum BlendMode {
62    /// Source color replaces the backdrop.
63    Normal,
64    /// Product of source and backdrop.
65    Multiply,
66    /// Complement of the product of the complements.
67    Screen,
68    /// Multiply or screen, chosen by the backdrop.
69    Overlay,
70    /// The darker of source and backdrop, per component.
71    Darken,
72    /// The lighter of source and backdrop, per component.
73    Lighten,
74    /// Brightens the backdrop toward the source.
75    ColorDodge,
76    /// Darkens the backdrop toward the source.
77    ColorBurn,
78    /// Multiply or screen, chosen by the source (Overlay with roles swapped).
79    HardLight,
80    /// A softer variant of `HardLight`.
81    SoftLight,
82    /// Absolute difference of source and backdrop.
83    Difference,
84    /// Like `Difference`, with lower contrast.
85    Exclusion,
86}
87
88impl BlendMode {
89    /// The `/BM` name for this mode.
90    fn pdf_name(self) -> &'static str {
91        match self {
92            BlendMode::Normal => "Normal",
93            BlendMode::Multiply => "Multiply",
94            BlendMode::Screen => "Screen",
95            BlendMode::Overlay => "Overlay",
96            BlendMode::Darken => "Darken",
97            BlendMode::Lighten => "Lighten",
98            BlendMode::ColorDodge => "ColorDodge",
99            BlendMode::ColorBurn => "ColorBurn",
100            BlendMode::HardLight => "HardLight",
101            BlendMode::SoftLight => "SoftLight",
102            BlendMode::Difference => "Difference",
103            BlendMode::Exclusion => "Exclusion",
104        }
105    }
106}
107
108/// One deduped `/ExtGState` entry. Each [`Canvas`] setter writes only its
109/// own key, so at most one field is ever set — a `gs` referencing `/ca`
110/// alone must leave `/CA` and `/BM` untouched (ISO 32000 §8.4.5).
111#[derive(Debug, Clone, Copy, PartialEq, Default)]
112pub(crate) struct GState {
113    pub(crate) fill_alpha: Option<f32>,
114    pub(crate) stroke_alpha: Option<f32>,
115    pub(crate) blend_mode: Option<BlendMode>,
116}
117
118impl GState {
119    /// The `/ExtGState` dictionary for this entry.
120    pub(crate) fn ext_gstate_dict(self) -> Dict {
121        let mut dict = Dict::new();
122        if let Some(alpha) = self.fill_alpha {
123            dict.insert(name("ca"), Object::Real(f64::from(alpha)));
124        }
125        if let Some(alpha) = self.stroke_alpha {
126            dict.insert(name("CA"), Object::Real(f64::from(alpha)));
127        }
128        if let Some(mode) = self.blend_mode {
129            dict.insert(name("BM"), Object::Name(name(mode.pdf_name())));
130        }
131        dict
132    }
133}
134
135/// A `Name` from a string literal.
136fn name(text: &str) -> Name {
137    Name(text.to_string())
138}
139
140/// The accumulated output of a finished canvas.
141#[derive(Debug)]
142pub struct CanvasParts {
143    /// Operators, in paint order.
144    pub ops: Vec<Op>,
145    /// Distinct fonts in first-use order; index `i` is resource `F{i+1}`.
146    pub fonts: Vec<Standard14>,
147    /// Images in registration order; index `i` is resource `Im{i+1}`.
148    pub images: Vec<ImageData>,
149    /// Sub-canvases registered with [`Canvas::group`], paired with their
150    /// `/BBox`; index `i` is resource `Gp{i+1}`.
151    pub(crate) groups: Vec<(CanvasParts, [f32; 4])>,
152    /// Distinct `/ExtGState` entries in first-use order; index `i` is
153    /// resource `Gs{i+1}`.
154    pub(crate) gstates: Vec<GState>,
155}
156
157/// An imperative painter producing content-stream operators.
158#[derive(Debug, Default)]
159pub struct Canvas {
160    ops: Vec<Op>,
161    fonts: Vec<Standard14>,
162    images: Vec<ImageData>,
163    groups: Vec<(CanvasParts, [f32; 4])>,
164    gstates: Vec<GState>,
165}
166
167impl Canvas {
168    /// Creates an empty canvas.
169    pub fn new() -> Canvas {
170        Canvas::default()
171    }
172
173    /// Pushes the graphics state (`q`).
174    pub fn save(&mut self) {
175        self.ops.push(Op::Save);
176    }
177
178    /// Pops the graphics state (`Q`).
179    pub fn restore(&mut self) {
180        self.ops.push(Op::Restore);
181    }
182
183    /// Concatenates `m` onto the current transformation matrix (`cm`).
184    pub fn transform(&mut self, m: Matrix) {
185        self.ops.push(Op::Concat(m));
186    }
187
188    /// Sets the stroke line width (`w`).
189    pub fn set_line_width(&mut self, width: f32) {
190        self.ops.push(Op::SetLineWidth(width));
191    }
192
193    /// Sets the line cap style (`J`).
194    pub fn set_line_cap(&mut self, cap: LineCap) {
195        self.ops.push(Op::SetLineCap(cap as i32));
196    }
197
198    /// Sets the line join style (`j`).
199    pub fn set_line_join(&mut self, join: LineJoin) {
200        self.ops.push(Op::SetLineJoin(join as i32));
201    }
202
203    /// Sets the miter limit (`M`).
204    pub fn set_miter_limit(&mut self, limit: f32) {
205        self.ops.push(Op::SetMiterLimit(limit));
206    }
207
208    /// Sets the dash pattern (`d`).
209    pub fn set_dash(&mut self, pattern: &[f32], phase: f32) {
210        self.ops.push(Op::SetDash(pattern.to_vec(), phase));
211    }
212
213    /// Sets the fill color (`g`/`rg`/`k`).
214    pub fn set_fill(&mut self, color: Color) {
215        self.ops.push(color.fill_op());
216    }
217
218    /// Sets the stroke color (`G`/`RG`/`K`).
219    pub fn set_stroke(&mut self, color: Color) {
220        self.ops.push(color.stroke_op());
221    }
222
223    /// Begins a new subpath at `(x, y)` (`m`).
224    pub fn move_to(&mut self, x: f32, y: f32) {
225        self.ops.push(Op::MoveTo(x, y));
226    }
227
228    /// Straight segment to `(x, y)` (`l`).
229    pub fn line_to(&mut self, x: f32, y: f32) {
230        self.ops.push(Op::LineTo(x, y));
231    }
232
233    /// Cubic Bézier with two control points (`c`).
234    #[allow(clippy::too_many_arguments)] // six coordinates is the operator's arity
235    pub fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
236        self.ops.push(Op::CurveTo(x1, y1, x2, y2, x3, y3));
237    }
238
239    /// Closes the current subpath (`h`).
240    pub fn close(&mut self) {
241        self.ops.push(Op::ClosePath);
242    }
243
244    /// Appends a rectangle subpath (`re`).
245    pub fn rect(&mut self, x: f32, y: f32, width: f32, height: f32) {
246        self.ops.push(Op::Rect(x, y, width, height));
247    }
248
249    /// Appends a circle as four Bézier arcs, starting at the rightmost
250    /// point `(cx + r, cy)` and running counter-clockwise.
251    pub fn circle(&mut self, cx: f32, cy: f32, r: f32) {
252        self.ellipse(cx, cy, r, r);
253    }
254
255    /// Appends an axis-aligned ellipse as four Bézier arcs, starting at the
256    /// rightmost point `(cx + rx, cy)` and running counter-clockwise.
257    pub fn ellipse(&mut self, cx: f32, cy: f32, rx: f32, ry: f32) {
258        let ox = KAPPA * rx;
259        let oy = KAPPA * ry;
260        self.ops.push(Op::MoveTo(cx + rx, cy));
261        self.ops
262            .push(Op::CurveTo(cx + rx, cy + oy, cx + ox, cy + ry, cx, cy + ry));
263        self.ops
264            .push(Op::CurveTo(cx - ox, cy + ry, cx - rx, cy + oy, cx - rx, cy));
265        self.ops
266            .push(Op::CurveTo(cx - rx, cy - oy, cx - ox, cy - ry, cx, cy - ry));
267        self.ops
268            .push(Op::CurveTo(cx + ox, cy - ry, cx + rx, cy - oy, cx + rx, cy));
269        self.ops.push(Op::ClosePath);
270    }
271
272    /// Appends a closed polygon through `points`. Fewer than three points
273    /// appends nothing.
274    pub fn polygon(&mut self, points: &[Point]) {
275        if points.len() < 3 {
276            return;
277        }
278        self.ops.push(Op::MoveTo(points[0].x, points[0].y));
279        for point in &points[1..] {
280            self.ops.push(Op::LineTo(point.x, point.y));
281        }
282        self.ops.push(Op::ClosePath);
283    }
284
285    /// Fills the current path, nonzero winding (`f`).
286    pub fn fill(&mut self) {
287        self.ops.push(Op::Fill);
288    }
289
290    /// Fills the current path, even-odd (`f*`).
291    pub fn fill_even_odd(&mut self) {
292        self.ops.push(Op::FillEvenOdd);
293    }
294
295    /// Strokes the current path (`S`).
296    pub fn stroke(&mut self) {
297        self.ops.push(Op::Stroke);
298    }
299
300    /// Closes and strokes the current path (`s`).
301    pub fn close_stroke(&mut self) {
302        self.ops.push(Op::CloseStroke);
303    }
304
305    /// Fills then strokes the current path (`B`).
306    pub fn fill_stroke(&mut self) {
307        self.ops.push(Op::FillStroke);
308    }
309
310    /// Intersects the clip with the current path, nonzero (`W n`). The
311    /// path is consumed: it is ended without painting and a fresh path
312    /// starts afterwards.
313    pub fn clip(&mut self) {
314        self.ops.push(Op::ClipNonZero);
315        self.ops.push(Op::EndPath);
316    }
317
318    /// Intersects the clip with the current path, even-odd (`W* n`). The
319    /// path is consumed: it is ended without painting and a fresh path
320    /// starts afterwards.
321    pub fn clip_even_odd(&mut self) {
322        self.ops.push(Op::ClipEvenOdd);
323        self.ops.push(Op::EndPath);
324    }
325
326    /// Ends the current path without painting (`n`).
327    pub fn end_path(&mut self) {
328        self.ops.push(Op::EndPath);
329    }
330
331    /// Shows one line of text with its baseline origin at `(x, y)`
332    /// (`BT`/`Tf`/`Td`/`Tj`/`ET`). Errors on unencodable characters
333    /// before any operator is pushed, leaving the canvas untouched.
334    pub fn text(&mut self, text: &str, x: f32, y: f32, font: Standard14, size: f32) -> Result<()> {
335        let encoded = font.encode(text)?;
336        let index = match self.fonts.iter().position(|face| *face == font) {
337            Some(index) => index,
338            None => {
339                self.fonts.push(font);
340                self.fonts.len() - 1
341            }
342        };
343        self.ops.push(Op::BeginText);
344        self.ops
345            .push(Op::SetFont(Name(format!("F{}", index + 1)), size));
346        self.ops.push(Op::TextMove(x, y));
347        self.ops.push(Op::ShowText(encoded));
348        self.ops.push(Op::EndText);
349        Ok(())
350    }
351
352    /// Registers an image for use with [`draw_image`](Canvas::draw_image).
353    pub fn add_image(&mut self, image: ImageData) -> ImageHandle {
354        let handle = ImageHandle(self.images.len());
355        self.images.push(image);
356        handle
357    }
358
359    /// Paints a registered image into the axis-aligned box at `(x, y)` with
360    /// the given size (`q cm Do Q`).
361    pub fn draw_image(&mut self, image: ImageHandle, x: f32, y: f32, width: f32, height: f32) {
362        self.ops.push(Op::Save);
363        self.ops.push(Op::Concat(Matrix {
364            a: width,
365            b: 0.0,
366            c: 0.0,
367            d: height,
368            e: x,
369            f: y,
370        }));
371        self.ops
372            .push(Op::XObject(Name(format!("Im{}", image.0 + 1))));
373        self.ops.push(Op::Restore);
374    }
375
376    /// Registers a finished sub-canvas as a form group, with `bbox`
377    /// (`[llx, lly, urx, ury]`) becoming the form's `/BBox`. Paint it with
378    /// [`draw_group`](Canvas::draw_group).
379    pub fn group(&mut self, canvas: Canvas, bbox: [f32; 4]) -> GroupHandle {
380        let handle = GroupHandle(self.groups.len());
381        self.groups.push((canvas.into_parts(), bbox));
382        handle
383    }
384
385    /// Paints a registered group under `matrix` (`q cm /GpN Do Q`). Two
386    /// calls with the same `group` reference the same form resource.
387    pub fn draw_group(&mut self, group: GroupHandle, matrix: Matrix) {
388        self.ops.push(Op::Save);
389        self.ops.push(Op::Concat(matrix));
390        self.ops
391            .push(Op::XObject(Name(format!("Gp{}", group.0 + 1))));
392        self.ops.push(Op::Restore);
393    }
394
395    /// Sets the nonstroking alpha constant (`gs` referencing `/ca`).
396    pub fn set_fill_alpha(&mut self, alpha: f32) {
397        self.push_gstate(GState {
398            fill_alpha: Some(alpha),
399            ..GState::default()
400        });
401    }
402
403    /// Sets the stroking alpha constant (`gs` referencing `/CA`).
404    pub fn set_stroke_alpha(&mut self, alpha: f32) {
405        self.push_gstate(GState {
406            stroke_alpha: Some(alpha),
407            ..GState::default()
408        });
409    }
410
411    /// Sets the blend mode (`gs` referencing `/BM`).
412    pub fn set_blend_mode(&mut self, mode: BlendMode) {
413        self.push_gstate(GState {
414            blend_mode: Some(mode),
415            ..GState::default()
416        });
417    }
418
419    /// Emits a `gs` referencing `state`'s resource, reusing an identical
420    /// prior entry instead of adding a duplicate.
421    fn push_gstate(&mut self, state: GState) {
422        let index = match self.gstates.iter().position(|seen| *seen == state) {
423            Some(index) => index,
424            None => {
425                self.gstates.push(state);
426                self.gstates.len() - 1
427            }
428        };
429        self.ops
430            .push(Op::SetExtGState(Name(format!("Gs{}", index + 1))));
431    }
432
433    /// Pushes a raw operator — the escape hatch for anything the methods
434    /// above don't cover. Resource-referencing operators are the caller's
435    /// responsibility to keep consistent with the naming contract.
436    pub fn op(&mut self, op: Op) {
437        self.ops.push(op);
438    }
439
440    /// The operators accumulated so far, in paint order.
441    pub fn ops(&self) -> &[Op] {
442        &self.ops
443    }
444
445    /// Consumes the canvas into its parts for document assembly.
446    pub(crate) fn into_parts(self) -> CanvasParts {
447        CanvasParts {
448            ops: self.ops,
449            fonts: self.fonts,
450            images: self.images,
451            groups: self.groups,
452            gstates: self.gstates,
453        }
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use pdfboss_core::content::parse_content;
460    use pdfboss_core::Name;
461
462    use super::*;
463    use crate::content::serialize_ops;
464    use crate::error::Error;
465
466    fn name(text: &str) -> Name {
467        Name(text.into())
468    }
469
470    #[test]
471    fn state_ops_push_single_operators() {
472        let mut canvas = Canvas::new();
473        canvas.save();
474        canvas.restore();
475        canvas.transform(Matrix {
476            a: 1.0,
477            b: 2.0,
478            c: 3.0,
479            d: 4.0,
480            e: 5.0,
481            f: 6.0,
482        });
483        canvas.set_line_width(2.5);
484        canvas.set_miter_limit(4.0);
485        canvas.set_dash(&[3.0, 1.0], 0.5);
486        assert_eq!(
487            canvas.ops(),
488            [
489                Op::Save,
490                Op::Restore,
491                Op::Concat(Matrix {
492                    a: 1.0,
493                    b: 2.0,
494                    c: 3.0,
495                    d: 4.0,
496                    e: 5.0,
497                    f: 6.0,
498                }),
499                Op::SetLineWidth(2.5),
500                Op::SetMiterLimit(4.0),
501                Op::SetDash(vec![3.0, 1.0], 0.5),
502            ]
503        );
504    }
505
506    #[test]
507    fn line_cap_and_join_map_declaration_order() {
508        let mut canvas = Canvas::new();
509        canvas.set_line_cap(LineCap::Butt);
510        canvas.set_line_cap(LineCap::Round);
511        canvas.set_line_cap(LineCap::Square);
512        canvas.set_line_join(LineJoin::Miter);
513        canvas.set_line_join(LineJoin::Round);
514        canvas.set_line_join(LineJoin::Bevel);
515        assert_eq!(
516            canvas.ops(),
517            [
518                Op::SetLineCap(0),
519                Op::SetLineCap(1),
520                Op::SetLineCap(2),
521                Op::SetLineJoin(0),
522                Op::SetLineJoin(1),
523                Op::SetLineJoin(2),
524            ]
525        );
526    }
527
528    #[test]
529    fn fill_and_stroke_colors() {
530        let mut canvas = Canvas::new();
531        canvas.set_fill(Color::Gray(0.5));
532        canvas.set_fill(Color::Rgb(0.1, 0.2, 0.3));
533        canvas.set_fill(Color::Cmyk(0.1, 0.2, 0.3, 0.4));
534        canvas.set_stroke(Color::Gray(0.5));
535        canvas.set_stroke(Color::Rgb(0.1, 0.2, 0.3));
536        canvas.set_stroke(Color::Cmyk(0.1, 0.2, 0.3, 0.4));
537        assert_eq!(
538            canvas.ops(),
539            [
540                Op::SetFillGray(0.5),
541                Op::SetFillRGB(0.1, 0.2, 0.3),
542                Op::SetFillCMYK(0.1, 0.2, 0.3, 0.4),
543                Op::SetStrokeGray(0.5),
544                Op::SetStrokeRGB(0.1, 0.2, 0.3),
545                Op::SetStrokeCMYK(0.1, 0.2, 0.3, 0.4),
546            ]
547        );
548    }
549
550    #[test]
551    fn path_construction_ops() {
552        let mut canvas = Canvas::new();
553        canvas.move_to(1.0, 2.0);
554        canvas.line_to(3.0, 4.0);
555        canvas.curve_to(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
556        canvas.close();
557        canvas.rect(10.0, 20.0, 30.0, 40.0);
558        assert_eq!(
559            canvas.ops(),
560            [
561                Op::MoveTo(1.0, 2.0),
562                Op::LineTo(3.0, 4.0),
563                Op::CurveTo(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
564                Op::ClosePath,
565                Op::Rect(10.0, 20.0, 30.0, 40.0),
566            ]
567        );
568    }
569
570    #[test]
571    fn circle_appends_four_arcs_and_close() {
572        let mut canvas = Canvas::new();
573        canvas.circle(0.0, 0.0, 1.0);
574        assert_eq!(
575            canvas.ops(),
576            [
577                Op::MoveTo(1.0, 0.0),
578                Op::CurveTo(1.0, KAPPA, KAPPA, 1.0, 0.0, 1.0),
579                Op::CurveTo(-KAPPA, 1.0, -1.0, KAPPA, -1.0, 0.0),
580                Op::CurveTo(-1.0, -KAPPA, -KAPPA, -1.0, 0.0, -1.0),
581                Op::CurveTo(KAPPA, -1.0, 1.0, -KAPPA, 1.0, 0.0),
582                Op::ClosePath,
583            ]
584        );
585    }
586
587    #[test]
588    fn ellipse_appends_four_arcs_and_close() {
589        let (cx, cy, rx, ry) = (10.0f32, 20.0f32, 4.0f32, 2.0f32);
590        let ox = KAPPA * rx;
591        let oy = KAPPA * ry;
592        let mut canvas = Canvas::new();
593        canvas.ellipse(cx, cy, rx, ry);
594        assert_eq!(
595            canvas.ops(),
596            [
597                Op::MoveTo(cx + rx, cy),
598                Op::CurveTo(cx + rx, cy + oy, cx + ox, cy + ry, cx, cy + ry),
599                Op::CurveTo(cx - ox, cy + ry, cx - rx, cy + oy, cx - rx, cy),
600                Op::CurveTo(cx - rx, cy - oy, cx - ox, cy - ry, cx, cy - ry),
601                Op::CurveTo(cx + ox, cy - ry, cx + rx, cy - oy, cx + rx, cy),
602                Op::ClosePath,
603            ]
604        );
605    }
606
607    #[test]
608    fn polygon_appends_closed_path() {
609        let mut canvas = Canvas::new();
610        canvas.polygon(&[
611            Point::new(0.0, 0.0),
612            Point::new(10.0, 0.0),
613            Point::new(5.0, 8.0),
614        ]);
615        assert_eq!(
616            canvas.ops(),
617            [
618                Op::MoveTo(0.0, 0.0),
619                Op::LineTo(10.0, 0.0),
620                Op::LineTo(5.0, 8.0),
621                Op::ClosePath,
622            ]
623        );
624    }
625
626    #[test]
627    fn polygon_under_three_points_is_no_op() {
628        let mut canvas = Canvas::new();
629        canvas.polygon(&[]);
630        canvas.polygon(&[Point::new(1.0, 2.0)]);
631        canvas.polygon(&[Point::new(1.0, 2.0), Point::new(3.0, 4.0)]);
632        assert_eq!(canvas.ops(), []);
633    }
634
635    #[test]
636    fn paint_verbs_push_single_operators() {
637        let mut canvas = Canvas::new();
638        canvas.fill();
639        canvas.fill_even_odd();
640        canvas.stroke();
641        canvas.close_stroke();
642        canvas.fill_stroke();
643        canvas.end_path();
644        assert_eq!(
645            canvas.ops(),
646            [
647                Op::Fill,
648                Op::FillEvenOdd,
649                Op::Stroke,
650                Op::CloseStroke,
651                Op::FillStroke,
652                Op::EndPath,
653            ]
654        );
655    }
656
657    #[test]
658    fn clip_pairs_consume_the_path() {
659        let mut canvas = Canvas::new();
660        canvas.clip();
661        canvas.clip_even_odd();
662        assert_eq!(
663            canvas.ops(),
664            [Op::ClipNonZero, Op::EndPath, Op::ClipEvenOdd, Op::EndPath,]
665        );
666    }
667
668    #[test]
669    fn text_pushes_five_op_sequence() {
670        let mut canvas = Canvas::new();
671        canvas
672            .text("Hi", 72.0, 720.0, Standard14::Helvetica, 12.0)
673            .unwrap();
674        assert_eq!(
675            canvas.ops(),
676            [
677                Op::BeginText,
678                Op::SetFont(name("F1"), 12.0),
679                Op::TextMove(72.0, 720.0),
680                Op::ShowText(b"Hi".to_vec()),
681                Op::EndText,
682            ]
683        );
684        let parts = canvas.into_parts();
685        assert_eq!(parts.fonts, [Standard14::Helvetica]);
686    }
687
688    #[test]
689    fn texts_in_the_same_font_share_f1() {
690        let mut canvas = Canvas::new();
691        canvas
692            .text("one", 0.0, 0.0, Standard14::TimesRoman, 10.0)
693            .unwrap();
694        canvas
695            .text("two", 0.0, 20.0, Standard14::TimesRoman, 10.0)
696            .unwrap();
697        let font_names: Vec<&Op> = canvas
698            .ops()
699            .iter()
700            .filter(|op| matches!(op, Op::SetFont(..)))
701            .collect();
702        assert_eq!(
703            font_names,
704            [
705                &Op::SetFont(name("F1"), 10.0),
706                &Op::SetFont(name("F1"), 10.0),
707            ]
708        );
709        assert_eq!(canvas.into_parts().fonts, [Standard14::TimesRoman]);
710    }
711
712    #[test]
713    fn second_face_gets_f2() {
714        let mut canvas = Canvas::new();
715        canvas
716            .text("one", 0.0, 0.0, Standard14::Helvetica, 10.0)
717            .unwrap();
718        canvas
719            .text("two", 0.0, 20.0, Standard14::CourierBold, 10.0)
720            .unwrap();
721        canvas
722            .text("three", 0.0, 40.0, Standard14::Helvetica, 10.0)
723            .unwrap();
724        let font_names: Vec<&Op> = canvas
725            .ops()
726            .iter()
727            .filter(|op| matches!(op, Op::SetFont(..)))
728            .collect();
729        assert_eq!(
730            font_names,
731            [
732                &Op::SetFont(name("F1"), 10.0),
733                &Op::SetFont(name("F2"), 10.0),
734                &Op::SetFont(name("F1"), 10.0),
735            ]
736        );
737        assert_eq!(
738            canvas.into_parts().fonts,
739            [Standard14::Helvetica, Standard14::CourierBold]
740        );
741    }
742
743    #[test]
744    fn text_error_leaves_canvas_untouched() {
745        let mut canvas = Canvas::new();
746        canvas.save();
747        let result = canvas.text("\u{2318}", 0.0, 0.0, Standard14::Helvetica, 12.0);
748        assert!(matches!(
749            result,
750            Err(Error::Unencodable { ch: '\u{2318}', .. })
751        ));
752        assert_eq!(canvas.ops(), [Op::Save]);
753        assert!(canvas.into_parts().fonts.is_empty());
754    }
755
756    #[test]
757    fn draw_image_emits_save_concat_xobject_restore() {
758        let mut canvas = Canvas::new();
759        let first = canvas.add_image(ImageData::gray8(1, 1, vec![0]).unwrap());
760        let second = canvas.add_image(ImageData::gray8(1, 1, vec![255]).unwrap());
761        canvas.draw_image(first, 10.0, 20.0, 100.0, 50.0);
762        canvas.draw_image(second, 0.0, 0.0, 5.0, 5.0);
763        assert_eq!(
764            canvas.ops(),
765            [
766                Op::Save,
767                Op::Concat(Matrix {
768                    a: 100.0,
769                    b: 0.0,
770                    c: 0.0,
771                    d: 50.0,
772                    e: 10.0,
773                    f: 20.0,
774                }),
775                Op::XObject(name("Im1")),
776                Op::Restore,
777                Op::Save,
778                Op::Concat(Matrix {
779                    a: 5.0,
780                    b: 0.0,
781                    c: 0.0,
782                    d: 5.0,
783                    e: 0.0,
784                    f: 0.0,
785                }),
786                Op::XObject(name("Im2")),
787                Op::Restore,
788            ]
789        );
790        assert_eq!(canvas.into_parts().images.len(), 2);
791    }
792
793    #[test]
794    fn group_registers_subcanvas_parts_and_bbox() {
795        let mut sub = Canvas::new();
796        sub.rect(1.0, 2.0, 3.0, 4.0);
797        sub.fill();
798        let mut canvas = Canvas::new();
799        let handle = canvas.group(sub, [0.0, 0.0, 50.0, 20.0]);
800        assert_eq!(handle, GroupHandle(0));
801        let parts = canvas.into_parts();
802        assert_eq!(parts.groups.len(), 1);
803        let (group_parts, bbox) = &parts.groups[0];
804        assert_eq!(*bbox, [0.0, 0.0, 50.0, 20.0]);
805        assert_eq!(group_parts.ops, [Op::Rect(1.0, 2.0, 3.0, 4.0), Op::Fill]);
806    }
807
808    #[test]
809    fn second_group_gets_index_one() {
810        let mut canvas = Canvas::new();
811        let first = canvas.group(Canvas::new(), [0.0, 0.0, 1.0, 1.0]);
812        let second = canvas.group(Canvas::new(), [0.0, 0.0, 1.0, 1.0]);
813        assert_eq!(first, GroupHandle(0));
814        assert_eq!(second, GroupHandle(1));
815    }
816
817    #[test]
818    fn draw_group_emits_save_cm_xobject_restore_and_round_trips() {
819        let mut canvas = Canvas::new();
820        let handle = canvas.group(Canvas::new(), [0.0, 0.0, 10.0, 10.0]);
821        let matrix = Matrix {
822            a: 1.0,
823            b: 0.0,
824            c: 0.0,
825            d: 1.0,
826            e: 72.0,
827            f: 144.0,
828        };
829        canvas.draw_group(handle, matrix);
830        assert_eq!(
831            canvas.ops(),
832            [
833                Op::Save,
834                Op::Concat(matrix),
835                Op::XObject(name("Gp1")),
836                Op::Restore,
837            ]
838        );
839        let bytes = serialize_ops(canvas.ops());
840        let parsed = parse_content(&bytes).unwrap();
841        assert_eq!(parsed, canvas.ops());
842    }
843
844    #[test]
845    fn two_draws_of_one_group_reference_the_same_resource_name() {
846        let mut canvas = Canvas::new();
847        let handle = canvas.group(Canvas::new(), [0.0, 0.0, 10.0, 10.0]);
848        canvas.draw_group(handle, Matrix::identity());
849        canvas.draw_group(handle, Matrix::translate(5.0, 5.0));
850        let names: Vec<&Op> = canvas
851            .ops()
852            .iter()
853            .filter(|op| matches!(op, Op::XObject(_)))
854            .collect();
855        assert_eq!(
856            names,
857            [&Op::XObject(name("Gp1")), &Op::XObject(name("Gp1"))]
858        );
859    }
860
861    #[test]
862    fn second_registered_group_gets_gp2() {
863        let mut canvas = Canvas::new();
864        let first = canvas.group(Canvas::new(), [0.0, 0.0, 1.0, 1.0]);
865        let second = canvas.group(Canvas::new(), [0.0, 0.0, 1.0, 1.0]);
866        canvas.draw_group(first, Matrix::identity());
867        canvas.draw_group(second, Matrix::identity());
868        let names: Vec<&Op> = canvas
869            .ops()
870            .iter()
871            .filter(|op| matches!(op, Op::XObject(_)))
872            .collect();
873        assert_eq!(
874            names,
875            [&Op::XObject(name("Gp1")), &Op::XObject(name("Gp2"))]
876        );
877    }
878
879    #[test]
880    fn nested_groups_recurse() {
881        let mut innermost = Canvas::new();
882        innermost.fill();
883        let mut outer = Canvas::new();
884        let inner_handle = outer.group(innermost, [0.0, 0.0, 1.0, 1.0]);
885        outer.draw_group(inner_handle, Matrix::identity());
886        let mut canvas = Canvas::new();
887        let outer_handle = canvas.group(outer, [0.0, 0.0, 2.0, 2.0]);
888        canvas.draw_group(outer_handle, Matrix::identity());
889        let parts = canvas.into_parts();
890        let (outer_parts, _) = &parts.groups[0];
891        assert_eq!(outer_parts.groups.len(), 1);
892        let (inner_parts, _) = &outer_parts.groups[0];
893        assert_eq!(inner_parts.ops, [Op::Fill]);
894    }
895
896    #[test]
897    fn set_fill_alpha_pushes_gs_referencing_gs1() {
898        let mut canvas = Canvas::new();
899        canvas.set_fill_alpha(0.5);
900        assert_eq!(canvas.ops(), [Op::SetExtGState(name("Gs1"))]);
901        let parts = canvas.into_parts();
902        assert_eq!(parts.gstates.len(), 1);
903        assert_eq!(parts.gstates[0].fill_alpha, Some(0.5));
904        assert_eq!(parts.gstates[0].stroke_alpha, None);
905        assert_eq!(parts.gstates[0].blend_mode, None);
906    }
907
908    #[test]
909    fn set_stroke_alpha_and_blend_mode_get_distinct_resources() {
910        let mut canvas = Canvas::new();
911        canvas.set_fill_alpha(0.5);
912        canvas.set_stroke_alpha(0.5);
913        canvas.set_blend_mode(BlendMode::Multiply);
914        assert_eq!(
915            canvas.ops(),
916            [
917                Op::SetExtGState(name("Gs1")),
918                Op::SetExtGState(name("Gs2")),
919                Op::SetExtGState(name("Gs3")),
920            ]
921        );
922        assert_eq!(canvas.into_parts().gstates.len(), 3);
923    }
924
925    #[test]
926    fn identical_setter_calls_dedupe_to_one_resource() {
927        let mut canvas = Canvas::new();
928        canvas.set_fill_alpha(0.5);
929        canvas.set_fill_alpha(0.5);
930        assert_eq!(
931            canvas.ops(),
932            [Op::SetExtGState(name("Gs1")), Op::SetExtGState(name("Gs1"))]
933        );
934        assert_eq!(canvas.into_parts().gstates.len(), 1);
935    }
936
937    #[test]
938    fn gstate_setters_round_trip() {
939        let mut canvas = Canvas::new();
940        canvas.set_fill_alpha(0.25);
941        canvas.set_stroke_alpha(0.75);
942        canvas.set_blend_mode(BlendMode::Screen);
943        let bytes = serialize_ops(canvas.ops());
944        let parsed = parse_content(&bytes).unwrap();
945        assert_eq!(parsed, canvas.ops());
946    }
947}