piet_cairo/
lib.rs

1// Copyright 2019 the Piet Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! The Cairo backend for the Piet 2D graphics abstraction.
5
6#![cfg_attr(docsrs, feature(doc_auto_cfg))]
7#![deny(clippy::trivially_copy_pass_by_ref)]
8
9mod text;
10
11use std::borrow::Cow;
12
13use cairo::{Context, Filter, Format, ImageSurface, Matrix, Rectangle, SurfacePattern};
14
15use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Size};
16use piet::{
17    Color, Error, FixedGradient, Image, ImageFormat, InterpolationMode, IntoBrush, LineCap,
18    LineJoin, RenderContext, StrokeStyle,
19};
20
21pub use cairo;
22
23pub use crate::text::{CairoText, CairoTextLayout, CairoTextLayoutBuilder};
24
25pub struct CairoRenderContext<'a> {
26    // Cairo has this as Clone and with &self methods, but we do this to avoid
27    // concurrency problems.
28    ctx: &'a Context,
29    text: CairoText,
30    // because of the relationship between GTK and cairo (where GTK applies a transform
31    // to adjust for menus and window borders) we cannot trust the transform returned
32    // by cairo. Instead we maintain our own stack, which will contain
33    // only those transforms applied by us.
34    transform_stack: Vec<Affine>,
35    error: Result<(), cairo::Error>,
36}
37
38#[derive(Clone)]
39pub enum Brush {
40    Solid(u32),
41    Linear(cairo::LinearGradient),
42    Radial(cairo::RadialGradient),
43}
44
45#[derive(Clone)]
46pub struct CairoImage(ImageSurface);
47
48// we call this with different types of gradient that have `add_color_stop_rgba` fns,
49// and there's no trait for this behaviour so we use a macro. ¯\_(ツ)_/¯
50macro_rules! set_gradient_stops {
51    ($dst: expr, $stops: expr) => {
52        for stop in $stops {
53            let rgba = stop.color.as_rgba_u32();
54            $dst.add_color_stop_rgba(
55                stop.pos as f64,
56                byte_to_frac(rgba >> 24),
57                byte_to_frac(rgba >> 16),
58                byte_to_frac(rgba >> 8),
59                byte_to_frac(rgba),
60            );
61        }
62    };
63}
64
65impl<'a> RenderContext for CairoRenderContext<'a> {
66    type Brush = Brush;
67
68    type Text = CairoText;
69    type TextLayout = CairoTextLayout;
70
71    type Image = CairoImage;
72
73    fn status(&mut self) -> Result<(), Error> {
74        match self.error {
75            Ok(_) => Ok(()),
76            Err(err) => Err(Error::BackendError(err.into())),
77        }
78    }
79
80    fn clear(&mut self, region: impl Into<Option<Rect>>, color: Color) {
81        let region: Option<Rect> = region.into();
82        let _ = self.with_save(|rc| {
83            rc.ctx.reset_clip();
84            // we DO want to clip the specified region and reset the transformation
85            if let Some(region) = region {
86                rc.transform(rc.current_transform().inverse());
87                rc.clip(region);
88            }
89
90            //prepare the colors etc
91            let rgba = color.as_rgba_u32();
92            rc.ctx.set_source_rgba(
93                byte_to_frac(rgba >> 24),
94                byte_to_frac(rgba >> 16),
95                byte_to_frac(rgba >> 8),
96                byte_to_frac(rgba),
97            );
98            rc.ctx.set_operator(cairo::Operator::Source);
99            rc.ctx.paint().map_err(convert_error)
100        });
101    }
102
103    fn solid_brush(&mut self, color: Color) -> Brush {
104        Brush::Solid(color.as_rgba_u32())
105    }
106
107    fn gradient(&mut self, gradient: impl Into<FixedGradient>) -> Result<Brush, Error> {
108        match gradient.into() {
109            FixedGradient::Linear(linear) => {
110                let (x0, y0) = (linear.start.x, linear.start.y);
111                let (x1, y1) = (linear.end.x, linear.end.y);
112                let lg = cairo::LinearGradient::new(x0, y0, x1, y1);
113                set_gradient_stops!(&lg, &linear.stops);
114                Ok(Brush::Linear(lg))
115            }
116            FixedGradient::Radial(radial) => {
117                let (xc, yc) = (radial.center.x, radial.center.y);
118                let (xo, yo) = (radial.origin_offset.x, radial.origin_offset.y);
119                let r = radial.radius;
120                let rg = cairo::RadialGradient::new(xc + xo, yc + yo, 0.0, xc, yc, r);
121                set_gradient_stops!(&rg, &radial.stops);
122                Ok(Brush::Radial(rg))
123            }
124        }
125    }
126
127    fn fill(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
128        let brush = brush.make_brush(self, || shape.bounding_box());
129        self.set_path(shape);
130        self.set_brush(&brush);
131        self.ctx.set_fill_rule(cairo::FillRule::Winding);
132        self.error = self.ctx.fill();
133    }
134
135    fn fill_even_odd(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
136        let brush = brush.make_brush(self, || shape.bounding_box());
137        self.set_path(shape);
138        self.set_brush(&brush);
139        self.ctx.set_fill_rule(cairo::FillRule::EvenOdd);
140        self.error = self.ctx.fill();
141    }
142
143    fn clip(&mut self, shape: impl Shape) {
144        self.set_path(shape);
145        self.ctx.set_fill_rule(cairo::FillRule::Winding);
146        self.ctx.clip();
147    }
148
149    fn stroke(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>, width: f64) {
150        let brush = brush.make_brush(self, || shape.bounding_box());
151        self.set_path(shape);
152        self.set_stroke(width, None);
153        self.set_brush(&brush);
154        self.error = self.ctx.stroke();
155    }
156
157    fn stroke_styled(
158        &mut self,
159        shape: impl Shape,
160        brush: &impl IntoBrush<Self>,
161        width: f64,
162        style: &StrokeStyle,
163    ) {
164        let brush = brush.make_brush(self, || shape.bounding_box());
165        self.set_path(shape);
166        self.set_stroke(width, Some(style));
167        self.set_brush(&brush);
168        self.error = self.ctx.stroke();
169    }
170
171    fn text(&mut self) -> &mut Self::Text {
172        &mut self.text
173    }
174
175    fn draw_text(&mut self, layout: &Self::TextLayout, pos: impl Into<Point>) {
176        let pos = pos.into();
177        let offset = layout.pango_offset();
178        self.ctx.move_to(pos.x - offset.x, pos.y - offset.y);
179        pangocairo::functions::show_layout(self.ctx, layout.pango_layout());
180    }
181
182    fn save(&mut self) -> Result<(), Error> {
183        self.ctx.save().map_err(convert_error)?;
184        let state = self.transform_stack.last().copied().unwrap_or_default();
185        self.transform_stack.push(state);
186        Ok(())
187    }
188
189    fn restore(&mut self) -> Result<(), Error> {
190        if self.transform_stack.pop().is_some() {
191            // we're defensive about calling restore on the inner context,
192            // because an unbalanced call will trigger a panic in cairo-rs
193            self.ctx.restore().map_err(convert_error)
194        } else {
195            Err(Error::StackUnbalance)
196        }
197    }
198
199    fn finish(&mut self) -> Result<(), Error> {
200        self.ctx.target().flush();
201        Ok(())
202    }
203
204    fn transform(&mut self, transform: Affine) {
205        if let Some(last) = self.transform_stack.last_mut() {
206            *last *= transform;
207        } else {
208            self.transform_stack.push(transform);
209        }
210        self.ctx.transform(affine_to_matrix(transform));
211    }
212
213    fn current_transform(&self) -> Affine {
214        self.transform_stack.last().copied().unwrap_or_default()
215    }
216
217    // allows e.g. raw_data[dst_off + x * 4 + 2] = buf[src_off + x * 4 + 0];
218    #[allow(clippy::identity_op)]
219    fn make_image_with_stride(
220        &mut self,
221        width: usize,
222        height: usize,
223        stride: usize,
224        buf: &[u8],
225        format: ImageFormat,
226    ) -> Result<Self::Image, Error> {
227        let cairo_fmt = match format {
228            ImageFormat::Rgb | ImageFormat::Grayscale => Format::Rgb24,
229            ImageFormat::RgbaSeparate | ImageFormat::RgbaPremul => Format::ARgb32,
230            _ => return Err(Error::NotSupported),
231        };
232        let width_int = width as i32;
233        let height_int = height as i32;
234        let mut image = ImageSurface::create(cairo_fmt, width_int, height_int)
235            .map_err(|e| Error::BackendError(Box::new(e)))?;
236
237        // early-return if the image has no data in it
238        if width_int == 0 || height_int == 0 {
239            return Ok(CairoImage(image));
240        }
241
242        // Confident no borrow errors because we just created it.
243        let image_stride = image.stride() as usize;
244        {
245            if buf.len()
246                < piet::util::expected_image_buffer_size(
247                    width * format.bytes_per_pixel(),
248                    height,
249                    stride,
250                )
251            {
252                return Err(Error::InvalidInput);
253            }
254
255            let mut data = image.data().map_err(|e| Error::BackendError(Box::new(e)))?;
256            for y in 0..height {
257                let src_off = y * stride;
258                let data = &mut data[y * image_stride..];
259                match format {
260                    ImageFormat::Rgb => {
261                        for x in 0..width {
262                            write_rgb(
263                                data,
264                                x,
265                                buf[src_off + x * 3 + 0],
266                                buf[src_off + x * 3 + 1],
267                                buf[src_off + x * 3 + 2],
268                            );
269                        }
270                    }
271                    ImageFormat::RgbaPremul => {
272                        // It's annoying that Cairo exposes only ARGB. Ah well. Let's
273                        // hope that LLVM generates pretty good code for this.
274                        // TODO: consider adding BgraPremul format.
275                        for x in 0..width {
276                            write_rgba(
277                                data,
278                                x,
279                                buf[src_off + x * 4 + 0],
280                                buf[src_off + x * 4 + 1],
281                                buf[src_off + x * 4 + 2],
282                                buf[src_off + x * 4 + 3],
283                            );
284                        }
285                    }
286                    ImageFormat::RgbaSeparate => {
287                        fn premul(x: u8, a: u8) -> u8 {
288                            let y = (x as u16) * (a as u16);
289                            ((y + (y >> 8) + 0x80) >> 8) as u8
290                        }
291                        for x in 0..width {
292                            let a = buf[src_off + x * 4 + 3];
293                            write_rgba(
294                                data,
295                                x,
296                                premul(buf[src_off + x * 4 + 0], a),
297                                premul(buf[src_off + x * 4 + 1], a),
298                                premul(buf[src_off + x * 4 + 2], a),
299                                a,
300                            );
301                        }
302                    }
303                    ImageFormat::Grayscale => {
304                        for x in 0..width {
305                            write_rgb(
306                                data,
307                                x,
308                                buf[src_off + x],
309                                buf[src_off + x],
310                                buf[src_off + x],
311                            );
312                        }
313                    }
314                    _ => return Err(Error::NotSupported),
315                }
316            }
317        }
318        Ok(CairoImage(image))
319    }
320
321    #[inline]
322    fn draw_image(
323        &mut self,
324        image: &Self::Image,
325        dst_rect: impl Into<Rect>,
326        interp: InterpolationMode,
327    ) {
328        self.draw_image_inner(&image.0, None, dst_rect.into(), interp);
329    }
330
331    #[inline]
332    fn draw_image_area(
333        &mut self,
334        image: &Self::Image,
335        src_rect: impl Into<Rect>,
336        dst_rect: impl Into<Rect>,
337        interp: InterpolationMode,
338    ) {
339        self.draw_image_inner(&image.0, Some(src_rect.into()), dst_rect.into(), interp);
340    }
341
342    fn capture_image_area(&mut self, src_rect: impl Into<Rect>) -> Result<Self::Image, Error> {
343        let src_rect: Rect = src_rect.into();
344
345        // In order to capture the correct image area, we first need to convert from
346        // user space (the logical rectangle) to device space (the "physical" rectangle).
347        // For example, in a HiDPI (2x) setting, a user-space rectangle of 20x20 would be
348        // 40x40 in device space.
349        let user_rect = Rectangle::new(
350            src_rect.x0,
351            src_rect.y0,
352            src_rect.width(),
353            src_rect.height(),
354        );
355        let device_rect = self.user_to_device(&user_rect);
356
357        // This is the surface to which we draw the captured image area
358        let target_surface = ImageSurface::create(
359            Format::ARgb32,
360            device_rect.width() as i32,
361            device_rect.height() as i32,
362        )
363        .map_err(convert_error)?;
364        let target_ctx = Context::new(&target_surface).map_err(convert_error)?;
365
366        // Since we (potentially) don't want to capture the entire surface, we crop the
367        // source surface to the requested "sub-surface" using `create_for_rectangle`.
368        let cropped_source_surface = self
369            .ctx
370            .target()
371            .create_for_rectangle(device_rect)
372            .map_err(convert_error)?;
373
374        // Finally, we fill the entirety of the target surface (via the target context)
375        // with the select region of the source surface.
376        target_ctx
377            .set_source_surface(&cropped_source_surface, 0.0, 0.0)
378            .map_err(convert_error)?;
379        target_ctx.rectangle(0.0, 0.0, device_rect.width(), device_rect.height());
380        target_ctx.fill().map_err(convert_error)?;
381
382        Ok(CairoImage(target_surface))
383    }
384
385    fn blurred_rect(&mut self, rect: Rect, blur_radius: f64, brush: &impl IntoBrush<Self>) {
386        let brush = brush.make_brush(self, || rect);
387        match compute_blurred_rect(rect, blur_radius) {
388            Ok((image, origin)) => {
389                self.set_brush(&brush);
390                self.error = self.ctx.mask_surface(&image, origin.x, origin.y);
391            }
392            Err(err) => self.error = Err(err),
393        }
394    }
395}
396
397impl<'a> IntoBrush<CairoRenderContext<'a>> for Brush {
398    fn make_brush<'b>(
399        &'b self,
400        _piet: &mut CairoRenderContext,
401        _bbox: impl FnOnce() -> Rect,
402    ) -> std::borrow::Cow<'b, Brush> {
403        Cow::Borrowed(self)
404    }
405}
406
407impl Image for CairoImage {
408    fn size(&self) -> Size {
409        Size::new(self.0.width().into(), self.0.height().into())
410    }
411}
412
413impl<'a> CairoRenderContext<'a> {
414    /// Create a new Cairo back-end.
415    ///
416    /// At the moment, it uses the "toy text API" for text layout, but when
417    /// we change to a more sophisticated text layout approach, we'll probably
418    /// need a factory for that as an additional argument.
419    pub fn new(ctx: &Context) -> CairoRenderContext<'_> {
420        CairoRenderContext {
421            ctx,
422            text: CairoText::new(),
423            transform_stack: Vec::new(),
424            error: Ok(()),
425        }
426    }
427
428    /// Set the source pattern to the brush.
429    ///
430    /// Cairo is super stateful, and we're trying to have more retained stuff.
431    /// This is part of the impedance matching.
432    fn set_brush(&mut self, brush: &Brush) {
433        match *brush {
434            Brush::Solid(rgba) => self.ctx.set_source_rgba(
435                byte_to_frac(rgba >> 24),
436                byte_to_frac(rgba >> 16),
437                byte_to_frac(rgba >> 8),
438                byte_to_frac(rgba),
439            ),
440            Brush::Linear(ref linear) => self.error = self.ctx.set_source(linear),
441            Brush::Radial(ref radial) => self.error = self.ctx.set_source(radial),
442        }
443    }
444
445    /// Set the stroke parameters.
446    fn set_stroke(&mut self, width: f64, style: Option<&StrokeStyle>) {
447        let default_style = StrokeStyle::default();
448        let style = style.unwrap_or(&default_style);
449
450        self.ctx.set_line_width(width);
451        self.ctx.set_line_join(convert_line_join(style.line_join));
452        self.ctx.set_line_cap(convert_line_cap(style.line_cap));
453
454        if let Some(limit) = style.miter_limit() {
455            self.ctx.set_miter_limit(limit);
456        }
457        self.ctx.set_dash(&style.dash_pattern, style.dash_offset);
458    }
459
460    fn set_path(&mut self, shape: impl Shape) {
461        // This shouldn't be necessary, we always leave the context in no-path
462        // state. But just in case, and it should be harmless.
463        self.ctx.new_path();
464        let mut last = Point::ZERO;
465        for el in shape.path_elements(1e-3) {
466            match el {
467                PathEl::MoveTo(p) => {
468                    self.ctx.move_to(p.x, p.y);
469                    last = p;
470                }
471                PathEl::LineTo(p) => {
472                    self.ctx.line_to(p.x, p.y);
473                    last = p;
474                }
475                PathEl::QuadTo(p1, p2) => {
476                    let q = QuadBez::new(last, p1, p2);
477                    let c = q.raise();
478                    self.ctx
479                        .curve_to(c.p1.x, c.p1.y, c.p2.x, c.p2.y, p2.x, p2.y);
480                    last = p2;
481                }
482                PathEl::CurveTo(p1, p2, p3) => {
483                    self.ctx.curve_to(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
484                    last = p3;
485                }
486                PathEl::ClosePath => self.ctx.close_path(),
487            }
488        }
489    }
490
491    fn draw_image_inner(
492        &mut self,
493        image: &ImageSurface,
494        src_rect: Option<Rect>,
495        dst_rect: Rect,
496        interp: InterpolationMode,
497    ) {
498        let src_rect = match src_rect {
499            Some(src_rect) => src_rect,
500            None => Size::new(image.width() as f64, image.height() as f64).to_rect(),
501        };
502        // Cairo returns an error if we try to paint an empty image, causing us to panic. We check if
503        // either the source or destination is empty, and early-return if so.
504        if src_rect.is_zero_area() || dst_rect.is_zero_area() {
505            return;
506        }
507
508        let _ = self.with_save(|rc| {
509            let surface_pattern = SurfacePattern::create(image);
510            let filter = match interp {
511                InterpolationMode::NearestNeighbor => Filter::Nearest,
512                InterpolationMode::Bilinear => Filter::Bilinear,
513            };
514            surface_pattern.set_filter(filter);
515            let scale_x = dst_rect.width() / src_rect.width();
516            let scale_y = dst_rect.height() / src_rect.height();
517            rc.clip(dst_rect);
518            rc.ctx.translate(
519                dst_rect.x0 - scale_x * src_rect.x0,
520                dst_rect.y0 - scale_y * src_rect.y0,
521            );
522            rc.ctx.scale(scale_x, scale_y);
523            rc.error = rc.ctx.set_source(&surface_pattern);
524            rc.error = rc.ctx.paint();
525            Ok(())
526        });
527    }
528
529    fn user_to_device(&self, user_rect: &Rectangle) -> Rectangle {
530        let (x, y) = self.ctx.user_to_device(user_rect.x(), user_rect.y());
531        let (width, height) = self
532            .ctx
533            .user_to_device(user_rect.width(), user_rect.height());
534
535        Rectangle::new(x, y, width, height)
536    }
537}
538
539fn convert_line_cap(line_cap: LineCap) -> cairo::LineCap {
540    match line_cap {
541        LineCap::Butt => cairo::LineCap::Butt,
542        LineCap::Round => cairo::LineCap::Round,
543        LineCap::Square => cairo::LineCap::Square,
544    }
545}
546
547fn convert_line_join(line_join: LineJoin) -> cairo::LineJoin {
548    match line_join {
549        LineJoin::Miter { .. } => cairo::LineJoin::Miter,
550        LineJoin::Round => cairo::LineJoin::Round,
551        LineJoin::Bevel => cairo::LineJoin::Bevel,
552    }
553}
554
555fn byte_to_frac(byte: u32) -> f64 {
556    ((byte & 255) as f64) * (1.0 / 255.0)
557}
558
559/// Can't implement RoundFrom here because both types belong to other crates.
560fn affine_to_matrix(affine: Affine) -> Matrix {
561    let a = affine.as_coeffs();
562
563    Matrix::new(a[0], a[1], a[2], a[3], a[4], a[5])
564}
565
566fn compute_blurred_rect(rect: Rect, radius: f64) -> Result<(ImageSurface, Point), cairo::Error> {
567    let size = piet::util::size_for_blurred_rect(rect, radius);
568    match ImageSurface::create(Format::A8, size.width as i32, size.height as i32) {
569        Ok(mut image) => {
570            let stride = image.stride() as usize;
571            // An error is returned when either:
572            //      The reference to image is dropped (it isnt since its still in scope),
573            //      There is an error on image (there isnt since we havnt used it yet),
574            //      The pointer to the image is null aka the surface isnt an imagesurface (it is an imagesurface),
575            //      Or the surface is finished (it isnt, we know because we dont finish it).
576            // Since we know none of these cases should happen, we know that this should not panic.
577            let mut data = image.data().unwrap();
578            let rect_exp = piet::util::compute_blurred_rect(rect, radius, stride, &mut data);
579            std::mem::drop(data);
580            let origin = rect_exp.origin();
581            Ok((image, origin))
582        }
583        Err(err) => Err(err),
584    }
585}
586
587fn convert_error(err: cairo::Error) -> Error {
588    Error::BackendError(err.into())
589}
590
591fn write_rgba(data: &mut [u8], column: usize, r: u8, g: u8, b: u8, a: u8) {
592    // From the cairo docs for CAIRO_FORMAT_ARGB32:
593    // > each pixel is a 32-bit quantity, with alpha in the upper 8 bits, then red,
594    // > then green, then blue. The 32-bit quantities are stored native-endian.
595    let (a, r, g, b) = (u32::from(a), u32::from(r), u32::from(g), u32::from(b));
596    let pixel = a << 24 | r << 16 | g << 8 | b;
597
598    data[4 * column..4 * (column + 1)].copy_from_slice(&pixel.to_ne_bytes());
599}
600
601fn write_rgb(data: &mut [u8], column: usize, r: u8, g: u8, b: u8) {
602    // From the cairo docs for CAIRO_FORMAT_RGB24:
603    //  each pixel is a 32-bit quantity, with the upper 8 bits unused.
604    write_rgba(data, column, r, g, b, 0);
605}