plt_cairo/
lib.rs

1use std::{error, f64, marker, path};
2#[cfg(any(feature = "svg", feature = "png"))]
3use std::{fs, io};
4#[cfg(feature = "svg")]
5use std::env;
6
7/// Converts a Cairo error to a draw error.
8fn convert_err<E: error::Error + marker::Sync + marker::Send + 'static>(
9    e: E,
10) -> draw::DrawError {
11    draw::DrawError::BackendError(e.into())
12}
13
14/// The Cairo backend for `plt`.
15#[derive(Debug)]
16pub struct CairoCanvas {
17    size: draw::Size,
18    context: cairo::Context,
19    image_format: draw::ImageFormat,
20    #[allow(dead_code)]
21    temp_file: Option<path::PathBuf>,
22}
23impl CairoCanvas {
24    /// Construct from existing context.
25    pub fn from_context(
26        context: &cairo::Context,
27        size: draw::Size,
28        image_format: draw::ImageFormat,
29    ) -> Self {
30        Self {
31            size,
32            context: context.clone(),
33            image_format,
34            temp_file: None,
35        }
36    }
37}
38impl draw::Canvas for CairoCanvas {
39    fn new(desc: draw::CanvasDescriptor) -> Result<Self, draw::DrawError> {
40        let (context, temp_file) = match desc.image_format {
41            draw::ImageFormat::Bitmap => {
42                let surface = cairo::ImageSurface::create(
43                    cairo::Format::ARgb32,
44                    desc.size.width as i32,
45                    desc.size.height as i32,
46                )
47                .map_err(convert_err)?;
48
49                (cairo::Context::new(&surface).map_err(convert_err)?, None)
50            },
51            draw::ImageFormat::Svg => {
52                #[cfg(feature = "svg")]
53                {
54                    let mut temp_filename = env::temp_dir();
55                    temp_filename.push("plt_temp.svg");
56                    let temp_file = Some(temp_filename);
57
58                    let surface = cairo::SvgSurface::new(
59                        desc.size.width.into(),
60                        desc.size.height.into(),
61                        temp_file.as_ref(),
62                    )
63                    .map_err(|e| draw::DrawError::BackendError(e.into()))?;
64
65                    (cairo::Context::new(&surface).map_err(convert_err)?, temp_file)
66                }
67
68                #[cfg(not(feature = "svg"))]
69                return Err(draw::DrawError::UnsupportedImageFormat(
70                    "svg feature is not enabled".to_string()
71                ))
72            },
73            image_format => {
74                return Err(draw::DrawError::UnsupportedImageFormat(
75                    format!("{:?} is not supported by the Cairo backend", image_format)
76                ))
77            }
78        };
79
80        context.set_source_rgba(
81            desc.face_color.r,
82            desc.face_color.g,
83            desc.face_color.b,
84            desc.face_color.a,
85        );
86
87        context.paint().unwrap();
88
89        Ok(Self {
90            size: desc.size,
91            context,
92            image_format: desc.image_format,
93            temp_file,
94        })
95    }
96
97    fn draw_shape(&mut self, desc: draw::ShapeDescriptor) -> Result<(), draw::DrawError> {
98        let origin = CairoPoint::from_point(desc.point, self.size);
99
100        self.context.save().map_err(convert_err)?;
101
102        if let Some(area) = desc.clip_area {
103            self.clip_area(area);
104        }
105
106        match desc.shape {
107            draw::Shape::Rectangle { h, w } => {
108                self.context.rectangle(
109                    origin.x - (w as f64) / 2.0,
110                    origin.y - (h as f64) / 2.0,
111                    w as f64,
112                    h as f64,
113                );
114                self.context.close_path();
115            },
116            draw::Shape::Square { l } => {
117                self.context.rectangle(
118                    origin.x - (l as f64) / 2.0,
119                    origin.y - (l as f64) / 2.0,
120                    l as f64,
121                    l as f64,
122                );
123                self.context.close_path();
124            },
125            draw::Shape::Circle { r } => {
126                self.context.arc(
127                    origin.x,
128                    origin.y,
129                    r as f64,
130                    0.0,
131                    2.0 * f64::consts::PI,
132                );
133                self.context.close_path();
134            },
135            shape => {
136                return Err(draw::DrawError::UnsupportedShape(
137                    format!("{:?} is not supported by the Cairo backend", shape)
138                ))
139            }
140        };
141
142        // fill shape
143        self.context.set_source_rgba(
144            desc.fill_color.r,
145            desc.fill_color.g,
146            desc.fill_color.b,
147            desc.fill_color.a,
148        );
149        self.context.fill_preserve().map_err(convert_err)?;
150
151        // outline shape
152        self.context.set_dash(desc.line_dashes, 0.0);
153        self.context.set_line_width(desc.line_width as f64);
154        self.context.set_source_rgba(
155            desc.line_color.r,
156            desc.line_color.g,
157            desc.line_color.b,
158            desc.line_color.a,
159        );
160        self.context.stroke().map_err(convert_err)?;
161
162        self.reset_clip();
163
164        self.context.restore().map_err(convert_err)?;
165
166        Ok(())
167    }
168
169    fn draw_line(&mut self, desc: draw::LineDescriptor) -> Result<(), draw::DrawError> {
170        let p1 = CairoPoint::from_point(desc.line.p1, self.size);
171        let p2 = CairoPoint::from_point(desc.line.p2, self.size);
172
173        self.context.save().map_err(convert_err)?;
174
175        if let Some(area) = desc.clip_area {
176            self.clip_area(area);
177        }
178
179        self.context.set_source_rgba(
180            desc.line_color.r,
181            desc.line_color.g,
182            desc.line_color.b,
183            desc.line_color.a,
184        );
185        self.context.set_line_width(desc.line_width as f64);
186
187        self.context.set_dash(desc.dashes, 0.0);
188
189        let offset = if desc.line_width % 2 == 0 { 0.0 } else { 0.5 };
190
191        self.context.line_to(p1.x + offset, p1.y - offset);
192        self.context.line_to(p2.x + offset, p2.y - offset);
193
194        self.context.stroke().map_err(convert_err)?;
195
196        self.reset_clip();
197
198        self.context.restore().map_err(convert_err)?;
199
200        Ok(())
201    }
202
203    fn draw_curve(&mut self, desc: draw::CurveDescriptor) -> Result<(), draw::DrawError> {
204        self.context.save().map_err(convert_err)?;
205
206        if let Some(area) = desc.clip_area {
207            self.clip_area(area);
208        }
209
210        self.context.set_source_rgba(
211            desc.line_color.r,
212            desc.line_color.g,
213            desc.line_color.b,
214            desc.line_color.a,
215        );
216        self.context.set_line_width(desc.line_width as f64);
217        self.context.set_line_join(cairo::LineJoin::Round);
218
219        self.context.set_dash(desc.dashes, 0.0);
220
221        let offset = if desc.line_width % 2 == 0 { 0.0 } else { 0.5 };
222
223        for point in desc.points {
224            let point = CairoPoint::from_point(point, self.size);
225
226            self.context.line_to(point.x + offset, point.y - offset);
227        }
228
229        self.context.stroke().map_err(convert_err)?;
230
231        self.reset_clip();
232
233        self.context.restore().map_err(convert_err)?;
234
235        Ok(())
236    }
237
238    fn fill_region(&mut self, desc: draw::FillDescriptor) -> Result<(), draw::DrawError> {
239        self.context.save().map_err(convert_err)?;
240
241        if let Some(area) = desc.clip_area {
242            self.clip_area(area);
243        }
244
245        self.context.set_source_rgba(
246            desc.fill_color.r,
247            desc.fill_color.g,
248            desc.fill_color.b,
249            desc.fill_color.a,
250        );
251
252        for point in desc.points {
253            let point = CairoPoint::from_point(point, self.size);
254
255            self.context.line_to(point.x, point.y);
256        }
257
258        self.context.close_path();
259
260        self.context.fill().map_err(convert_err)?;
261
262        self.reset_clip();
263
264        self.context.restore().map_err(convert_err)?;
265
266        Ok(())
267    }
268
269    fn draw_text(&mut self, desc: draw::TextDescriptor) -> Result<(), draw::DrawError> {
270        let position = CairoPoint::from_point(desc.position, self.size);
271
272        self.context.save().map_err(convert_err)?;
273
274        if let Some(area) = desc.clip_area {
275            self.clip_area(area);
276        }
277
278        self.context.set_source_rgba(
279            desc.color.r,
280            desc.color.g,
281            desc.color.b,
282            desc.color.a,
283        );
284
285        self.context.select_font_face(
286            font_to_cairo(desc.font.name),
287            font_slant_to_cairo(desc.font.slant),
288            font_weight_to_cairo(desc.font.weight),
289        );
290        self.context.set_font_size(desc.font.size as f64);
291
292        let extents = self.context.text_extents(&desc.text).map_err(convert_err)?;
293
294        let position = align_text(position, desc.rotation, extents, desc.alignment);
295        self.context.move_to(position.x, position.y);
296
297        self.context.save().map_err(convert_err)?;
298        self.context.rotate(desc.rotation);
299        self.context.show_text(&desc.text).map_err(convert_err)?;
300        self.context.restore().map_err(convert_err)?;
301
302        self.context.stroke().map_err(convert_err)?;
303
304        self.reset_clip();
305
306        self.context.restore().map_err(convert_err)?;
307
308        Ok(())
309    }
310
311    fn text_size(&mut self, desc: draw::TextDescriptor) -> Result<draw::Size, draw::DrawError> {
312        self.context.save().map_err(convert_err)?;
313
314        self.context.set_source_rgba(
315            desc.color.r,
316            desc.color.g,
317            desc.color.b,
318            desc.color.a,
319        );
320
321        self.context.select_font_face(
322            font_to_cairo(desc.font.name),
323            font_slant_to_cairo(desc.font.slant),
324            font_weight_to_cairo(desc.font.weight),
325        );
326        self.context.set_font_size(desc.font.size as f64);
327
328        let extents = self.context.text_extents(&desc.text).map_err(convert_err)?;
329
330        self.context.stroke().map_err(convert_err)?;
331
332        self.context.restore().map_err(convert_err)?;
333
334        Ok(draw::Size {
335            width: extents.width().ceil() as u32,
336            height: extents.height().ceil() as u32,
337        })
338    }
339
340    fn save_file<P: AsRef<path::Path>>(
341        &mut self,
342        desc: draw::SaveFileDescriptor<P>,
343    ) -> Result<(), draw::DrawError> {
344        match self.image_format {
345            draw::ImageFormat::Bitmap => {
346                match desc.format {
347                    #[cfg(feature = "png")]
348                    draw::FileFormat::Png => {
349                        // temporarily remove surface from context
350                        let mut surface = cairo::ImageSurface::try_from(
351                            self.context.target()
352                        )
353                        .unwrap();
354                        let blank_surface = cairo::ImageSurface::create(
355                            cairo::Format::ARgb32,
356                            0,
357                            0,
358                        )
359                        .map_err(convert_err)?;
360                        self.context = cairo::Context::new(&blank_surface).map_err(convert_err)?;
361
362                        let file = fs::File::create(desc.filename)?;
363                        let w = &mut io::BufWriter::new(file);
364
365                        // configure encoder
366                        let mut encoder = png::Encoder::new(
367                            w,
368                            self.size.width as u32,
369                            self.size.height as u32,
370                        );
371                        encoder.set_color(png::ColorType::Rgba);
372                        encoder.set_depth(png::BitDepth::Eight);
373                        let mut writer = encoder.write_header().map_err(convert_err)?;
374
375                        // extract buffer from cairo
376                        let buffer_raw = surface.data().map_err(convert_err)?;
377                        // fix color byte ordering
378                        let buffer = buffer_raw.chunks(4)
379                            .flat_map(|rgba| [rgba[2], rgba[1], rgba[0], rgba[3]])
380                            .collect::<Vec<_>>();
381
382                        // set dpi
383                        let ppu = (desc.dpi as f64 * (1000.0 / 25.4)) as u32;
384                        let xppu = ppu.to_be_bytes();
385                        let yppu = ppu.to_be_bytes();
386                        let unit = png::Unit::Meter;
387                        writer.write_chunk(
388                            png::chunk::pHYs,
389                            &[
390                                xppu[0], xppu[1], xppu[2], xppu[3],
391                                yppu[0], yppu[1], yppu[2], yppu[3],
392                                unit as u8,
393                            ],
394                        )
395                        .map_err(convert_err)?;
396
397                        writer.write_image_data(&buffer[..]).map_err(convert_err)?;
398
399                        drop(buffer_raw);
400                        drop(buffer);
401
402                        // return surface to self
403                        self.context = cairo::Context::new(&surface).map_err(convert_err)?;
404                    },
405                    #[cfg(not(feature = "png"))]
406                    draw::FileFormat::Png => {
407                        return Err(draw::DrawError::UnsupportedFileFormat(
408                            "png feature is not enabled".to_string()
409                        ))
410                    },
411                    file_format => {
412                        return Err(draw::DrawError::UnsupportedFileFormat(format!(
413                            "{:?} is not supported by the Cairo backend for bitmap images",
414                            file_format,
415                        )))
416                    },
417                }
418            },
419            draw::ImageFormat::Svg => {
420                #[cfg(feature = "svg")]
421                match desc.format {
422                    draw::FileFormat::Svg => {
423                        // finish writing file
424                        let old_surface = cairo::SvgSurface::try_from(
425                            self.context.target()
426                        )
427                        .unwrap();
428                        old_surface.finish();
429
430                        if let Some(temp_file) = &self.temp_file {
431                            // copy temp file to new specified location
432                            fs::copy(temp_file, desc.filename.as_ref())?;
433
434                            // remove temp file
435                            fs::remove_file(temp_file)?;
436                        }
437                    },
438                    file_format => {
439                        return Err(draw::DrawError::UnsupportedFileFormat(
440                            format!("{:?} is not supported for svg images", file_format)
441                        ))
442                    },
443                }
444
445                #[cfg(not(feature = "svg"))]
446                return Err(draw::DrawError::UnsupportedFileFormat(
447                    "svg feature is not enabled".to_string()
448                ))
449            },
450            image_format => {
451                return Err(draw::DrawError::UnsupportedImageFormat(
452                    format!("{:?} is not supported by the Cairo backend", image_format)
453                ))
454            }
455        };
456
457        #[allow(unreachable_code)]
458        Ok(())
459    }
460    fn size(&self) -> Result<draw::Size, draw::DrawError> {
461        Ok(self.size)
462    }
463}
464impl CairoCanvas {
465    fn reset_clip(&mut self) {
466        self.context.reset_clip();
467    }
468    fn clip_area(&mut self, area: draw::Area) {
469        self.context.reset_clip();
470        self.context.new_path();
471
472        let points = [
473            draw::Point { x: area.xmin as f64, y: area.ymin as f64 },
474            draw::Point { x: area.xmin as f64, y: area.ymax as f64 },
475            draw::Point { x: area.xmax as f64, y: area.ymax as f64 },
476            draw::Point { x: area.xmax as f64, y: area.ymin as f64 },
477        ];
478
479        for point in points {
480            let point = CairoPoint::from_point(point, self.size);
481            self.context.line_to(point.x, point.y);
482        }
483
484        self.context.clip();
485    }
486}
487
488// private
489
490#[derive(Copy, Clone, Debug)]
491struct CairoPoint {
492    pub x: f64,
493    pub y: f64,
494}
495impl CairoPoint {
496    fn from_point(point: draw::Point, size: draw::Size) -> Self {
497        Self { x: point.x, y: (size.height as f64 - point.y) }
498    }
499}
500
501fn font_to_cairo(name: draw::FontName) -> &'static str {
502    match name {
503        draw::FontName::Arial => "Arial",
504        draw::FontName::Georgia => "Georgia",
505        _ => "Arial",
506    }
507}
508fn font_slant_to_cairo(slant: draw::FontSlant) -> cairo::FontSlant {
509    match slant {
510        draw::FontSlant::Normal => cairo::FontSlant::Normal,
511        draw::FontSlant::Italic => cairo::FontSlant::Italic,
512        draw::FontSlant::Oblique => cairo::FontSlant::Oblique,
513    }
514}
515fn font_weight_to_cairo(weight: draw::FontWeight) -> cairo::FontWeight {
516    match weight {
517        draw::FontWeight::Normal => cairo::FontWeight::Normal,
518        draw::FontWeight::Bold => cairo::FontWeight::Bold,
519    }
520}
521
522fn align_text(
523    position: CairoPoint,
524    rotation: f64,
525    extents: cairo::TextExtents,
526    alignment: draw::Alignment,
527) -> CairoPoint {
528    let (x, y) = match alignment {
529        draw::Alignment::Center => (
530            position.x - (extents.x_bearing() + extents.width() / 2.0)*rotation.cos()
531                + (extents.y_bearing() + extents.height() / 2.0)*rotation.sin(),
532            position.y - (extents.y_bearing() + extents.height() / 2.0)*rotation.cos()
533                - (extents.x_bearing() + extents.width() / 2.0)*rotation.sin(),
534        ),
535        draw::Alignment::Right => (
536            position.x - extents.x_bearing()*rotation.cos()
537                - extents.width()*rotation.cos().clamp(0.0, 1.0)
538                + extents.y_bearing()*rotation.sin().clamp(0.0, 1.0),
539            position.y - (extents.y_bearing() + (extents.height() / 2.0))*rotation.cos()
540                - (extents.x_bearing() + extents.width() / 2.0)*rotation.sin(),
541        ),
542        draw::Alignment::Left => (
543            position.x - extents.x_bearing()*rotation.cos()
544                - extents.width()*rotation.cos().clamp(-1.0, 0.0)
545                + extents.y_bearing()*rotation.sin()
546                + extents.height()*rotation.sin().clamp(0.0, 1.0),
547            position.y - (extents.y_bearing() + extents.height() / 2.0)*rotation.cos()
548                - (extents.x_bearing() + extents.width() / 2.0)*rotation.sin(),
549        ),
550        draw::Alignment::Top => (
551            position.x - (extents.x_bearing() + extents.width() / 2.0)*rotation.cos()
552                + (extents.y_bearing() + extents.height() / 2.0)*rotation.sin(),
553            position.y - extents.y_bearing()*rotation.cos()
554                - extents.x_bearing()*rotation.sin()
555                - extents.width()*rotation.sin().clamp(-1.0, 0.0)
556                - extents.height()*rotation.cos().clamp(-1.0, 0.0),
557        ),
558        draw::Alignment::Bottom => (
559            position.x - (extents.x_bearing() + extents.width() / 2.0)*rotation.cos()
560                + (extents.y_bearing() + extents.height() / 2.0)*rotation.sin(),
561            position.y - extents.y_bearing()*rotation.cos()
562                - extents.height()*rotation.cos().clamp(0.0, 1.0)
563                - extents.x_bearing()*rotation.sin()
564                - extents.width()*rotation.sin().clamp(0.0, 1.0),
565        ),
566        draw::Alignment::TopRight => (
567            position.x - extents.x_bearing()*rotation.cos()
568                - extents.width()*rotation.cos().clamp(0.0, 1.0)
569                + extents.y_bearing()*rotation.sin()
570                + extents.height()*rotation.sin().clamp(-1.0, 0.0),
571            position.y - extents.y_bearing()*rotation.cos()
572                - extents.height()*rotation.cos().clamp(-1.0, 0.0)
573                - extents.x_bearing()*rotation.sin()
574                - extents.width()*rotation.sin().clamp(-1.0, 0.0),
575        ),
576        draw::Alignment::TopLeft => (
577            position.x - extents.x_bearing()*rotation.cos()
578                - extents.width()*rotation.cos().clamp(-1.0, 0.0)
579                + extents.y_bearing()*rotation.sin()
580                + extents.height()*rotation.sin().clamp(0.0, 1.0),
581            position.y - extents.y_bearing()*rotation.cos()
582                - extents.height()*rotation.cos().clamp(-1.0, 0.0)
583                + extents.x_bearing()*rotation.sin()
584                - extents.width()*rotation.sin().clamp(-1.0, 0.0),
585        ),
586        draw::Alignment::BottomRight => (
587            position.x - extents.x_bearing()*rotation.cos()
588                - extents.width()*rotation.cos().clamp(0.0, 1.0)
589                + extents.y_bearing()*rotation.sin()
590                + extents.height()*rotation.sin().clamp(-1.0, 0.0),
591            position.y - extents.y_bearing()*rotation.cos()
592                - extents.height()*rotation.cos().clamp(0.0, 1.0)
593                + extents.x_bearing()*rotation.sin()
594                - extents.width()*rotation.sin().clamp(0.0, 1.0),
595        ),
596        draw::Alignment::BottomLeft => (
597            position.x - extents.x_bearing()*rotation.cos()
598                - extents.width()*rotation.cos().clamp(-1.0, 0.0)
599                + extents.y_bearing()*rotation.sin()
600                + extents.height()*rotation.sin().clamp(0.0, 1.0),
601            position.y - extents.y_bearing()*rotation.cos()
602                - extents.height()*rotation.cos().clamp(0.0, 1.0)
603                + extents.x_bearing()*rotation.sin()
604                - extents.width()*rotation.sin().clamp(0.0, 1.0),
605        ),
606    };
607
608    CairoPoint { x, y }
609}