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
10pub 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 } / 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 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 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 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 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}