swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! CoreGraphics — turning a `CGImage` into bytes, and bytes into a PNG.
//!
//! AppKit's `NSBitmapImageRep` does both of these on the Mac, and the macOS
//! capture paths still use it: those renders are compared byte-for-byte against
//! a Swift reference, and swapping the pixel plumbing underneath them would
//! invalidate that. CoreGraphics and ImageIO are the same API on every Apple
//! platform, so this is what the iOS paths use instead.
//!
//! The two are not interchangeable — a bitmap context cannot hold
//! non-premultiplied alpha — but that does not matter: each platform is only
//! ever compared against a reference rendered the same way it was.

use core::ffi::{c_char, c_void};

unsafe extern "C" {
    fn CGImageGetWidth(image: *mut c_void) -> usize;
    fn CGImageGetHeight(image: *mut c_void) -> usize;
    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,
        space: *mut c_void,
        bitmap_info: u32,
    ) -> *mut c_void;
    fn CGBitmapContextCreateImage(ctx: *mut c_void) -> *mut c_void;
    fn CGContextDrawImage(ctx: *mut c_void, rect: CGRect, image: *mut c_void);
    fn CGContextRelease(ctx: *mut c_void);
    fn CGContextTranslateCTM(ctx: *mut c_void, tx: f64, ty: f64);
    fn CGContextScaleCTM(ctx: *mut c_void, sx: f64, sy: f64);
    fn CGImageRelease(image: *mut c_void);

    fn CFStringCreateWithCString(alloc: *const c_void, s: *const c_char, enc: u32) -> *const c_void;
    fn CFURLCreateFromFileSystemRepresentation(
        alloc: *const c_void,
        path: *const u8,
        len: isize,
        is_dir: bool,
    ) -> *const c_void;
    fn CFRelease(o: *const c_void);
    fn CGImageDestinationCreateWithURL(
        url: *const c_void,
        ty: *const c_void,
        count: usize,
        options: *const c_void,
    ) -> *mut c_void;
    fn CGImageDestinationAddImage(dest: *mut c_void, image: *mut c_void, props: *const c_void);
    fn CGImageDestinationFinalize(dest: *mut c_void) -> bool;
}

#[repr(C)]
struct CGPoint {
    x: f64,
    y: f64,
}
#[repr(C)]
struct CGSize {
    width: f64,
    height: f64,
}
#[repr(C)]
struct CGRect {
    origin: CGPoint,
    size: CGSize,
}

/// `kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big` — bytes in memory
/// order R, G, B, A, which is what [`crate::render::Rendered`] holds.
///
/// Premultiplied because a bitmap *context* cannot be anything else; only an
/// image can carry straight alpha.
const RGBA8: u32 = 1 | (4 << 12);
const UTF8: u32 = 0x0800_0100;

/// Draw a `CGImage` into a tightly packed RGBA8 buffer.
///
/// # Safety
/// `image` must be a live `CGImage`.
pub unsafe fn cgimage_to_rgba(image: *mut c_void) -> (u32, u32, Vec<u8>) {
    unsafe {
        let w = CGImageGetWidth(image);
        let h = CGImageGetHeight(image);
        let mut pixels = vec![0u8; w * h * 4];
        let space = CGColorSpaceCreateDeviceRGB();
        let ctx = CGBitmapContextCreate(
            pixels.as_mut_ptr() as *mut c_void,
            w,
            h,
            8,
            w * 4,
            space,
            RGBA8,
        );
        assert!(!ctx.is_null(), "could not create a {w}x{h} bitmap context");
        CGContextDrawImage(
            ctx,
            CGRect {
                origin: CGPoint { x: 0.0, y: 0.0 },
                size: CGSize { width: w as f64, height: h as f64 },
            },
            image,
        );
        CGContextRelease(ctx);
        CGColorSpaceRelease(space);
        (w as u32, h as u32, pixels)
    }
}

/// Build a `CGImage` from tightly packed RGBA8 bytes. Caller owns the result.
///
/// # Safety
/// `pixels` must be at least `width * height * 4` bytes.
pub unsafe fn rgba_to_cgimage(width: u32, height: u32, pixels: &[u8]) -> *mut c_void {
    unsafe {
        let (w, h) = (width as usize, height as usize);
        let mut copy = pixels[..w * h * 4].to_vec();
        let space = CGColorSpaceCreateDeviceRGB();
        let ctx = CGBitmapContextCreate(
            copy.as_mut_ptr() as *mut c_void,
            w,
            h,
            8,
            w * 4,
            space,
            RGBA8,
        );
        let image = if ctx.is_null() { core::ptr::null_mut() } else { CGBitmapContextCreateImage(ctx) };
        if !ctx.is_null() {
            CGContextRelease(ctx);
        }
        CGColorSpaceRelease(space);
        image
    }
}

/// Write RGBA8 bytes to a PNG file.
pub fn write_png(width: u32, height: u32, pixels: &[u8], path: &str) -> bool {
    unsafe {
        let image = rgba_to_cgimage(width, height, pixels);
        if image.is_null() {
            return false;
        }
        let url = CFURLCreateFromFileSystemRepresentation(
            core::ptr::null(),
            path.as_ptr(),
            path.len() as isize,
            false,
        );
        let png = CFStringCreateWithCString(core::ptr::null(), c"public.png".as_ptr(), UTF8);
        let dest = CGImageDestinationCreateWithURL(url, png, 1, core::ptr::null());
        let ok = if dest.is_null() {
            false
        } else {
            CGImageDestinationAddImage(dest, image, core::ptr::null());
            let ok = CGImageDestinationFinalize(dest);
            CFRelease(dest as *const c_void);
            ok
        };
        CFRelease(png);
        CFRelease(url);
        CGImageRelease(image);
        ok
    }
}

/// Rasterise a `CALayer` into a tightly packed RGBA8 buffer.
///
/// The flip is not cosmetic: a bitmap context has its origin at the bottom
/// left and UIKit's layers have theirs at the top left, so without it every
/// capture comes out upside down.
///
/// # Safety
/// `layer` must be a live `CALayer`.
pub unsafe fn capture_layer(layer: *mut c_void, width: u32, height: u32) -> Vec<u8> {
    unsafe {
        let (w, h) = (width as usize, height as usize);
        let mut pixels = vec![0u8; w * h * 4];
        let space = CGColorSpaceCreateDeviceRGB();
        let ctx = CGBitmapContextCreate(
            pixels.as_mut_ptr() as *mut c_void,
            w,
            h,
            8,
            w * 4,
            space,
            RGBA8,
        );
        assert!(!ctx.is_null(), "could not create a {w}x{h} bitmap context");
        CGContextTranslateCTM(ctx, 0.0, h as f64);
        CGContextScaleCTM(ctx, 1.0, -1.0);
        crate::objc::send_id(layer, crate::objc::sel(c"renderInContext:"), ctx);
        CGContextRelease(ctx);
        CGColorSpaceRelease(space);
        pixels
    }
}