#![cfg(all(
target_os = "macos",
feature = "cocoa-legacy",
not(any(feature = "mini", feature = "embedded"))
))]
#![allow(non_snake_case)]
use std::os::raw::{c_int, c_void};
pub(crate) type CGContextRef = *mut c_void;
pub(crate) type CGColorSpaceRef = *mut c_void;
pub(crate) type CGImageRef = *mut c_void;
pub(crate) const BITMAP_ALPHA_PREMULTIPLIED_LAST: u32 = 1;
pub(crate) const BITMAP_BYTE_ORDER_32_LITTLE: u32 = 2 << 12;
#[repr(C)]
#[derive(Clone, Copy)]
pub(crate) struct CGRect {
pub origin: CGPoint,
pub size: CGSize,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub(crate) struct CGPoint {
pub x: f64,
pub y: f64,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub(crate) struct CGSize {
pub width: f64,
pub height: f64,
}
impl CGRect {
pub(crate) fn new(width: f64, height: f64) -> Self {
Self { origin: CGPoint { x: 0.0, y: 0.0 }, size: CGSize { width, height } }
}
}
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
pub(crate) fn CGColorSpaceCreateDeviceRGB() -> CGColorSpaceRef;
pub(crate) fn CGColorSpaceRelease(space: CGColorSpaceRef);
pub(crate) fn CGBitmapContextCreate(
data: *mut c_void,
width: usize,
height: usize,
bits_per_component: usize,
bytes_per_row: usize,
color_space: CGColorSpaceRef,
bitmap_info: u32,
) -> CGContextRef;
pub(crate) fn CGBitmapContextGetData(context: CGContextRef) -> *mut c_void;
pub(crate) fn CGBitmapContextCreateImage(context: CGContextRef) -> CGImageRef;
pub(crate) fn CGContextRelease(context: CGContextRef);
pub(crate) fn CGImageRelease(image: CGImageRef);
pub(crate) fn CGContextSaveGState(context: CGContextRef);
pub(crate) fn CGContextRestoreGState(context: CGContextRef);
pub(crate) fn CGContextDrawImage(context: CGContextRef, rect: CGRect, image: CGImageRef);
pub(crate) fn CGBitmapContextGetBytesPerRow(context: CGContextRef) -> usize;
pub(crate) fn CGImageGetWidth(image: CGImageRef) -> usize;
pub(crate) fn CGImageGetHeight(image: CGImageRef) -> usize;
pub(crate) fn CGContextSetInterpolationQuality(context: CGContextRef, quality: c_int);
}
pub(crate) unsafe fn blit_rgba(
context: CGContextRef,
width: u32,
height: u32,
frame: &[u8],
) -> bool {
if context.is_null() || width == 0 || height == 0 {
return false;
}
let expected = width as usize * height as usize * 4;
if frame.len() < expected {
log::error!(
"[macos] canvas: frame is {} bytes, need {} for {width}x{height}",
frame.len(),
expected
);
return false;
}
let color_space = CGColorSpaceCreateDeviceRGB();
if color_space.is_null() {
log::error!("[macos] canvas: CGColorSpaceCreateDeviceRGB returned null");
return false;
}
let bitmap = CGBitmapContextCreate(
std::ptr::null_mut(),
width as usize,
height as usize,
8,
width as usize * 4,
color_space,
BITMAP_ALPHA_PREMULTIPLIED_LAST | BITMAP_BYTE_ORDER_32_LITTLE,
);
if bitmap.is_null() {
CGColorSpaceRelease(color_space);
log::error!("[macos] canvas: CGBitmapContextCreate returned null for {width}x{height}");
return false;
}
let destination = CGBitmapContextGetData(bitmap) as *mut u8;
if !destination.is_null() {
let stride = CGBitmapContextGetBytesPerRow(bitmap);
for y in 0..height as usize {
for x in 0..width as usize {
let source = (y * width as usize + x) * 4;
let target = y * stride + x * 4;
let alpha = frame[source + 3] as u32;
*destination.add(target) = (frame[source] as u32 * alpha / 255) as u8;
*destination.add(target + 1) = (frame[source + 1] as u32 * alpha / 255) as u8;
*destination.add(target + 2) = (frame[source + 2] as u32 * alpha / 255) as u8;
*destination.add(target + 3) = alpha as u8;
}
}
}
let image = CGBitmapContextCreateImage(bitmap);
CGContextRelease(bitmap);
CGColorSpaceRelease(color_space);
if image.is_null() {
log::error!("[macos] canvas: CGBitmapContextCreateImage returned null");
return false;
}
CGContextSaveGState(context);
CGContextSetInterpolationQuality(context, 0); let image_width = CGImageGetWidth(image) as f64;
let image_height = CGImageGetHeight(image) as f64;
CGContextDrawImage(context, CGRect::new(image_width, image_height), image);
CGContextRestoreGState(context);
CGImageRelease(image);
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rect_is_built_at_the_origin() {
let rect = CGRect::new(12.0, 34.0);
assert_eq!(rect.origin.x, 0.0);
assert_eq!(rect.origin.y, 0.0);
assert_eq!(rect.size.width, 12.0);
assert_eq!(rect.size.height, 34.0);
}
#[test]
fn bitmap_info_encodes_premultiplied_rgba_little_endian() {
assert_eq!(BITMAP_ALPHA_PREMULTIPLIED_LAST, 1);
assert_eq!(BITMAP_BYTE_ORDER_32_LITTLE, 2 << 12);
assert_eq!(BITMAP_ALPHA_PREMULTIPLIED_LAST | BITMAP_BYTE_ORDER_32_LITTLE, 0x2001);
}
#[test]
fn blit_rejects_degenerate_input_without_touching_coregraphics() {
let empty: [u8; 0] = [];
assert!(!unsafe { blit_rgba(std::ptr::null_mut(), 4, 4, &empty) });
}
#[test]
fn blit_preserves_channel_order_and_orientation() {
const WIDTH: u32 = 4;
const HEIGHT: u32 = 4;
let mut frame = vec![0u8; (WIDTH * HEIGHT * 4) as usize];
for y in 0..HEIGHT {
for x in 0..WIDTH {
let offset = ((y * WIDTH + x) * 4) as usize;
let (r, g, b) = match (x < WIDTH / 2, y < HEIGHT / 2) {
(true, true) => (255, 0, 0),
(false, true) => (0, 255, 0),
(true, false) => (0, 0, 255),
(false, false) => (255, 255, 255),
};
frame[offset] = r;
frame[offset + 1] = g;
frame[offset + 2] = b;
frame[offset + 3] = 255;
}
}
unsafe {
let space = CGColorSpaceCreateDeviceRGB();
assert!(!space.is_null(), "colour space");
let context = CGBitmapContextCreate(
std::ptr::null_mut(),
WIDTH as usize,
HEIGHT as usize,
8,
WIDTH as usize * 4,
space,
BITMAP_ALPHA_PREMULTIPLIED_LAST | BITMAP_BYTE_ORDER_32_LITTLE,
);
assert!(!context.is_null(), "destination context");
assert!(blit_rgba(context, WIDTH, HEIGHT, &frame), "blit must succeed");
let data = CGBitmapContextGetData(context) as *mut u8;
assert!(!data.is_null(), "bitmap data");
let bytes_per_row = CGBitmapContextGetBytesPerRow(context) as usize;
let scale = (bytes_per_row / (WIDTH as usize * 4)).max(1);
let at = |x: u32, y: u32| {
let px = x as usize * scale;
let py = y as usize * scale;
let o = py * bytes_per_row + px * 4;
(*data.add(o), *data.add(o + 1), *data.add(o + 2))
};
assert_eq!(at(0, 0), (255, 0, 0), "top-left must stay red");
assert_eq!(at(WIDTH - 1, 0), (0, 255, 0), "top-right must stay green");
assert_eq!(at(0, HEIGHT - 1), (0, 0, 255), "bottom-left must stay blue");
assert_eq!(at(WIDTH - 1, HEIGHT - 1), (255, 255, 255), "bottom-right must stay white");
CGContextRelease(context);
CGColorSpaceRelease(space);
}
}
}