piet_coregraphics/
lib.rs

1// Copyright 2020 the Piet Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! The CoreGraphics backend for the Piet 2D graphics abstraction.
5
6#![deny(clippy::trivially_copy_pass_by_ref)]
7
8mod ct_helpers;
9mod gradient;
10mod text;
11
12use std::borrow::Cow;
13use std::sync::Arc;
14
15use core_graphics::base::{
16    kCGImageAlphaLast, kCGImageAlphaPremultipliedLast, kCGRenderingIntentDefault, CGFloat,
17};
18use core_graphics::color_space::CGColorSpace;
19use core_graphics::context::{CGContextRef, CGInterpolationQuality, CGLineCap, CGLineJoin};
20use core_graphics::data_provider::CGDataProvider;
21use core_graphics::geometry::{CGAffineTransform, CGPoint, CGRect, CGSize};
22use core_graphics::gradient::CGGradientDrawingOptions;
23use core_graphics::image::CGImage;
24
25use piet::kurbo::{Affine, PathEl, Point, QuadBez, Rect, Shape, Size};
26
27use piet::{
28    Color, Error, FixedGradient, Image, ImageFormat, InterpolationMode, IntoBrush, LineCap,
29    LineJoin, RenderContext, RoundInto, StrokeStyle,
30};
31
32pub use crate::text::{CoreGraphicsText, CoreGraphicsTextLayout, CoreGraphicsTextLayoutBuilder};
33
34use gradient::Gradient;
35
36// getting this to be a const takes some gymnastics
37const GRADIENT_DRAW_BEFORE_AND_AFTER: CGGradientDrawingOptions =
38    CGGradientDrawingOptions::from_bits_truncate(
39        CGGradientDrawingOptions::CGGradientDrawsAfterEndLocation.bits()
40            | CGGradientDrawingOptions::CGGradientDrawsBeforeStartLocation.bits(),
41    );
42
43pub struct CoreGraphicsContext<'a> {
44    // Cairo has this as Clone and with &self methods, but we do this to avoid
45    // concurrency problems.
46    ctx: &'a mut CGContextRef,
47    text: CoreGraphicsText,
48    // because of the relationship between cocoa and coregraphics (where cocoa
49    // may be asked to flip the y-axis) we cannot trust the transform returned
50    // by CTContextGetCTM. Instead we maintain our own stack, which will contain
51    // only those transforms applied by us.
52    transform_stack: Vec<Affine>,
53    y_down: bool,
54    height: f64,
55}
56
57impl<'a> CoreGraphicsContext<'a> {
58    /// Create a new context with the y-origin at the top-left corner.
59    ///
60    /// This is not the default for CoreGraphics; but it is the default for piet.
61    /// To map between the two coordinate spaces you must also pass an explicit
62    /// height argument.
63    ///
64    /// The optional `text` argument can be a reusable `CoreGraphicsText` struct;
65    /// a new one will be constructed if `None` is passed.
66    pub fn new_y_up(
67        ctx: &mut CGContextRef,
68        height: f64,
69        text: Option<CoreGraphicsText>,
70    ) -> CoreGraphicsContext {
71        Self::new_impl(ctx, Some(height), text, false)
72    }
73
74    /// Create a new context with the y-origin at the bottom right corner.
75    ///
76    /// This is the default for core graphics, but not for piet.
77    ///
78    /// The optional `text` argument can be a reusable `CoreGraphicsText` struct;
79    /// a new one will be constructed if `None` is passed.
80    pub fn new_y_down(
81        ctx: &mut CGContextRef,
82        text: Option<CoreGraphicsText>,
83    ) -> CoreGraphicsContext {
84        Self::new_impl(ctx, None, text, true)
85    }
86
87    fn new_impl(
88        ctx: &mut CGContextRef,
89        height: Option<f64>,
90        text: Option<CoreGraphicsText>,
91        y_down: bool,
92    ) -> CoreGraphicsContext {
93        ctx.save();
94        if let Some(height) = height {
95            let xform = Affine::FLIP_Y * Affine::translate((0.0, -height));
96            ctx.concat_ctm(to_cgaffine(xform));
97        }
98        let text = text.unwrap_or_else(CoreGraphicsText::new_with_unique_state);
99
100        CoreGraphicsContext {
101            ctx,
102            text,
103            transform_stack: Vec::new(),
104            y_down,
105            height: height.unwrap_or_default(),
106        }
107    }
108}
109
110impl<'a> Drop for CoreGraphicsContext<'a> {
111    fn drop(&mut self) {
112        self.ctx.restore();
113    }
114}
115
116#[derive(Clone)]
117pub enum Brush {
118    Solid(Color),
119    Gradient(Gradient),
120}
121
122/// A core-graphics image
123#[derive(Clone)]
124pub enum CoreGraphicsImage {
125    /// Empty images are not supported for core-graphics, so we need a variant here to handle that
126    /// case.
127    Empty,
128    YUp(CGImage),
129    YDown(CGImage),
130}
131
132impl CoreGraphicsImage {
133    fn from_cgimage_and_ydir(image: CGImage, y_down: bool) -> Self {
134        match y_down {
135            true => CoreGraphicsImage::YDown(image),
136            false => CoreGraphicsImage::YUp(image),
137        }
138    }
139    pub fn as_cgimage(&self) -> Option<&CGImage> {
140        match self {
141            CoreGraphicsImage::Empty => None,
142            CoreGraphicsImage::YUp(image) | CoreGraphicsImage::YDown(image) => Some(image),
143        }
144    }
145}
146
147impl<'a> RenderContext for CoreGraphicsContext<'a> {
148    type Brush = Brush;
149    type Text = CoreGraphicsText;
150    type TextLayout = CoreGraphicsTextLayout;
151    type Image = CoreGraphicsImage;
152
153    fn clear(&mut self, region: impl Into<Option<Rect>>, color: Color) {
154        // save cannot fail
155        let _ = self.save();
156        // remove any existing clip
157        self.ctx.reset_clip();
158        // remove the current transform
159        let current_xform = self.current_transform();
160        let xform = current_xform.inverse();
161        self.transform(xform);
162
163        let region = region
164            .into()
165            .map(to_cgrect)
166            .unwrap_or_else(|| self.ctx.clip_bounding_box());
167        let (r, g, b, a) = color.as_rgba();
168        self.ctx
169            .set_blend_mode(core_graphics::context::CGBlendMode::Copy);
170        self.ctx.set_rgb_fill_color(r, g, b, a);
171        self.ctx.fill_rect(region);
172        // restore cannot fail, because we saved at the start of the method
173        self.restore().unwrap();
174    }
175
176    fn solid_brush(&mut self, color: Color) -> Brush {
177        Brush::Solid(color)
178    }
179
180    fn gradient(&mut self, gradient: impl Into<FixedGradient>) -> Result<Brush, Error> {
181        let gradient = Gradient::from_piet_gradient(gradient.into());
182        Ok(Brush::Gradient(gradient))
183    }
184
185    /// Fill a shape.
186    fn fill(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
187        let brush = brush.make_brush(self, || shape.bounding_box());
188        self.set_path(shape);
189        match brush.as_ref() {
190            Brush::Solid(color) => {
191                self.set_fill_color(*color);
192                self.ctx.fill_path();
193            }
194            Brush::Gradient(grad) => {
195                self.ctx.save();
196                self.ctx.clip();
197                grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER);
198                self.ctx.restore();
199            }
200        }
201    }
202
203    fn fill_even_odd(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
204        let brush = brush.make_brush(self, || shape.bounding_box());
205        self.set_path(shape);
206        match brush.as_ref() {
207            Brush::Solid(color) => {
208                self.set_fill_color(*color);
209                self.ctx.eo_fill_path();
210            }
211            Brush::Gradient(grad) => {
212                self.ctx.save();
213                self.ctx.eo_clip();
214                grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER);
215                self.ctx.restore();
216            }
217        }
218    }
219
220    fn clip(&mut self, shape: impl Shape) {
221        self.set_path(shape);
222        self.ctx.clip();
223    }
224
225    fn stroke(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>, width: f64) {
226        let brush = brush.make_brush(self, || shape.bounding_box());
227        self.set_path(shape);
228        self.set_stroke(width.round_into(), None);
229        match brush.as_ref() {
230            Brush::Solid(color) => {
231                self.set_stroke_color(*color);
232                self.ctx.stroke_path();
233            }
234            Brush::Gradient(grad) => {
235                self.ctx.save();
236                self.ctx.replace_path_with_stroked_path();
237                self.ctx.clip();
238                grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER);
239                self.ctx.restore();
240            }
241        }
242    }
243
244    fn stroke_styled(
245        &mut self,
246        shape: impl Shape,
247        brush: &impl IntoBrush<Self>,
248        width: f64,
249        style: &StrokeStyle,
250    ) {
251        let brush = brush.make_brush(self, || shape.bounding_box());
252        self.set_path(shape);
253        self.set_stroke(width.round_into(), Some(style));
254        match brush.as_ref() {
255            Brush::Solid(color) => {
256                self.set_stroke_color(*color);
257                self.ctx.stroke_path();
258            }
259            Brush::Gradient(grad) => {
260                self.ctx.save();
261                self.ctx.replace_path_with_stroked_path();
262                self.ctx.clip();
263                grad.fill(self.ctx, GRADIENT_DRAW_BEFORE_AND_AFTER);
264                self.ctx.restore();
265            }
266        }
267    }
268
269    fn text(&mut self) -> &mut Self::Text {
270        &mut self.text
271    }
272
273    fn draw_text(&mut self, layout: &Self::TextLayout, pos: impl Into<Point>) {
274        let pos = pos.into();
275        self.ctx.save();
276        // inverted coordinate system; text is drawn from bottom left corner,
277        // and (0, 0) in context is also bottom left.
278        self.ctx.translate(pos.x, layout.frame_size.height + pos.y);
279        self.ctx.scale(1.0, -1.0);
280        layout.draw(self.ctx);
281        self.ctx.restore();
282    }
283
284    fn save(&mut self) -> Result<(), Error> {
285        self.ctx.save();
286        let state = self.transform_stack.last().copied().unwrap_or_default();
287        self.transform_stack.push(state);
288        Ok(())
289    }
290
291    fn restore(&mut self) -> Result<(), Error> {
292        if self.transform_stack.pop().is_some() {
293            // we're defensive about calling restore on the inner context,
294            // because an unbalanced call will trigger an assert in C
295            self.ctx.restore();
296            Ok(())
297        } else {
298            Err(Error::StackUnbalance)
299        }
300    }
301
302    fn finish(&mut self) -> Result<(), Error> {
303        Ok(())
304    }
305
306    fn transform(&mut self, transform: Affine) {
307        if let Some(last) = self.transform_stack.last_mut() {
308            *last *= transform;
309        } else {
310            self.transform_stack.push(transform);
311        }
312        self.ctx.concat_ctm(to_cgaffine(transform));
313    }
314
315    fn make_image_with_stride(
316        &mut self,
317        width: usize,
318        height: usize,
319        stride: usize,
320        buf: &[u8],
321        format: ImageFormat,
322    ) -> Result<Self::Image, Error> {
323        if width == 0 || height == 0 {
324            return Ok(CoreGraphicsImage::Empty);
325        }
326        assert!(!buf.is_empty() && buf.len() <= format.bytes_per_pixel() * width * height);
327        let data = Arc::new(piet::util::image_buffer_to_tightly_packed(
328            buf, width, height, stride, format,
329        )?);
330        let data_provider = CGDataProvider::from_buffer(data);
331        let (colorspace, bitmap_info, bytes) = match format {
332            ImageFormat::Rgb => (CGColorSpace::create_device_rgb(), 0, 3),
333            ImageFormat::RgbaPremul => (
334                CGColorSpace::create_device_rgb(),
335                kCGImageAlphaPremultipliedLast,
336                4,
337            ),
338            ImageFormat::RgbaSeparate => (CGColorSpace::create_device_rgb(), kCGImageAlphaLast, 4),
339            ImageFormat::Grayscale => (CGColorSpace::create_device_gray(), 0, 1),
340            _ => unimplemented!(),
341        };
342        let bits_per_component = 8;
343        // this doesn't matter, we set interpolation mode manually in draw_image
344        let should_interpolate = false;
345        let rendering_intent = kCGRenderingIntentDefault;
346        let image = CGImage::new(
347            width,
348            height,
349            bits_per_component,
350            bytes * bits_per_component,
351            width * bytes,
352            &colorspace,
353            bitmap_info,
354            &data_provider,
355            should_interpolate,
356            rendering_intent,
357        );
358
359        Ok(CoreGraphicsImage::from_cgimage_and_ydir(image, self.y_down))
360    }
361
362    fn draw_image(
363        &mut self,
364        src_image: &Self::Image,
365        rect: impl Into<Rect>,
366        interp: InterpolationMode,
367    ) {
368        let image_y_down: bool;
369        let image = match src_image {
370            CoreGraphicsImage::YDown(img) => {
371                image_y_down = true;
372                img
373            }
374            CoreGraphicsImage::YUp(img) => {
375                image_y_down = false;
376                img
377            }
378            CoreGraphicsImage::Empty => return,
379        };
380
381        self.ctx.save();
382        //https://developer.apple.com/documentation/coregraphics/cginterpolationquality?language=objc
383        let quality = match interp {
384            InterpolationMode::NearestNeighbor => {
385                CGInterpolationQuality::CGInterpolationQualityNone
386            }
387            InterpolationMode::Bilinear => CGInterpolationQuality::CGInterpolationQualityDefault,
388        };
389        self.ctx.set_interpolation_quality(quality);
390        let rect = rect.into();
391
392        if self.y_down && !image_y_down {
393            // The CGImage does not need to be inverted, draw it directly to the context.
394            self.ctx.draw_image(to_cgrect(rect), image);
395        } else {
396            // The CGImage needs to be flipped, which we do by translating the drawing rect to be
397            // centered around the origin before inverting the context.
398            self.ctx.translate(rect.min_x(), rect.max_y());
399            self.ctx.scale(1.0, -1.0);
400            self.ctx
401                .draw_image(to_cgrect(rect.with_origin(Point::ZERO)), image);
402        }
403
404        self.ctx.restore();
405    }
406
407    fn draw_image_area(
408        &mut self,
409        image: &Self::Image,
410        src_rect: impl Into<Rect>,
411        dst_rect: impl Into<Rect>,
412        interp: InterpolationMode,
413    ) {
414        if let CoreGraphicsImage::YDown(image) = image {
415            if let Some(cropped) = image.cropped(to_cgrect(src_rect)) {
416                self.draw_image(&CoreGraphicsImage::YDown(cropped), dst_rect, interp);
417            }
418        } else if let CoreGraphicsImage::YUp(image) = image {
419            if let Some(cropped) = image.cropped(to_cgrect(src_rect)) {
420                self.draw_image(&CoreGraphicsImage::YUp(cropped), dst_rect, interp);
421            }
422        }
423    }
424
425    fn capture_image_area(&mut self, src_rect: impl Into<Rect>) -> Result<Self::Image, Error> {
426        let src_rect = src_rect.into();
427
428        // When creating a CoreGraphicsContext, a transformation matrix is applied to map
429        // between piet's coordinate system and CoreGraphic's coordinate system
430        // (see [`CoreGraphicsContext::new_impl`] for details). Since the `src_rect` we receive
431        // as parameter is in piet's coordinate system, we need to first convert it to the CG one,
432        // as otherwise our captured image area would be wrong.
433        let src_cgrect = if self.y_down {
434            // If the active context is y-down (Piet's default) then we can use the context's
435            // transformation matrix directly.
436            let matrix = self.ctx.get_ctm();
437            to_cgrect(src_rect).apply_transform(&matrix)
438        } else {
439            // Otherwise the active context is y-up (macOS default in coregraphics), and we need to
440            // temporarily translate and flip the context to capture the correct area.
441            let y_dir_adjusted_src_rect = Rect::new(
442                src_rect.x0,
443                self.height - src_rect.y0,
444                src_rect.x1,
445                self.height - src_rect.y1,
446            );
447            let matrix = self.ctx.get_ctm();
448            to_cgrect(y_dir_adjusted_src_rect).apply_transform(&matrix)
449        };
450
451        if src_cgrect.size.width < 1.0 || src_cgrect.size.height < 1.0 {
452            return Err(Error::InvalidInput);
453        }
454
455        if src_cgrect.size.width > self.ctx.width() as f64
456            || src_cgrect.size.height > self.ctx.height() as f64
457        {
458            return Err(Error::InvalidInput);
459        }
460
461        let full_image = self.ctx.create_image().ok_or(Error::InvalidInput)?;
462
463        if src_cgrect.size.width.round() as usize == self.ctx.width()
464            && src_cgrect.size.height.round() as usize == self.ctx.height()
465        {
466            return Ok(CoreGraphicsImage::from_cgimage_and_ydir(
467                full_image,
468                self.y_down,
469            ));
470        }
471
472        let cropped_image_result = full_image.cropped(src_cgrect);
473        if let Some(image) = cropped_image_result {
474            // CGImage::cropped calls CGImageCreateWithImageInRect to set the bounds of the image,
475            // but it does not affect the underlying image data. This causes issues when using the
476            // captured images if the image's width does not match the original context's row size.
477            // To fix this, we create a new image-sized bitmap context, paint the image to it, and
478            // then re-capture. This forces coregraphics to resize the image to its specified bounds.
479            let cropped_image_size = Size::new(src_cgrect.size.width, src_cgrect.size.height);
480            let cropped_image_rect = Rect::from_origin_size(Point::ZERO, cropped_image_size);
481            let cropped_image_context = core_graphics::context::CGContext::create_bitmap_context(
482                None,
483                cropped_image_size.width as usize,
484                cropped_image_size.height as usize,
485                8,
486                0,
487                &core_graphics::color_space::CGColorSpace::create_device_rgb(),
488                core_graphics::base::kCGImageAlphaPremultipliedLast,
489            );
490            cropped_image_context.draw_image(to_cgrect(cropped_image_rect), &image);
491            let cropped_image = cropped_image_context
492                .create_image()
493                .expect("Failed to capture cropped image from resize context");
494
495            Ok(CoreGraphicsImage::from_cgimage_and_ydir(
496                cropped_image,
497                self.y_down,
498            ))
499        } else {
500            Err(Error::InvalidInput)
501        }
502    }
503
504    fn blurred_rect(&mut self, rect: Rect, blur_radius: f64, brush: &impl IntoBrush<Self>) {
505        let (image, rect) = compute_blurred_rect(rect, blur_radius);
506        let cg_rect = to_cgrect(rect);
507        self.ctx.save();
508        self.ctx.clip_to_mask(cg_rect, &image);
509        self.fill(rect, brush);
510        self.ctx.restore()
511    }
512
513    fn current_transform(&self) -> Affine {
514        self.transform_stack.last().copied().unwrap_or_default()
515    }
516
517    fn status(&mut self) -> Result<(), Error> {
518        Ok(())
519    }
520}
521
522impl<'a> IntoBrush<CoreGraphicsContext<'a>> for Brush {
523    fn make_brush<'b>(
524        &'b self,
525        _piet: &mut CoreGraphicsContext,
526        _bbox: impl FnOnce() -> Rect,
527    ) -> std::borrow::Cow<'b, Brush> {
528        Cow::Borrowed(self)
529    }
530}
531
532impl Image for CoreGraphicsImage {
533    fn size(&self) -> Size {
534        // `size_t` (which could be 64 bits wide) does not losslessly convert to `f64`. In
535        // reality, the image you're working with would have to be pretty big to be an issue, and
536        // the issue would only be accuracy of the size.
537        match self {
538            CoreGraphicsImage::Empty => Size::new(0., 0.),
539            CoreGraphicsImage::YDown(image) | CoreGraphicsImage::YUp(image) => {
540                Size::new(image.width() as f64, image.height() as f64)
541            }
542        }
543    }
544}
545
546fn convert_line_join(line_join: LineJoin) -> CGLineJoin {
547    match line_join {
548        LineJoin::Miter { .. } => CGLineJoin::CGLineJoinMiter,
549        LineJoin::Round => CGLineJoin::CGLineJoinRound,
550        LineJoin::Bevel => CGLineJoin::CGLineJoinBevel,
551    }
552}
553
554fn convert_line_cap(line_cap: LineCap) -> CGLineCap {
555    match line_cap {
556        LineCap::Butt => CGLineCap::CGLineCapButt,
557        LineCap::Round => CGLineCap::CGLineCapRound,
558        LineCap::Square => CGLineCap::CGLineCapSquare,
559    }
560}
561
562impl<'a> CoreGraphicsContext<'a> {
563    fn set_fill_color(&mut self, color: Color) {
564        let (r, g, b, a) = Color::as_rgba(color);
565        self.ctx.set_rgb_fill_color(r, g, b, a);
566    }
567
568    fn set_stroke_color(&mut self, color: Color) {
569        let (r, g, b, a) = Color::as_rgba(color);
570        self.ctx.set_rgb_stroke_color(r, g, b, a);
571    }
572
573    /// Set the stroke parameters.
574    fn set_stroke(&mut self, width: f64, style: Option<&StrokeStyle>) {
575        let default_style = StrokeStyle::default();
576        let style = style.unwrap_or(&default_style);
577        self.ctx.set_line_width(width);
578
579        self.ctx.set_line_join(convert_line_join(style.line_join));
580        self.ctx.set_line_cap(convert_line_cap(style.line_cap));
581
582        if let Some(limit) = style.miter_limit() {
583            self.ctx.set_miter_limit(limit);
584        }
585
586        self.ctx
587            .set_line_dash(style.dash_offset, &style.dash_pattern);
588    }
589
590    fn set_path(&mut self, shape: impl Shape) {
591        // This shouldn't be necessary, we always leave the context in no-path
592        // state. But just in case, and it should be harmless.
593        self.ctx.begin_path();
594        let mut last = Point::default();
595        for el in shape.path_elements(1e-3) {
596            match el {
597                PathEl::MoveTo(p) => {
598                    self.ctx.move_to_point(p.x, p.y);
599                    last = p;
600                }
601                PathEl::LineTo(p) => {
602                    self.ctx.add_line_to_point(p.x, p.y);
603                    last = p;
604                }
605                PathEl::QuadTo(p1, p2) => {
606                    let q = QuadBez::new(last, p1, p2);
607                    let c = q.raise();
608                    self.ctx
609                        .add_curve_to_point(c.p1.x, c.p1.y, c.p2.x, c.p2.y, p2.x, p2.y);
610                    last = p2;
611                }
612                PathEl::CurveTo(p1, p2, p3) => {
613                    self.ctx
614                        .add_curve_to_point(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
615                    last = p3;
616                }
617                PathEl::ClosePath => self.ctx.close_path(),
618            }
619        }
620    }
621}
622
623fn compute_blurred_rect(rect: Rect, radius: f64) -> (CGImage, Rect) {
624    let size = piet::util::size_for_blurred_rect(rect, radius);
625    let width = size.width as usize;
626    let height = size.height as usize;
627
628    let mut data = vec![0u8; width * height];
629    let rect_exp = piet::util::compute_blurred_rect(rect, radius, width, &mut data);
630
631    let data_provider = CGDataProvider::from_buffer(Arc::new(data));
632    let color_space = CGColorSpace::create_device_gray();
633    let image = CGImage::new(
634        width,
635        height,
636        8,
637        8,
638        width,
639        &color_space,
640        0,
641        &data_provider,
642        false,
643        0,
644    );
645    (image, rect_exp)
646}
647
648fn to_cgpoint(point: Point) -> CGPoint {
649    CGPoint::new(point.x as CGFloat, point.y as CGFloat)
650}
651
652fn to_cgsize(size: Size) -> CGSize {
653    CGSize::new(size.width, size.height)
654}
655
656fn to_cgrect(rect: impl Into<Rect>) -> CGRect {
657    let rect = rect.into();
658    CGRect::new(&to_cgpoint(rect.origin()), &to_cgsize(rect.size()))
659}
660
661fn to_cgaffine(affine: Affine) -> CGAffineTransform {
662    let [a, b, c, d, tx, ty] = affine.as_coeffs();
663    CGAffineTransform::new(a, b, c, d, tx, ty)
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use core_graphics::color_space::CGColorSpace;
670    use core_graphics::context::CGContext;
671
672    fn make_context(size: impl Into<Size>) -> CGContext {
673        let size = size.into();
674        CGContext::create_bitmap_context(
675            None,
676            size.width as usize,
677            size.height as usize,
678            8,
679            0,
680            &CGColorSpace::create_device_rgb(),
681            core_graphics::base::kCGImageAlphaPremultipliedLast,
682        )
683    }
684
685    fn equalish_affine(one: Affine, two: Affine) -> bool {
686        one.as_coeffs()
687            .iter()
688            .zip(two.as_coeffs().iter())
689            .all(|(a, b)| (a - b).abs() < f64::EPSILON)
690    }
691
692    macro_rules! assert_affine_eq {
693        ($left:expr, $right:expr) => {{
694            if !equalish_affine($left, $right) {
695                panic!(
696                    "assertion failed: `(one == two)`\n\
697                one: {:?}\n\
698                two: {:?}",
699                    $left.as_coeffs(),
700                    $right.as_coeffs()
701                )
702            }
703        }};
704    }
705
706    #[test]
707    fn get_affine_y_up() {
708        let mut ctx = make_context((400.0, 400.0));
709        let mut piet = CoreGraphicsContext::new_y_up(&mut ctx, 400.0, None);
710        let affine = piet.current_transform();
711        assert_affine_eq!(affine, Affine::default());
712
713        let one = Affine::translate((50.0, 20.0));
714        let two = Affine::rotate(2.2);
715        let three = Affine::FLIP_Y;
716        let four = Affine::scale_non_uniform(2.0, -1.5);
717
718        piet.save().unwrap();
719        piet.transform(one);
720        piet.transform(one);
721        piet.save().unwrap();
722        piet.transform(two);
723        piet.save().unwrap();
724        piet.transform(three);
725        assert_affine_eq!(piet.current_transform(), one * one * two * three);
726        piet.transform(four);
727        piet.save().unwrap();
728
729        assert_affine_eq!(piet.current_transform(), one * one * two * three * four);
730        piet.restore().unwrap();
731        assert_affine_eq!(piet.current_transform(), one * one * two * three * four);
732        piet.restore().unwrap();
733        assert_affine_eq!(piet.current_transform(), one * one * two);
734        piet.restore().unwrap();
735        assert_affine_eq!(piet.current_transform(), one * one);
736        piet.restore().unwrap();
737        assert_affine_eq!(piet.current_transform(), Affine::default());
738    }
739
740    #[test]
741    fn capture_image_area() {
742        let mut ctx = make_context((400.0, 400.0));
743        let mut piet = CoreGraphicsContext::new_y_down(&mut ctx, None);
744
745        assert!(piet
746            .capture_image_area(Rect::new(0.0, 0.0, 0.0, 0.0))
747            .is_err());
748        assert!(piet
749            .capture_image_area(Rect::new(0.0, 0.0, 500.0, 400.0))
750            .is_err());
751        assert!(piet
752            .capture_image_area(Rect::new(100.0, 100.0, 200.0, 200.0))
753            .is_ok());
754
755        let copy = piet
756            .capture_image_area(Rect::new(100.0, 100.0, 200.0, 200.0))
757            .unwrap();
758
759        let unwrapped_copy = copy.as_cgimage().unwrap();
760        let rewrapped_copy = CoreGraphicsImage::from_cgimage_and_ydir(unwrapped_copy.clone(), true);
761
762        piet.draw_image(
763            &rewrapped_copy,
764            Rect::new(0.0, 0.0, 400.0, 400.0),
765            InterpolationMode::Bilinear,
766        );
767    }
768}