apple-cf 0.8.0

Safe Rust bindings for Apple's shared Core* frameworks (CoreFoundation, CoreMedia, CoreVideo, CoreGraphics, IOSurface, Dispatch).
Documentation
// CoreGraphics Bridge - CGRect, CGSize, CGPoint, CGImage

import CoreGraphics
import Foundation
import ImageIO
import UniformTypeIdentifiers

// MARK: - CGRect Bridge

public struct CGRectBridge {
    public var x: Double
    public var y: Double
    public var width: Double
    public var height: Double

    public init(x: Double, y: Double, width: Double, height: Double) {
        self.x = x
        self.y = y
        self.width = width
        self.height = height
    }

    public init(rect: CGRect) {
        x = Double(rect.origin.x)
        y = Double(rect.origin.y)
        width = Double(rect.size.width)
        height = Double(rect.size.height)
    }

    public func toCGRect() -> CGRect {
        CGRect(x: x, y: y, width: width, height: height)
    }
}

// MARK: - CGSize Bridge

public struct CGSizeBridge {
    public var width: Double
    public var height: Double

    public init(width: Double, height: Double) {
        self.width = width
        self.height = height
    }

    public init(size: CGSize) {
        width = Double(size.width)
        height = Double(size.height)
    }

    public func toCGSize() -> CGSize {
        CGSize(width: width, height: height)
    }
}

// MARK: - CGPoint Bridge

public struct CGPointBridge {
    public var x: Double
    public var y: Double

    public init(x: Double, y: Double) {
        self.x = x
        self.y = y
    }

    public init(point: CGPoint) {
        x = Double(point.x)
        y = Double(point.y)
    }

    public func toCGPoint() -> CGPoint {
        CGPoint(x: x, y: y)
    }
}

// MARK: - CGImage Bridge

@_cdecl("cgimage_get_width")
public func getCGImageWidth(_ image: OpaquePointer) -> Int {
    let cgImage = Unmanaged<CGImage>.fromOpaque(UnsafeRawPointer(image)).takeUnretainedValue()
    return cgImage.width
}

@_cdecl("cgimage_get_height")
public func getCGImageHeight(_ image: OpaquePointer) -> Int {
    let cgImage = Unmanaged<CGImage>.fromOpaque(UnsafeRawPointer(image)).takeUnretainedValue()
    return cgImage.height
}

@_cdecl("cgimage_release")
public func releaseCGImage(_ image: OpaquePointer) {
    Unmanaged<CGImage>.fromOpaque(UnsafeRawPointer(image)).release()
}

@_cdecl("cgimage_retain")
public func retainCGImage(_ image: OpaquePointer) -> OpaquePointer {
    let cgImage = Unmanaged<CGImage>.fromOpaque(UnsafeRawPointer(image)).takeUnretainedValue()
    return OpaquePointer(Unmanaged.passRetained(cgImage).toOpaque())
}

@_cdecl("cgimage_render_rgba_into")
public func renderCGImageRGBAInto(
    _ image: OpaquePointer,
    _ destBuffer: UnsafeMutableRawPointer,
    _ destCapacity: Int
) -> Int {
    let cgImage = Unmanaged<CGImage>.fromOpaque(UnsafeRawPointer(image)).takeUnretainedValue()

    let width = cgImage.width
    let height = cgImage.height
    let bytesPerPixel = 4 // RGBA
    let bytesPerRow = width * bytesPerPixel
    let totalBytes = height * bytesPerRow

    // Refuse to render into a buffer the caller didn't size correctly. The
    // Rust side allocates exactly width*height*4 bytes; if the image's
    // dimensions changed between the size query and this call (e.g. some
    // future API mutates a CGImage), we abort rather than overflow.
    guard totalBytes <= destCapacity else {
        return 0
    }

    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue

    // CGContext draws straight into the caller-provided buffer — no Swift
    // malloc, no extra memcpy. Replaces the previous double-allocate path
    // (CGContext-owned buffer + UnsafeMutableRawPointer.allocate + copyMemory)
    // and removes the matching Rust-side .to_vec() third copy.
    guard let context = CGContext(
        data: destBuffer,
        width: width,
        height: height,
        bitsPerComponent: 8,
        bytesPerRow: bytesPerRow,
        space: colorSpace,
        bitmapInfo: bitmapInfo
    ) else {
        return 0
    }

    // .copy unconditionally overwrites destination pixels. CGContext.draw
    // defaults to .normal which blends source-over-dest when the source has
    // any non-opaque alpha — that would make the output depend on whatever
    // was in the (sometimes uninitialised) destination buffer, breaking
    // determinism across rgba_data() (uninit dest) vs rgba_data_into()
    // (caller-supplied dest).
    context.setBlendMode(.copy)
    context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))

    return totalBytes
}

