Skip to main content

swiftui_native/
cg.rs

1//! CoreGraphics — turning a `CGImage` into bytes, and bytes into a PNG.
2//!
3//! AppKit's `NSBitmapImageRep` does both of these on the Mac, and the macOS
4//! capture paths still use it: those renders are compared byte-for-byte against
5//! a Swift reference, and swapping the pixel plumbing underneath them would
6//! invalidate that. CoreGraphics and ImageIO are the same API on every Apple
7//! platform, so this is what the iOS paths use instead.
8//!
9//! The two are not interchangeable — a bitmap context cannot hold
10//! non-premultiplied alpha — but that does not matter: each platform is only
11//! ever compared against a reference rendered the same way it was.
12
13use core::ffi::{c_char, c_void};
14
15unsafe extern "C" {
16    fn CGImageGetWidth(image: *mut c_void) -> usize;
17    fn CGImageGetHeight(image: *mut c_void) -> usize;
18    fn CGColorSpaceCreateDeviceRGB() -> *mut c_void;
19    fn CGColorSpaceRelease(space: *mut c_void);
20    fn CGBitmapContextCreate(
21        data: *mut c_void,
22        width: usize,
23        height: usize,
24        bits_per_component: usize,
25        bytes_per_row: usize,
26        space: *mut c_void,
27        bitmap_info: u32,
28    ) -> *mut c_void;
29    fn CGBitmapContextCreateImage(ctx: *mut c_void) -> *mut c_void;
30    fn CGContextDrawImage(ctx: *mut c_void, rect: CGRect, image: *mut c_void);
31    fn CGContextRelease(ctx: *mut c_void);
32    fn CGContextTranslateCTM(ctx: *mut c_void, tx: f64, ty: f64);
33    fn CGContextScaleCTM(ctx: *mut c_void, sx: f64, sy: f64);
34    fn CGImageRelease(image: *mut c_void);
35
36    fn CFStringCreateWithCString(alloc: *const c_void, s: *const c_char, enc: u32) -> *const c_void;
37    fn CFURLCreateFromFileSystemRepresentation(
38        alloc: *const c_void,
39        path: *const u8,
40        len: isize,
41        is_dir: bool,
42    ) -> *const c_void;
43    fn CFRelease(o: *const c_void);
44    fn CGImageDestinationCreateWithURL(
45        url: *const c_void,
46        ty: *const c_void,
47        count: usize,
48        options: *const c_void,
49    ) -> *mut c_void;
50    fn CGImageDestinationAddImage(dest: *mut c_void, image: *mut c_void, props: *const c_void);
51    fn CGImageDestinationFinalize(dest: *mut c_void) -> bool;
52}
53
54#[repr(C)]
55struct CGPoint {
56    x: f64,
57    y: f64,
58}
59#[repr(C)]
60struct CGSize {
61    width: f64,
62    height: f64,
63}
64#[repr(C)]
65struct CGRect {
66    origin: CGPoint,
67    size: CGSize,
68}
69
70/// `kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big` — bytes in memory
71/// order R, G, B, A, which is what [`crate::render::Rendered`] holds.
72///
73/// Premultiplied because a bitmap *context* cannot be anything else; only an
74/// image can carry straight alpha.
75const RGBA8: u32 = 1 | (4 << 12);
76const UTF8: u32 = 0x0800_0100;
77
78/// Draw a `CGImage` into a tightly packed RGBA8 buffer.
79///
80/// # Safety
81/// `image` must be a live `CGImage`.
82pub unsafe fn cgimage_to_rgba(image: *mut c_void) -> (u32, u32, Vec<u8>) {
83    unsafe {
84        let w = CGImageGetWidth(image);
85        let h = CGImageGetHeight(image);
86        let mut pixels = vec![0u8; w * h * 4];
87        let space = CGColorSpaceCreateDeviceRGB();
88        let ctx = CGBitmapContextCreate(
89            pixels.as_mut_ptr() as *mut c_void,
90            w,
91            h,
92            8,
93            w * 4,
94            space,
95            RGBA8,
96        );
97        assert!(!ctx.is_null(), "could not create a {w}x{h} bitmap context");
98        CGContextDrawImage(
99            ctx,
100            CGRect {
101                origin: CGPoint { x: 0.0, y: 0.0 },
102                size: CGSize { width: w as f64, height: h as f64 },
103            },
104            image,
105        );
106        CGContextRelease(ctx);
107        CGColorSpaceRelease(space);
108        (w as u32, h as u32, pixels)
109    }
110}
111
112/// Build a `CGImage` from tightly packed RGBA8 bytes. Caller owns the result.
113///
114/// # Safety
115/// `pixels` must be at least `width * height * 4` bytes.
116pub unsafe fn rgba_to_cgimage(width: u32, height: u32, pixels: &[u8]) -> *mut c_void {
117    unsafe {
118        let (w, h) = (width as usize, height as usize);
119        let mut copy = pixels[..w * h * 4].to_vec();
120        let space = CGColorSpaceCreateDeviceRGB();
121        let ctx = CGBitmapContextCreate(
122            copy.as_mut_ptr() as *mut c_void,
123            w,
124            h,
125            8,
126            w * 4,
127            space,
128            RGBA8,
129        );
130        let image = if ctx.is_null() { core::ptr::null_mut() } else { CGBitmapContextCreateImage(ctx) };
131        if !ctx.is_null() {
132            CGContextRelease(ctx);
133        }
134        CGColorSpaceRelease(space);
135        image
136    }
137}
138
139/// Write RGBA8 bytes to a PNG file.
140pub fn write_png(width: u32, height: u32, pixels: &[u8], path: &str) -> bool {
141    unsafe {
142        let image = rgba_to_cgimage(width, height, pixels);
143        if image.is_null() {
144            return false;
145        }
146        let url = CFURLCreateFromFileSystemRepresentation(
147            core::ptr::null(),
148            path.as_ptr(),
149            path.len() as isize,
150            false,
151        );
152        let png = CFStringCreateWithCString(core::ptr::null(), c"public.png".as_ptr(), UTF8);
153        let dest = CGImageDestinationCreateWithURL(url, png, 1, core::ptr::null());
154        let ok = if dest.is_null() {
155            false
156        } else {
157            CGImageDestinationAddImage(dest, image, core::ptr::null());
158            let ok = CGImageDestinationFinalize(dest);
159            CFRelease(dest as *const c_void);
160            ok
161        };
162        CFRelease(png);
163        CFRelease(url);
164        CGImageRelease(image);
165        ok
166    }
167}
168
169/// Rasterise a `CALayer` into a tightly packed RGBA8 buffer.
170///
171/// The flip is not cosmetic: a bitmap context has its origin at the bottom
172/// left and UIKit's layers have theirs at the top left, so without it every
173/// capture comes out upside down.
174///
175/// # Safety
176/// `layer` must be a live `CALayer`.
177pub unsafe fn capture_layer(layer: *mut c_void, width: u32, height: u32) -> Vec<u8> {
178    unsafe {
179        let (w, h) = (width as usize, height as usize);
180        let mut pixels = vec![0u8; w * h * 4];
181        let space = CGColorSpaceCreateDeviceRGB();
182        let ctx = CGBitmapContextCreate(
183            pixels.as_mut_ptr() as *mut c_void,
184            w,
185            h,
186            8,
187            w * 4,
188            space,
189            RGBA8,
190        );
191        assert!(!ctx.is_null(), "could not create a {w}x{h} bitmap context");
192        CGContextTranslateCTM(ctx, 0.0, h as f64);
193        CGContextScaleCTM(ctx, 1.0, -1.0);
194        crate::objc::send_id(layer, crate::objc::sel(c"renderInContext:"), ctx);
195        CGContextRelease(ctx);
196        CGColorSpaceRelease(space);
197        pixels
198    }
199}