1use 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)] const KAPPA: f32 = 0.552284749831;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum LineCap {
28 #[default]
30 Butt,
31 Round,
33 Square,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum LineJoin {
40 #[default]
42 Miter,
43 Round,
45 Bevel,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct ImageHandle(pub(crate) usize);
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct GroupHandle(pub(crate) usize);
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum BlendMode {
62 Normal,
64 Multiply,
66 Screen,
68 Overlay,
70 Darken,
72 Lighten,
74 ColorDodge,
76 ColorBurn,
78 HardLight,
80 SoftLight,
82 Difference,
84 Exclusion,
86}
87
88impl BlendMode {
89 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#[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 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
135fn name(text: &str) -> Name {
137 Name(text.to_string())
138}
139
140#[derive(Debug)]
142pub struct CanvasParts {
143 pub ops: Vec<Op>,
145 pub fonts: Vec<Standard14>,
147 pub images: Vec<ImageData>,
149 pub(crate) groups: Vec<(CanvasParts, [f32; 4])>,
152 pub(crate) gstates: Vec<GState>,
155}
156
157#[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 pub fn new() -> Canvas {
170 Canvas::default()
171 }
172
173 pub fn save(&mut self) {
175 self.ops.push(Op::Save);
176 }
177
178 pub fn restore(&mut self) {
180 self.ops.push(Op::Restore);
181 }
182
183 pub fn transform(&mut self, m: Matrix) {
185 self.ops.push(Op::Concat(m));
186 }
187
188 pub fn set_line_width(&mut self, width: f32) {
190 self.ops.push(Op::SetLineWidth(width));
191 }
192
193 pub fn set_line_cap(&mut self, cap: LineCap) {
195 self.ops.push(Op::SetLineCap(cap as i32));
196 }
197
198 pub fn set_line_join(&mut self, join: LineJoin) {
200 self.ops.push(Op::SetLineJoin(join as i32));
201 }
202
203 pub fn set_miter_limit(&mut self, limit: f32) {
205 self.ops.push(Op::SetMiterLimit(limit));
206 }
207
208 pub fn set_dash(&mut self, pattern: &[f32], phase: f32) {
210 self.ops.push(Op::SetDash(pattern.to_vec(), phase));
211 }
212
213 pub fn set_fill(&mut self, color: Color) {
215 self.ops.push(color.fill_op());
216 }
217
218 pub fn set_stroke(&mut self, color: Color) {
220 self.ops.push(color.stroke_op());
221 }
222
223 pub fn move_to(&mut self, x: f32, y: f32) {
225 self.ops.push(Op::MoveTo(x, y));
226 }
227
228 pub fn line_to(&mut self, x: f32, y: f32) {
230 self.ops.push(Op::LineTo(x, y));
231 }
232
233 #[allow(clippy::too_many_arguments)] 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 pub fn close(&mut self) {
241 self.ops.push(Op::ClosePath);
242 }
243
244 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 pub fn circle(&mut self, cx: f32, cy: f32, r: f32) {
252 self.ellipse(cx, cy, r, r);
253 }
254
255 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 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 pub fn fill(&mut self) {
287 self.ops.push(Op::Fill);
288 }
289
290 pub fn fill_even_odd(&mut self) {
292 self.ops.push(Op::FillEvenOdd);
293 }
294
295 pub fn stroke(&mut self) {
297 self.ops.push(Op::Stroke);
298 }
299
300 pub fn close_stroke(&mut self) {
302 self.ops.push(Op::CloseStroke);
303 }
304
305 pub fn fill_stroke(&mut self) {
307 self.ops.push(Op::FillStroke);
308 }
309
310 pub fn clip(&mut self) {
314 self.ops.push(Op::ClipNonZero);
315 self.ops.push(Op::EndPath);
316 }
317
318 pub fn clip_even_odd(&mut self) {
322 self.ops.push(Op::ClipEvenOdd);
323 self.ops.push(Op::EndPath);
324 }
325
326 pub fn end_path(&mut self) {
328 self.ops.push(Op::EndPath);
329 }
330
331 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 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 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 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 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 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 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 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 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 pub fn op(&mut self, op: Op) {
437 self.ops.push(op);
438 }
439
440 pub fn ops(&self) -> &[Op] {
442 &self.ops
443 }
444
445 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}