plotters_cairo/
backend.rs

1use cairo::{Context as CairoContext, FontSlant, FontWeight};
2
3use plotters_backend::text_anchor::{HPos, VPos};
4#[allow(unused_imports)]
5use plotters_backend::{
6    BackendColor, BackendCoord, BackendStyle, BackendTextStyle, DrawingBackend, DrawingErrorKind,
7    FontStyle, FontTransform,
8};
9
10/// The drawing backend that is backed with a Cairo context
11pub struct CairoBackend<'a> {
12    context: &'a CairoContext,
13    width: u32,
14    height: u32,
15    init_flag: bool,
16}
17
18#[derive(Debug)]
19pub struct CairoError;
20
21impl std::fmt::Display for CairoError {
22    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
23        write!(fmt, "{:?}", self)
24    }
25}
26
27impl std::error::Error for CairoError {}
28
29impl<'a> CairoBackend<'a> {
30    fn set_color(&self, color: &BackendColor) {
31        self.context.set_source_rgba(
32            f64::from(color.rgb.0) / 255.0,
33            f64::from(color.rgb.1) / 255.0,
34            f64::from(color.rgb.2) / 255.0,
35            color.alpha,
36        );
37    }
38
39    fn set_stroke_width(&self, width: u32) {
40        self.context.set_line_width(f64::from(width));
41    }
42
43    fn set_font<S: BackendTextStyle>(&self, font: &S) {
44        match font.style() {
45            FontStyle::Normal => self.context.select_font_face(
46                font.family().as_str(),
47                FontSlant::Normal,
48                FontWeight::Normal,
49            ),
50            FontStyle::Bold => self.context.select_font_face(
51                font.family().as_str(),
52                FontSlant::Normal,
53                FontWeight::Bold,
54            ),
55            FontStyle::Oblique => self.context.select_font_face(
56                font.family().as_str(),
57                FontSlant::Oblique,
58                FontWeight::Normal,
59            ),
60            FontStyle::Italic => self.context.select_font_face(
61                font.family().as_str(),
62                FontSlant::Italic,
63                FontWeight::Normal,
64            ),
65        };
66        self.context.set_font_size(font.size());
67    }
68
69    pub fn new(context: &'a CairoContext, (w, h): (u32, u32)) -> Result<Self, CairoError> {
70        Ok(Self {
71            context,
72            width: w,
73            height: h,
74            init_flag: false,
75        })
76    }
77}
78
79impl<'a> DrawingBackend for CairoBackend<'a> {
80    type ErrorType = cairo::Error;
81
82    fn get_size(&self) -> (u32, u32) {
83        (self.width, self.height)
84    }
85
86    fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
87        if !self.init_flag {
88            let (x0, y0, x1, y1) = self
89                .context
90                .clip_extents()
91                .map_err(DrawingErrorKind::DrawingError)?;
92
93            self.context.scale(
94                (x1 - x0) / f64::from(self.width),
95                (y1 - y0) / f64::from(self.height),
96            );
97
98            self.init_flag = true;
99        }
100
101        Ok(())
102    }
103
104    fn present(&mut self) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
105        Ok(())
106    }
107
108    fn draw_pixel(
109        &mut self,
110        point: BackendCoord,
111        color: BackendColor,
112    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
113        self.context
114            .rectangle(f64::from(point.0), f64::from(point.1), 1.0, 1.0);
115        self.context.set_source_rgba(
116            f64::from(color.rgb.0) / 255.0,
117            f64::from(color.rgb.1) / 255.0,
118            f64::from(color.rgb.2) / 255.0,
119            color.alpha,
120        );
121
122        self.context
123            .fill()
124            .map_err(DrawingErrorKind::DrawingError)?;
125
126        Ok(())
127    }
128
129    fn draw_line<S: BackendStyle>(
130        &mut self,
131        from: BackendCoord,
132        to: BackendCoord,
133        style: &S,
134    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
135        self.set_color(&style.color());
136        self.set_stroke_width(style.stroke_width());
137
138        self.context.move_to(f64::from(from.0), f64::from(from.1));
139        self.context.line_to(f64::from(to.0), f64::from(to.1));
140
141        self.context
142            .stroke()
143            .map_err(DrawingErrorKind::DrawingError)?;
144
145        Ok(())
146    }
147
148    fn draw_rect<S: BackendStyle>(
149        &mut self,
150        upper_left: BackendCoord,
151        bottom_right: BackendCoord,
152        style: &S,
153        fill: bool,
154    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
155        self.set_color(&style.color());
156        self.set_stroke_width(style.stroke_width());
157
158        self.context.rectangle(
159            f64::from(upper_left.0),
160            f64::from(upper_left.1),
161            f64::from(bottom_right.0 - upper_left.0),
162            f64::from(bottom_right.1 - upper_left.1),
163        );
164
165        if fill {
166            self.context
167                .fill()
168                .map_err(DrawingErrorKind::DrawingError)?;
169        } else {
170            self.context
171                .stroke()
172                .map_err(DrawingErrorKind::DrawingError)?;
173        }
174
175        Ok(())
176    }
177
178    fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
179        &mut self,
180        path: I,
181        style: &S,
182    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
183        self.set_color(&style.color());
184        self.set_stroke_width(style.stroke_width());
185
186        let mut path = path.into_iter();
187        if let Some((x, y)) = path.next() {
188            self.context.move_to(f64::from(x), f64::from(y));
189        }
190
191        for (x, y) in path {
192            self.context.line_to(f64::from(x), f64::from(y));
193        }
194
195        self.context
196            .stroke()
197            .map_err(DrawingErrorKind::DrawingError)?;
198
199        Ok(())
200    }
201
202    fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
203        &mut self,
204        path: I,
205        style: &S,
206    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
207        self.set_color(&style.color());
208        self.set_stroke_width(style.stroke_width());
209
210        let mut path = path.into_iter();
211
212        if let Some((x, y)) = path.next() {
213            self.context.move_to(f64::from(x), f64::from(y));
214
215            for (x, y) in path {
216                self.context.line_to(f64::from(x), f64::from(y));
217            }
218
219            self.context.close_path();
220            self.context
221                .fill()
222                .map_err(DrawingErrorKind::DrawingError)?;
223        }
224
225        Ok(())
226    }
227
228    fn draw_circle<S: BackendStyle>(
229        &mut self,
230        center: BackendCoord,
231        radius: u32,
232        style: &S,
233        fill: bool,
234    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
235        self.set_color(&style.color());
236        self.set_stroke_width(style.stroke_width());
237
238        self.context.new_sub_path();
239        self.context.arc(
240            f64::from(center.0),
241            f64::from(center.1),
242            f64::from(radius),
243            0.0,
244            std::f64::consts::PI * 2.0,
245        );
246
247        if fill {
248            self.context
249                .fill()
250                .map_err(DrawingErrorKind::DrawingError)?;
251        } else {
252            self.context
253                .stroke()
254                .map_err(DrawingErrorKind::DrawingError)?;
255        }
256
257        Ok(())
258    }
259
260    fn estimate_text_size<S: BackendTextStyle>(
261        &self,
262        text: &str,
263        font: &S,
264    ) -> Result<(u32, u32), DrawingErrorKind<Self::ErrorType>> {
265        self.set_font(font);
266
267        let extents = self
268            .context
269            .text_extents(text)
270            .map_err(DrawingErrorKind::DrawingError)?;
271
272        Ok((extents.width() as u32, extents.height() as u32))
273    }
274
275    fn draw_text<S: BackendTextStyle>(
276        &mut self,
277        text: &str,
278        style: &S,
279        pos: BackendCoord,
280    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
281        let color = style.color();
282        let (mut x, mut y) = (pos.0, pos.1);
283
284        let degree = match style.transform() {
285            FontTransform::None => 0.0,
286            FontTransform::Rotate90 => 90.0,
287            FontTransform::Rotate180 => 180.0,
288            FontTransform::Rotate270 => 270.0,
289            //FontTransform::RotateAngle(angle) => angle as f64,
290        } / 180.0
291            * std::f64::consts::PI;
292
293        if degree != 0.0 {
294            self.context
295                .save()
296                .map_err(DrawingErrorKind::DrawingError)?;
297            self.context.translate(f64::from(x), f64::from(y));
298            self.context.rotate(degree);
299
300            x = 0;
301            y = 0;
302        }
303
304        self.set_font(style);
305        self.set_color(&color);
306
307        let extents = self
308            .context
309            .text_extents(text)
310            .map_err(DrawingErrorKind::DrawingError)?;
311
312        let dx = match style.anchor().h_pos {
313            HPos::Left => 0.0,
314            HPos::Right => -extents.width(),
315            HPos::Center => -extents.width() / 2.0,
316        };
317        let dy = match style.anchor().v_pos {
318            VPos::Top => extents.height(),
319            VPos::Center => extents.height() / 2.0,
320            VPos::Bottom => 0.0,
321        };
322
323        self.context.move_to(
324            f64::from(x) + dx - extents.x_bearing(),
325            f64::from(y) + dy - extents.y_bearing() - extents.height(),
326        );
327
328        self.context
329            .show_text(text)
330            .map_err(DrawingErrorKind::DrawingError)?;
331
332        if degree != 0.0 {
333            self.context
334                .restore()
335                .map_err(DrawingErrorKind::DrawingError)?;
336        }
337
338        Ok(())
339    }
340}
341
342#[cfg(test)]
343mod test {
344    use super::*;
345    use plotters::prelude::*;
346    use plotters_backend::text_anchor::{HPos, Pos, VPos};
347    use std::fs;
348    use std::path::Path;
349
350    static DST_DIR: &str = "target/test/cairo";
351
352    fn checked_save_file(name: &str, content: &str) {
353        /*
354          Please use the PS file to manually verify the results.
355
356          You may want to use Ghostscript to view the file.
357        */
358        assert!(!content.is_empty());
359        fs::create_dir_all(DST_DIR).unwrap();
360        let file_name = format!("{}.ps", name);
361        let file_path = Path::new(DST_DIR).join(file_name);
362        println!("{:?} created", file_path);
363        fs::write(file_path, &content).unwrap();
364    }
365
366    fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) {
367        let buffer: Vec<u8> = vec![];
368        let surface = cairo::PsSurface::for_stream(500.0, 500.0, buffer).unwrap();
369        let cr = CairoContext::new(&surface).unwrap();
370        let root = CairoBackend::new(&cr, (500, 500))
371            .unwrap()
372            .into_drawing_area();
373
374        // Text could be rendered to different elements if has whitespaces
375        let mut chart = ChartBuilder::on(&root)
376            .caption("this-is-a-test", ("sans-serif", 20))
377            .set_all_label_area_size(40)
378            .build_cartesian_2d(0..10, 0..10)
379            .unwrap();
380
381        chart
382            .configure_mesh()
383            .set_all_tick_mark_size(tick_size)
384            .draw()
385            .unwrap();
386
387        let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
388        let content = String::from_utf8(buffer).unwrap();
389        checked_save_file(test_name, &content);
390
391        // FIXME: through some change in cairo or something the caption no longer
392        // appears in plaintext so this assertion will fail even though the postscript
393        // file contains the heading
394        assert!(content.contains("this-is-a-test"));
395    }
396
397    #[test]
398    fn test_draw_mesh_no_ticks() {
399        draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks");
400    }
401
402    #[test]
403    fn test_draw_mesh_negative_ticks() {
404        draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks");
405    }
406
407    #[test]
408    fn test_text_draw() {
409        let buffer: Vec<u8> = vec![];
410        let (width, height) = (1500, 800);
411        let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap();
412        let cr = CairoContext::new(&surface).unwrap();
413        let root = CairoBackend::new(&cr, (width, height))
414            .unwrap()
415            .into_drawing_area();
416        let root = root
417            .titled("Image Title", ("sans-serif", 60).into_font())
418            .unwrap();
419
420        let mut chart = ChartBuilder::on(&root)
421            .caption("All anchor point positions", ("sans-serif", 20))
422            .set_all_label_area_size(40)
423            .build_cartesian_2d(0..100, 0..50)
424            .unwrap();
425
426        chart
427            .configure_mesh()
428            .disable_x_mesh()
429            .disable_y_mesh()
430            .x_desc("X Axis")
431            .y_desc("Y Axis")
432            .draw()
433            .unwrap();
434
435        let ((x1, y1), (x2, y2), (x3, y3)) = ((-30, 30), (0, -30), (30, 30));
436
437        for (dy, trans) in [
438            FontTransform::None,
439            FontTransform::Rotate90,
440            FontTransform::Rotate180,
441            FontTransform::Rotate270,
442        ]
443        .iter()
444        .enumerate()
445        {
446            for (dx1, h_pos) in [HPos::Left, HPos::Right, HPos::Center].iter().enumerate() {
447                for (dx2, v_pos) in [VPos::Top, VPos::Center, VPos::Bottom].iter().enumerate() {
448                    let x = 150_i32 + (dx1 as i32 * 3 + dx2 as i32) * 150;
449                    let y = 120 + dy as i32 * 150;
450                    let draw = |x, y, text| {
451                        root.draw(&Circle::new((x, y), 3, &BLACK.mix(0.5))).unwrap();
452                        let style = TextStyle::from(("sans-serif", 20).into_font())
453                            .pos(Pos::new(*h_pos, *v_pos))
454                            .transform(trans.clone());
455                        root.draw_text(text, &style, (x, y)).unwrap();
456                    };
457                    draw(x + x1, y + y1, "dood");
458                    draw(x + x2, y + y2, "dog");
459                    draw(x + x3, y + y3, "goog");
460                }
461            }
462        }
463
464        let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
465        let content = String::from_utf8(buffer).unwrap();
466        checked_save_file("test_text_draw", &content);
467
468        // FIXME: see `draw_mesh_with_custom_ticks`
469        assert_eq!(content.matches("dog").count(), 36);
470        assert_eq!(content.matches("dood").count(), 36);
471        assert_eq!(content.matches("goog").count(), 36);
472    }
473
474    #[test]
475    fn test_text_clipping() {
476        let buffer: Vec<u8> = vec![];
477        let (width, height) = (500_i32, 500_i32);
478        let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap();
479        let cr = CairoContext::new(&surface).unwrap();
480        let root = CairoBackend::new(&cr, (width as u32, height as u32))
481            .unwrap()
482            .into_drawing_area();
483
484        let style = TextStyle::from(("sans-serif", 20).into_font())
485            .pos(Pos::new(HPos::Center, VPos::Center));
486        root.draw_text("TOP LEFT", &style, (0, 0)).unwrap();
487        root.draw_text("TOP CENTER", &style, (width / 2, 0))
488            .unwrap();
489        root.draw_text("TOP RIGHT", &style, (width, 0)).unwrap();
490
491        root.draw_text("MIDDLE LEFT", &style, (0, height / 2))
492            .unwrap();
493        root.draw_text("MIDDLE RIGHT", &style, (width, height / 2))
494            .unwrap();
495
496        root.draw_text("BOTTOM LEFT", &style, (0, height)).unwrap();
497        root.draw_text("BOTTOM CENTER", &style, (width / 2, height))
498            .unwrap();
499        root.draw_text("BOTTOM RIGHT", &style, (width, height))
500            .unwrap();
501
502        let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
503        let content = String::from_utf8(buffer).unwrap();
504        checked_save_file("test_text_clipping", &content);
505    }
506
507    #[test]
508    fn test_series_labels() {
509        let buffer: Vec<u8> = vec![];
510        let (width, height) = (500, 500);
511        let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap();
512        let cr = CairoContext::new(&surface).unwrap();
513        let root = CairoBackend::new(&cr, (width, height))
514            .unwrap()
515            .into_drawing_area();
516
517        let mut chart = ChartBuilder::on(&root)
518            .caption("All series label positions", ("sans-serif", 20))
519            .set_all_label_area_size(40)
520            .build_cartesian_2d(0..50, 0..50)
521            .unwrap();
522
523        chart
524            .configure_mesh()
525            .disable_x_mesh()
526            .disable_y_mesh()
527            .draw()
528            .unwrap();
529
530        chart
531            .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED)))
532            .expect("Drawing error")
533            .label("Series 1")
534            .legend(|(x, y)| Circle::new((x, y), 3, RED.filled()));
535
536        chart
537            .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE)))
538            .expect("Drawing error")
539            .label("Series 2")
540            .legend(|(x, y)| Circle::new((x, y), 3, BLUE.filled()));
541
542        for pos in vec![
543            SeriesLabelPosition::UpperLeft,
544            SeriesLabelPosition::MiddleLeft,
545            SeriesLabelPosition::LowerLeft,
546            SeriesLabelPosition::UpperMiddle,
547            SeriesLabelPosition::MiddleMiddle,
548            SeriesLabelPosition::LowerMiddle,
549            SeriesLabelPosition::UpperRight,
550            SeriesLabelPosition::MiddleRight,
551            SeriesLabelPosition::LowerRight,
552            SeriesLabelPosition::Coordinate(70, 70),
553        ]
554        .into_iter()
555        {
556            chart
557                .configure_series_labels()
558                .border_style(&BLACK.mix(0.5))
559                .position(pos)
560                .draw()
561                .expect("Drawing error");
562        }
563
564        let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
565        let content = String::from_utf8(buffer).unwrap();
566        checked_save_file("test_series_labels", &content);
567    }
568
569    #[test]
570    fn test_draw_pixel_alphas() {
571        let buffer: Vec<u8> = vec![];
572        let (width, height) = (100_i32, 100_i32);
573        let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap();
574        let cr = CairoContext::new(&surface).unwrap();
575        let root = CairoBackend::new(&cr, (width as u32, height as u32))
576            .unwrap()
577            .into_drawing_area();
578
579        for i in -20..20 {
580            let alpha = i as f64 * 0.1;
581            root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha))
582                .unwrap();
583        }
584
585        let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
586        let content = String::from_utf8(buffer).unwrap();
587        checked_save_file("test_draw_pixel_alphas", &content);
588    }
589}