//! CoreGraphics FFI bindings for bitmap rendering. //! //! Provides wrappers around `CGColorSpace`, `CGContext` (bitmap contexts), //! and `CGImage` for software rendering into a Rust-owned pixel buffer. //! //! # Safety //! //! This module contains `unsafe` code for FFI with CoreGraphics. //! The `platform` crate is one of the few crates where `unsafe` is permitted. use std::os::raw::c_void; use std::ptr::NonNull; // --------------------------------------------------------------------------- // CoreGraphics framework link // --------------------------------------------------------------------------- #[link(name = "CoreGraphics", kind = "framework")] extern "C" { fn CGColorSpaceCreateDeviceRGB() -> *mut c_void; fn CGColorSpaceRelease(space: *mut c_void); fn CGBitmapContextCreate( data: *mut c_void, width: usize, height: usize, bits_per_component: usize, bytes_per_row: usize, colorspace: *mut c_void, bitmap_info: u32, ) -> *mut c_void; fn CGBitmapContextCreateImage(context: *mut c_void) -> *mut c_void; fn CGContextRelease(context: *mut c_void); fn CGImageRelease(image: *mut c_void); fn CGContextDrawImage(context: *mut c_void, rect: CGRect, image: *mut c_void); fn CGContextSetRGBFillColor(context: *mut c_void, red: f64, green: f64, blue: f64, alpha: f64); fn CGContextFillRect(context: *mut c_void, rect: CGRect); } // --------------------------------------------------------------------------- // Geometry (re-export compatible with appkit types) // --------------------------------------------------------------------------- /// `CGRect` — a rectangle defined by origin and size. /// /// This is layout-compatible with `NSRect` / the appkit `NSRect` type. #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct CGRect { pub origin: CGPoint, pub size: CGSize, } /// `CGPoint` — a point in 2D space. #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct CGPoint { pub x: f64, pub y: f64, } /// `CGSize` — a 2D size. #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct CGSize { pub width: f64, pub height: f64, } impl CGRect { /// Create a new rectangle. pub fn new(x: f64, y: f64, width: f64, height: f64) -> CGRect { CGRect { origin: CGPoint { x, y }, size: CGSize { width, height }, } } } // --------------------------------------------------------------------------- // CGBitmapInfo constants // --------------------------------------------------------------------------- /// Alpha info mask (lower 5 bits of CGBitmapInfo). pub const K_CG_IMAGE_ALPHA_PREMULTIPLIED_FIRST: u32 = 2; /// Byte order mask: 32-bit host byte order. pub const K_CG_BITMAP_BYTE_ORDER_32_HOST: u32 = 0; /// Standard BGRA premultiplied alpha format used by macOS windowing. /// /// Pixel layout in memory on little-endian (ARM64): B, G, R, A /// This matches what CoreGraphics and AppKit expect for display. pub const BITMAP_INFO_PREMULTIPLIED_FIRST: u32 = K_CG_IMAGE_ALPHA_PREMULTIPLIED_FIRST | K_CG_BITMAP_BYTE_ORDER_32_HOST; // --------------------------------------------------------------------------- // ColorSpace // --------------------------------------------------------------------------- /// An owned CoreGraphics device RGB color space. pub struct ColorSpace(NonNull); impl ColorSpace { /// Create a device RGB color space. pub fn device_rgb() -> Option { let ptr = unsafe { CGColorSpaceCreateDeviceRGB() }; NonNull::new(ptr).map(ColorSpace) } /// Return the raw pointer. #[inline] pub fn as_ptr(&self) -> *mut c_void { self.0.as_ptr() } } impl Drop for ColorSpace { fn drop(&mut self) { unsafe { CGColorSpaceRelease(self.as_ptr()) } } } // --------------------------------------------------------------------------- // BitmapContext — a CGBitmapContext backed by a Rust-owned pixel buffer // --------------------------------------------------------------------------- /// A CoreGraphics bitmap context backed by a Rust-owned pixel buffer. /// /// The pixel buffer is BGRA, 8 bits per component, premultiplied alpha. /// The buffer is owned by this struct and the `CGBitmapContext` renders into it. pub struct BitmapContext { context: NonNull, buffer: Vec, width: usize, height: usize, } impl BitmapContext { /// Create a new bitmap context with the given dimensions. /// /// Allocates a Rust-owned pixel buffer and wraps it in a CGBitmapContext. /// Pixel format: 4 bytes per pixel (BGRA, premultiplied alpha). pub fn new(width: usize, height: usize) -> Option { if width == 0 || height == 0 { return None; } let bytes_per_row = width * 4; let mut buffer = vec![0u8; bytes_per_row * height]; let color_space = ColorSpace::device_rgb()?; let ctx = unsafe { CGBitmapContextCreate( buffer.as_mut_ptr() as *mut c_void, width, height, 8, // bits per component bytes_per_row, color_space.as_ptr(), BITMAP_INFO_PREMULTIPLIED_FIRST, ) }; let context = NonNull::new(ctx)?; Some(BitmapContext { context, buffer, width, height, }) } /// Get the width in pixels. #[inline] pub fn width(&self) -> usize { self.width } /// Get the height in pixels. #[inline] pub fn height(&self) -> usize { self.height } /// Get the bytes per row (stride). #[inline] pub fn bytes_per_row(&self) -> usize { self.width * 4 } /// Get a shared reference to the raw pixel buffer. /// /// Pixel format: BGRA, 8 bits per component, premultiplied alpha. /// Layout: row-major, bottom-to-top (CG default coordinate system). #[inline] pub fn pixels(&self) -> &[u8] { &self.buffer } /// Get a mutable reference to the raw pixel buffer. /// /// After modifying pixels directly, changes are immediately visible to /// CoreGraphics since the bitmap context wraps this buffer. #[inline] pub fn pixels_mut(&mut self) -> &mut [u8] { &mut self.buffer } /// Create a `CGImage` from the current bitmap context contents. /// /// Returns `None` if the image cannot be created. pub fn create_image(&self) -> Option { let img = unsafe { CGBitmapContextCreateImage(self.context.as_ptr()) }; NonNull::new(img).map(Image) } /// Fill a rectangle with an RGBA color using CoreGraphics. /// /// Color components are in 0.0..=1.0 range. pub fn fill_rect(&self, rect: CGRect, r: f64, g: f64, b: f64, a: f64) { unsafe { CGContextSetRGBFillColor(self.context.as_ptr(), r, g, b, a); CGContextFillRect(self.context.as_ptr(), rect); } } /// Clear the entire buffer to a given RGBA color. /// /// Color components are in 0.0..=1.0 range. This uses CoreGraphics /// `CGContextFillRect` to fill the entire bitmap. pub fn clear(&self, r: f64, g: f64, b: f64, a: f64) { let rect = CGRect::new(0.0, 0.0, self.width as f64, self.height as f64); self.fill_rect(rect, r, g, b, a); } /// Return the raw CGContextRef pointer. /// /// Useful for passing to other CG drawing functions. #[inline] pub fn as_ptr(&self) -> *mut c_void { self.context.as_ptr() } } impl Drop for BitmapContext { fn drop(&mut self) { unsafe { CGContextRelease(self.context.as_ptr()) } } } // --------------------------------------------------------------------------- // Image — an owned CGImage // --------------------------------------------------------------------------- /// An owned CoreGraphics image (CGImageRef). /// /// Created from a [`BitmapContext`] via [`BitmapContext::create_image`]. pub struct Image(NonNull); impl Image { /// Return the raw CGImageRef pointer. #[inline] pub fn as_ptr(&self) -> *mut c_void { self.0.as_ptr() } } impl Drop for Image { fn drop(&mut self) { unsafe { CGImageRelease(self.as_ptr()) } } } // --------------------------------------------------------------------------- // Drawing a CGImage into a CGContext // --------------------------------------------------------------------------- /// Draw a `CGImage` into a CoreGraphics context at the given rectangle. /// /// This is used in `drawRect:` to blit the bitmap into the view's context. /// /// # Safety /// /// `target_context` must be a valid CGContextRef (e.g., from /// `NSGraphicsContext.currentContext.CGContext`). pub unsafe fn draw_image_in_context(target_context: *mut c_void, rect: CGRect, image: &Image) { CGContextDrawImage(target_context, rect, image.as_ptr()); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; #[test] fn cgrect_new() { let rect = CGRect::new(10.0, 20.0, 300.0, 400.0); assert_eq!(rect.origin.x, 10.0); assert_eq!(rect.origin.y, 20.0); assert_eq!(rect.size.width, 300.0); assert_eq!(rect.size.height, 400.0); } #[test] fn bitmap_info_constant() { assert_eq!(BITMAP_INFO_PREMULTIPLIED_FIRST, 2); } #[test] fn create_color_space() { let cs = ColorSpace::device_rgb().expect("should create device RGB color space"); assert!(!cs.as_ptr().is_null()); } #[test] fn create_bitmap_context() { let ctx = BitmapContext::new(100, 100).expect("should create bitmap context"); assert_eq!(ctx.width(), 100); assert_eq!(ctx.height(), 100); assert_eq!(ctx.bytes_per_row(), 400); assert_eq!(ctx.pixels().len(), 100 * 400); } #[test] fn bitmap_context_zero_size_returns_none() { assert!(BitmapContext::new(0, 100).is_none()); assert!(BitmapContext::new(100, 0).is_none()); assert!(BitmapContext::new(0, 0).is_none()); } #[test] fn bitmap_context_pixels_mut() { let mut ctx = BitmapContext::new(10, 10).expect("should create"); let pixels = ctx.pixels_mut(); // Write a red pixel at (0, 0): BGRA format pixels[0] = 0; // B pixels[1] = 0; // G pixels[2] = 255; // R pixels[3] = 255; // A assert_eq!(ctx.pixels()[0], 0); assert_eq!(ctx.pixels()[2], 255); } #[test] fn bitmap_context_clear_and_create_image() { let ctx = BitmapContext::new(50, 50).expect("should create"); ctx.clear(0.0, 0.0, 1.0, 1.0); // blue let img = ctx.create_image().expect("should create image"); assert!(!img.as_ptr().is_null()); } #[test] fn bitmap_context_fill_rect() { let ctx = BitmapContext::new(100, 100).expect("should create"); let rect = CGRect::new(10.0, 10.0, 50.0, 50.0); ctx.fill_rect(rect, 1.0, 0.0, 0.0, 1.0); // red rectangle let img = ctx .create_image() .expect("should create image from filled context"); assert!(!img.as_ptr().is_null()); } }