Skip to main content

apple_cf/cg/
context.rs

1//! `CGContext` — offscreen bitmap drawing with Core Graphics.
2
3use core::ffi::c_void;
4use core::ptr;
5
6use crate::NullPointerError;
7
8use super::{CGAffineTransform, CGRect};
9use super::drawing::{CGColorSpace, CGImage};
10use super::ffi;
11
12const BITS_PER_COMPONENT_8: usize = 8;
13const CG_IMAGE_ALPHA_NONE: u32 = 0;
14const CG_IMAGE_ALPHA_ONLY: u32 = 7;
15const CG_IMAGE_ALPHA_PREMULTIPLIED_LAST: u32 = 1;
16const CG_BITMAP_BYTE_ORDER_32_BIG: u32 = 16_384;
17const RGBA8_BITMAP_INFO: u32 = CG_IMAGE_ALPHA_PREMULTIPLIED_LAST | CG_BITMAP_BYTE_ORDER_32_BIG;
18const GRAYSCALE8_BITMAP_INFO: u32 = CG_IMAGE_ALPHA_NONE;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21#[repr(u32)]
22pub enum LineCap {
23    Butt = 0,
24    Round = 1,
25    Square = 2,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29#[repr(u32)]
30pub enum LineJoin {
31    Miter = 0,
32    Round = 1,
33    Bevel = 2,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37#[repr(u32)]
38pub enum PathDrawingMode {
39    Fill = 0,
40    EvenOddFill = 1,
41    Stroke = 2,
42    FillStroke = 3,
43    EvenOddFillStroke = 4,
44}
45
46/// Reference-counted `CGContextRef` backed by a bitmap.
47///
48/// Clones retain the same mutable native context; they are not independent
49/// drawing surfaces.
50#[derive(Debug)]
51pub struct CGContext {
52    ptr: *mut c_void,
53}
54
55crate::utils::retained::cf_retained!(
56    CGContext,
57    field = ptr,
58    retain = ffi::CGContextRetain,
59    release = ffi::CGContextRelease,
60    drop = null_out,
61);
62
63impl CGContext {
64    /// Wrap a raw `CGContextRef` pointer — takes ownership without retaining.
65    ///
66    /// # Safety
67    ///
68    /// `ptr` must be a non-null `CGContextRef` whose ownership the caller is
69    /// transferring to the returned [`CGContext`].
70    #[must_use]
71    pub const unsafe fn from_raw(ptr: *mut c_void) -> Self {
72        Self { ptr }
73    }
74
75    fn new_bitmap(
76        width: usize,
77        height: usize,
78        bytes_per_row: usize,
79        color_space: &CGColorSpace,
80        bitmap_info: u32,
81    ) -> Result<Self, NullPointerError> {
82        let context = unsafe {
83            if bytes_per_row == 0 {
84                ffi::CGBitmapContextCreate(
85                    ptr::null_mut(),
86                    width,
87                    height,
88                    BITS_PER_COMPONENT_8,
89                    0,
90                    color_space.as_ptr(),
91                    bitmap_info,
92                )
93            } else {
94                crate::ffi::acf_cg_context_create_bitmap(
95                    width,
96                    height,
97                    bytes_per_row,
98                    color_space.as_ptr(),
99                    bitmap_info,
100                )
101            }
102        };
103
104        if context.is_null() {
105            Err(NullPointerError::new("CGBitmapContextCreate"))
106        } else {
107            let context = Self { ptr: context };
108            let data = context.data();
109            let len = context.buffer_len();
110            if !data.is_null() && len != 0 {
111                unsafe { data.write_bytes(0, len) };
112            }
113            Ok(context)
114        }
115    }
116
117    /// Create a premultiplied-last RGBA8 bitmap context.
118    ///
119    /// # Errors
120    ///
121    /// Returns [`NullPointerError`] if Core Graphics fails to create the bitmap context.
122    pub fn new_rgba8(width: usize, height: usize) -> Result<Self, NullPointerError> {
123        let color_space = CGColorSpace::device_rgb();
124        Self::new_bitmap(width, height, 0, &color_space, RGBA8_BITMAP_INFO)
125    }
126
127    #[allow(clippy::missing_errors_doc)]
128    pub fn new_srgb_rgba8(width: usize, height: usize) -> Result<Self, NullPointerError> {
129        let bytes_per_row = width
130            .checked_mul(4)
131            .ok_or_else(|| NullPointerError::new("CGBitmapContextCreate"))?;
132        let color_space = CGColorSpace::srgb();
133        Self::new_bitmap(width, height, bytes_per_row, &color_space, RGBA8_BITMAP_INFO)
134    }
135
136    #[must_use]
137    #[allow(clippy::missing_safety_doc)]
138    pub unsafe fn copy_bytes(&self) -> Vec<u8> {
139        unsafe { self.as_bytes() }.to_vec()
140    }
141
142    /// Create an 8-bit grayscale bitmap context.
143    ///
144    /// # Errors
145    ///
146    /// Returns [`NullPointerError`] if Core Graphics fails to create the bitmap context.
147    pub fn new_grayscale(width: usize, height: usize) -> Result<Self, NullPointerError> {
148        let color_space = CGColorSpace::device_gray();
149        Self::new_bitmap(width, height, 0, &color_space, GRAYSCALE8_BITMAP_INFO)
150    }
151
152    #[allow(clippy::missing_errors_doc)]
153    pub fn new_alpha_only(width: usize, height: usize) -> Result<Self, NullPointerError> {
154        let color_space = CGColorSpace::device_gray();
155        Self::new_bitmap(width, height, width, &color_space, CG_IMAGE_ALPHA_ONLY)
156    }
157
158    #[allow(clippy::missing_errors_doc)]
159    pub fn new_premultiplied_last(width: usize, height: usize) -> Result<Self, NullPointerError> {
160        let bytes_per_row = width
161            .checked_mul(4)
162            .ok_or_else(|| NullPointerError::new("CGBitmapContextCreate"))?;
163        let color_space = CGColorSpace::device_rgb();
164        Self::new_bitmap(
165            width,
166            height,
167            bytes_per_row,
168            &color_space,
169            CG_IMAGE_ALPHA_PREMULTIPLIED_LAST,
170        )
171    }
172
173    fn buffer_len(&self) -> usize {
174        self.height().checked_mul(self.bytes_per_row()).unwrap_or(0)
175    }
176
177    /// Width in pixels.
178    #[must_use]
179    pub fn width(&self) -> usize {
180        unsafe { ffi::CGBitmapContextGetWidth(self.ptr) }
181    }
182
183    /// Height in pixels.
184    #[must_use]
185    pub fn height(&self) -> usize {
186        unsafe { ffi::CGBitmapContextGetHeight(self.ptr) }
187    }
188
189    /// Bytes per row.
190    #[must_use]
191    pub fn bytes_per_row(&self) -> usize {
192        unsafe { ffi::CGBitmapContextGetBytesPerRow(self.ptr) }
193    }
194
195    /// Bits per component.
196    #[must_use]
197    pub fn bits_per_component(&self) -> usize {
198        unsafe { ffi::CGBitmapContextGetBitsPerComponent(self.ptr) }
199    }
200
201    /// Bits per pixel.
202    #[must_use]
203    pub fn bits_per_pixel(&self) -> usize {
204        unsafe { ffi::CGBitmapContextGetBitsPerPixel(self.ptr) }
205    }
206
207    /// Raw bitmap data pointer.
208    ///
209    /// Dereferencing the pointer is unsafe because retained aliases and native
210    /// drawing operations can mutate the same storage.
211    #[must_use]
212    pub fn data(&self) -> *mut u8 {
213        unsafe { ffi::CGBitmapContextGetData(self.ptr).cast::<u8>() }
214    }
215
216    /// The context color space, if one is set.
217    #[must_use]
218    pub fn color_space(&self) -> Option<CGColorSpace> {
219        unsafe {
220            let color_space = ffi::CGBitmapContextGetColorSpace(self.ptr);
221            if color_space.is_null() {
222                None
223            } else {
224                Some(CGColorSpace::from_raw(ffi::CGColorSpaceRetain(color_space)))
225            }
226        }
227    }
228
229    /// The raw `CGImageAlphaInfo` value for the bitmap context.
230    #[must_use]
231    pub fn alpha_info(&self) -> u32 {
232        unsafe { ffi::CGBitmapContextGetAlphaInfo(self.ptr) }
233    }
234
235    /// The bitmap storage as immutable bytes.
236    ///
237    /// # Safety
238    ///
239    /// Every byte in the range must already be initialized. For the returned
240    /// reference's lifetime, the bitmap allocation must remain live and
241    /// immovable, and no retained alias or native call may draw into, clear,
242    /// replace, or otherwise mutate the context storage.
243    #[must_use]
244    pub unsafe fn as_bytes(&self) -> &[u8] {
245        let data = self.data();
246        let len = self.buffer_len();
247        if data.is_null() || len == 0 || isize::try_from(len).is_err() {
248            &[]
249        } else {
250            unsafe { std::slice::from_raw_parts(data.cast_const(), len) }
251        }
252    }
253
254    /// The bitmap storage as mutable bytes.
255    ///
256    /// # Safety
257    ///
258    /// Every byte in the range must already be initialized. For the returned
259    /// reference's lifetime, the caller must have unique access to the bitmap
260    /// storage across every retained wrapper and native alias, and no drawing
261    /// or snapshot operation may access the context.
262    #[must_use]
263    pub unsafe fn as_bytes_mut(&mut self) -> &mut [u8] {
264        let data = self.data();
265        let len = self.buffer_len();
266        if data.is_null() || len == 0 || isize::try_from(len).is_err() {
267            &mut []
268        } else {
269            unsafe { std::slice::from_raw_parts_mut(data, len) }
270        }
271    }
272
273    /// Set the current fill color in `DeviceRGB`.
274    pub fn set_rgb_fill_color(&self, r: f64, g: f64, b: f64, a: f64) {
275        unsafe { ffi::CGContextSetRGBFillColor(self.ptr, r, g, b, a) };
276    }
277
278    pub fn set_gray_fill_color(&self, gray: f64, alpha: f64) {
279        unsafe { crate::ffi::acf_cg_context_set_gray_fill_color(self.ptr, gray, alpha) };
280    }
281
282    pub fn set_text_fill_mode(&self) {
283        unsafe { crate::ffi::acf_cg_context_set_text_fill_mode(self.ptr) };
284    }
285
286    pub fn set_allows_antialiasing(&self, allowed: bool) {
287        unsafe { crate::ffi::acf_cg_context_set_allows_antialiasing(self.ptr, allowed) };
288    }
289
290    pub fn set_should_antialias(&self, enabled: bool) {
291        unsafe { crate::ffi::acf_cg_context_set_should_antialias(self.ptr, enabled) };
292    }
293
294    pub fn set_allows_font_subpixel_positioning(&self, allowed: bool) {
295        unsafe {
296            crate::ffi::acf_cg_context_set_allows_font_subpixel_positioning(self.ptr, allowed);
297        };
298    }
299
300    pub fn set_should_subpixel_position_fonts(&self, enabled: bool) {
301        unsafe { crate::ffi::acf_cg_context_set_should_subpixel_position_fonts(self.ptr, enabled) };
302    }
303
304    pub fn set_allows_font_subpixel_quantization(&self, allowed: bool) {
305        unsafe {
306            crate::ffi::acf_cg_context_set_allows_font_subpixel_quantization(self.ptr, allowed);
307        };
308    }
309
310    pub fn set_should_subpixel_quantize_fonts(&self, enabled: bool) {
311        unsafe { crate::ffi::acf_cg_context_set_should_subpixel_quantize_fonts(self.ptr, enabled) };
312    }
313
314    pub fn set_should_smooth_fonts(&self, enabled: bool) {
315        unsafe { crate::ffi::acf_cg_context_set_should_smooth_fonts(self.ptr, enabled) };
316    }
317
318    /// Set the current stroke color in `DeviceRGB`.
319    pub fn set_rgb_stroke_color(&self, r: f64, g: f64, b: f64, a: f64) {
320        unsafe { ffi::CGContextSetRGBStrokeColor(self.ptr, r, g, b, a) };
321    }
322
323    /// Set the stroke line width.
324    pub fn set_line_width(&self, w: f64) {
325        unsafe { ffi::CGContextSetLineWidth(self.ptr, w) };
326    }
327
328    /// Clear a rectangle to transparent black.
329    pub fn clear_rect(&self, x: f64, y: f64, w: f64, h: f64) {
330        unsafe { ffi::CGContextClearRect(self.ptr, CGRect::new(x, y, w, h)) };
331    }
332
333    /// Fill a rectangle.
334    pub fn fill_rect(&self, x: f64, y: f64, w: f64, h: f64) {
335        unsafe { ffi::CGContextFillRect(self.ptr, CGRect::new(x, y, w, h)) };
336    }
337
338    /// Stroke a rectangle.
339    pub fn stroke_rect(&self, x: f64, y: f64, w: f64, h: f64) {
340        unsafe { ffi::CGContextStrokeRect(self.ptr, CGRect::new(x, y, w, h)) };
341    }
342
343    /// Begin a new path.
344    pub fn begin_path(&self) {
345        unsafe { ffi::CGContextBeginPath(self.ptr) };
346    }
347
348    /// Close the current path.
349    pub fn close_path(&self) {
350        unsafe { ffi::CGContextClosePath(self.ptr) };
351    }
352
353    /// Move the current point.
354    pub fn move_to(&self, x: f64, y: f64) {
355        unsafe { ffi::CGContextMoveToPoint(self.ptr, x, y) };
356    }
357
358    /// Add a line segment to the current path.
359    pub fn add_line_to(&self, x: f64, y: f64) {
360        unsafe { ffi::CGContextAddLineToPoint(self.ptr, x, y) };
361    }
362
363    pub fn add_quad_curve_to(&self, cx: f64, cy: f64, x: f64, y: f64) {
364        unsafe { ffi::CGContextAddQuadCurveToPoint(self.ptr, cx, cy, x, y) };
365    }
366
367    pub fn add_curve_to(&self, c1x: f64, c1y: f64, c2x: f64, c2y: f64, x: f64, y: f64) {
368        unsafe { ffi::CGContextAddCurveToPoint(self.ptr, c1x, c1y, c2x, c2y, x, y) };
369    }
370
371    pub fn set_line_cap(&self, cap: LineCap) {
372        unsafe { ffi::CGContextSetLineCap(self.ptr, cap as u32) };
373    }
374
375    pub fn set_line_join(&self, join: LineJoin) {
376        unsafe { ffi::CGContextSetLineJoin(self.ptr, join as u32) };
377    }
378
379    pub fn set_miter_limit(&self, limit: f64) {
380        unsafe { ffi::CGContextSetMiterLimit(self.ptr, limit) };
381    }
382
383    pub fn draw_path(&self, mode: PathDrawingMode) {
384        unsafe { ffi::CGContextDrawPath(self.ptr, mode as u32) };
385    }
386
387    pub fn clip(&self, even_odd: bool) {
388        unsafe {
389            if even_odd {
390                ffi::CGContextEOClip(self.ptr);
391            } else {
392                ffi::CGContextClip(self.ptr);
393            }
394        }
395    }
396
397    pub fn concat_transform(&self, transform: CGAffineTransform) {
398        unsafe { ffi::CGContextConcatCTM(self.ptr, transform) };
399    }
400
401    /// Add a rectangle to the current path.
402    pub fn add_rect(&self, x: f64, y: f64, w: f64, h: f64) {
403        unsafe { ffi::CGContextAddRect(self.ptr, CGRect::new(x, y, w, h)) };
404    }
405
406    /// Add an ellipse inscribed in the rectangle.
407    pub fn add_ellipse_in_rect(&self, x: f64, y: f64, w: f64, h: f64) {
408        unsafe { ffi::CGContextAddEllipseInRect(self.ptr, CGRect::new(x, y, w, h)) };
409    }
410
411    /// Fill the current path.
412    pub fn fill_path(&self) {
413        unsafe { ffi::CGContextFillPath(self.ptr) };
414    }
415
416    /// Stroke the current path.
417    pub fn stroke_path(&self) {
418        unsafe { ffi::CGContextStrokePath(self.ptr) };
419    }
420
421    /// Draw an image into the target rectangle.
422    pub fn draw_image(&self, x: f64, y: f64, w: f64, h: f64, image: &CGImage) {
423        unsafe { ffi::CGContextDrawImage(self.ptr, CGRect::new(x, y, w, h), image.as_ptr()) };
424    }
425
426    /// Translate the current transformation matrix.
427    pub fn translate(&self, tx: f64, ty: f64) {
428        unsafe { ffi::CGContextTranslateCTM(self.ptr, tx, ty) };
429    }
430
431    /// Scale the current transformation matrix.
432    pub fn scale(&self, sx: f64, sy: f64) {
433        unsafe { ffi::CGContextScaleCTM(self.ptr, sx, sy) };
434    }
435
436    /// Rotate the current transformation matrix.
437    pub fn rotate(&self, radians: f64) {
438        unsafe { ffi::CGContextRotateCTM(self.ptr, radians) };
439    }
440
441    /// Save the current graphics state.
442    pub fn save_g_state(&self) {
443        unsafe { ffi::CGContextSaveGState(self.ptr) };
444    }
445
446    /// Restore the most recently saved graphics state.
447    pub fn restore_g_state(&self) {
448        unsafe { ffi::CGContextRestoreGState(self.ptr) };
449    }
450
451    /// Snapshot the current bitmap contents to a `CGImage`.
452    #[must_use]
453    pub fn snapshot_to_image(&self) -> Option<CGImage> {
454        let image = unsafe { ffi::CGBitmapContextCreateImage(self.ptr) };
455        if image.is_null() {
456            None
457        } else {
458            Some(unsafe { CGImage::from_raw(image) })
459        }
460    }
461
462    /// Raw `CGContextRef` pointer.
463    #[must_use]
464    pub const fn as_ptr(&self) -> *mut c_void {
465        self.ptr
466    }
467}