@_cdecl("cgimage_render_bgra_into")
public func renderCGImageBGRAInto(
    _ image: OpaquePointer,
    _ destBuffer: UnsafeMutableRawPointer,
    _ destCapacity: Int
) -> Int {
    let cgImage = Unmanaged<CGImage>.fromOpaque(UnsafeRawPointer(image)).takeUnretainedValue()

    let width = cgImage.width
    let height = cgImage.height
    let bytesPerPixel = 4 // BGRA
    let bytesPerRow = width * bytesPerPixel
    let totalBytes = height * bytesPerRow

    guard totalBytes <= destCapacity else {
        return 0
    }

    // Native pixel layout for ScreenCaptureKit-produced CGImages — premultipliedFirst
    // + byteOrder32Little = "BGRA" interpreted MSB-first, which matches the
    // CVPixelBufferGet*BaseAddress format `kCVPixelFormatType_32BGRA`. By matching
    // the source layout exactly, CGContext.draw skips the per-pixel
    // colour-channel swap that the RGBA path forces. Measured at ~20 ms
    // saved on a 4K screenshot.
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGImageAlphaInfo.premultipliedFirst.rawValue
        | CGBitmapInfo.byteOrder32Little.rawValue

    guard let context = CGContext(
        data: destBuffer,
        width: width,
        height: height,
        bitsPerComponent: 8,
        bytesPerRow: bytesPerRow,
        space: colorSpace,
        bitmapInfo: bitmapInfo
    ) else {
        return 0
    }

    // See cgimage_render_rgba_into above — same rationale for .copy.
    context.setBlendMode(.copy)
    context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))

    return totalBytes
}

// Legacy two-call API. Retained for back-compat with downstream consumers
// that may have linked against it; new code goes through cgimage_render_rgba_into
// above which avoids a 33 MB-per-4K-frame Swift-side memcpy + malloc.
@_cdecl("cgimage_get_data")
public func getCGImageData(_ image: OpaquePointer, _ outPtr: UnsafeMutablePointer<UnsafeRawPointer?>, _ outLength: UnsafeMutablePointer<Int>) -> Bool {
    let cgImage = Unmanaged<CGImage>.fromOpaque(UnsafeRawPointer(image)).takeUnretainedValue()

    let width = cgImage.width
    let height = cgImage.height
    let bytesPerPixel = 4 // RGBA
    let bytesPerRow = width * bytesPerPixel
    let totalBytes = height * bytesPerRow

    let buffer = UnsafeMutableRawPointer.allocate(byteCount: totalBytes, alignment: 1)

    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue

    guard let context = CGContext(
        data: buffer,
        width: width,
        height: height,
        bitsPerComponent: 8,
        bytesPerRow: bytesPerRow,
        space: colorSpace,
        bitmapInfo: bitmapInfo
    ) else {
        buffer.deallocate()
        return false
    }

    context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))

    outPtr.pointee = UnsafeRawPointer(buffer)
    outLength.pointee = totalBytes

    return true
}

@_cdecl("cgimage_free_data")
public func freeCGImageData(_ ptr: UnsafeMutableRawPointer) {
    ptr.deallocate()
}

@_cdecl("cgimage_save_png")
public func saveCGImageToPNG(_ image: OpaquePointer, _ pathPtr: UnsafePointer<CChar>) -> Bool {
    let cgImage = Unmanaged<CGImage>.fromOpaque(UnsafeRawPointer(image)).takeUnretainedValue()
    let path = String(cString: pathPtr)
    let url = URL(fileURLWithPath: path)

    guard let destination = CGImageDestinationCreateWithURL(url as CFURL, UTType.png.identifier as CFString, 1, nil) else {
        return false
    }

    CGImageDestinationAddImage(destination, cgImage, nil)
    return CGImageDestinationFinalize(destination)
}

/// Save CGImage to file with specified format
/// format: 0=PNG, 1=JPEG, 2=TIFF, 3=GIF, 4=BMP, 5=HEIC
/// quality: 0.0-1.0 for lossy formats (JPEG, HEIC)
@_cdecl("cgimage_save_to_file")
public func saveCGImageToFile(_ image: OpaquePointer, _ pathPtr: UnsafePointer<CChar>, _ format: Int32, _ quality: Float) -> Bool {
    let cgImage = Unmanaged<CGImage>.fromOpaque(UnsafeRawPointer(image)).takeUnretainedValue()
    let path = String(cString: pathPtr)
    let url = URL(fileURLWithPath: path)

    let utType: UTType
    switch format {
    case 0: utType = .png
    case 1: utType = .jpeg
    case 2: utType = .tiff
    case 3: utType = .gif
    case 4: utType = .bmp
    case 5: utType = .heic
    default: return false
    }

    guard let destination = CGImageDestinationCreateWithURL(url as CFURL, utType.identifier as CFString, 1, nil) else {
        return false
    }

    // Set quality for lossy formats
    var properties: [CFString: Any]? = nil
    if format == 1 || format == 5 { // JPEG or HEIC
        properties = [kCGImageDestinationLossyCompressionQuality: quality]
    }

    CGImageDestinationAddImage(destination, cgImage, properties as CFDictionary?)
    return CGImageDestinationFinalize(destination)
}

@_cdecl("cgimage_hash")
public func cgimageHash(_ image: OpaquePointer) -> Int {
    let cgImage = Unmanaged<CGImage>.fromOpaque(UnsafeRawPointer(image)).takeUnretainedValue()
    return cgImage.hashValue
}