#![allow(unsafe_code)]
use std::ffi::c_void;
use std::ptr::NonNull;
use std::sync::{mpsc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use block2::RcBlock;
use dispatch2::{DispatchQueue, DispatchQueueAttr};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2::{define_class, msg_send, AnyThread, DefinedClass, MainThreadMarker};
use objc2_app_kit::NSScreen;
use objc2_core_foundation::{
kCFBooleanTrue, kCFRunLoopDefaultMode, CFBoolean, CFDictionary, CFRetained, CFRunLoop,
CFString, CGFloat, CGPoint, CGRect, CGSize,
};
use objc2_core_graphics::{
CGBitmapContextCreate, CGColorSpace, CGContext, CGDisplayIsAsleep, CGError,
CGGetActiveDisplayList, CGImage, CGPreflightScreenCaptureAccess,
CGSessionCopyCurrentDictionary,
};
use objc2_core_media::CMSampleBuffer;
use objc2_foundation::{NSError, NSObject, NSObjectProtocol, NSProcessInfo};
use objc2_screen_capture_kit::{
SCContentFilter, SCScreenshotManager, SCShareableContent, SCStream, SCStreamConfiguration,
SCStreamOutput, SCStreamOutputType, SCWindow,
};
use super::ax;
use crate::backend::WindowTarget;
use crate::error::ComputerError;
use crate::image_util::{
self, classify_sck_error, error_for_missing_window, CaptureFault, DisplayFacts, Layer,
};
use crate::tree::NodeFrame;
use crate::types::{Frame, RawCapture};
const TIMEOUT: Duration = Duration::from_secs(5);
const SCREEN_RECORDING: &str = "Screen Recording is off. System Settings → Privacy & Security → Screen Recording → enable the app named by computer-use doctor";
const PIXEL_BGRA: u32 = 0x4247_5241;
pub(crate) fn capture_window(target: &WindowTarget) -> Result<RawCapture, ComputerError> {
if target.bounds.width == 0 || target.bounds.height == 0 {
return Err(ComputerError::InvalidArguments(
"window bounds are empty".into(),
));
}
if !CGPreflightScreenCaptureAccess() {
return Err(ComputerError::PermissionRequired(SCREEN_RECORDING.into()));
}
if let Some(error) = session_display_block() {
return Err(error);
}
match capture_sck(target) {
Ok(raw) => Ok(raw),
Err(error) if is_terminal(&error) => Err(error),
Err(error) => fallback_crop(target, &error),
}
}
fn capture_sck(target: &WindowTarget) -> Result<RawCapture, ComputerError> {
let mut warnings = Vec::new();
let (_content, windows) = shareable_with_window(target.window_id)?;
let parent = windows
.iter()
.find(|window| window_id(window) == target.window_id)
.ok_or_else(|| {
ComputerError::WindowNotFound(format!(
"cg window {} is not in SCShareableContent",
target.window_id
))
})?
.clone();
let scale = backing_scale(target.bounds.x, target.bounds.y);
let parent_image = capture_layer(&parent, target.bounds.width, target.bounds.height, scale)?;
let mut layers = vec![Layer {
image: parent_image,
origin_x: target.bounds.x,
origin_y: target.bounds.y,
logical_width: target.bounds.width,
logical_height: target.bounds.height,
}];
for frame in ax::attached_sheets(target.pid, target.bounds) {
let Some(sheet) = windows.iter().find(|window| {
window_id(window) != target.window_id
&& window_pid(window) == Some(target.pid)
&& frames_close(window_frame(window), frame)
}) else {
warnings.push("an accessibility sheet had no ScreenCaptureKit window".into());
continue;
};
let width = positive_points(frame.width);
let height = positive_points(frame.height);
match capture_layer(sheet, width, height, scale) {
Ok(image) => layers.push(Layer {
image,
origin_x: round_i32(frame.x),
origin_y: round_i32(frame.y),
logical_width: width,
logical_height: height,
}),
Err(error) if is_terminal(&error) => return Err(error),
Err(error) => warnings.push(format!("sheet capture failed: {error}")),
}
}
let (image, origin_x, origin_y, logical_width, logical_height) = if layers.len() == 1 {
let layer = layers.pop().expect("parent layer");
(
layer.image,
layer.origin_x,
layer.origin_y,
layer.logical_width,
layer.logical_height,
)
} else {
let composite = image_util::composite_layers(&layers, scale)?;
(
composite.image,
composite.origin_x,
composite.origin_y,
composite.logical_width,
composite.logical_height,
)
};
Ok(RawCapture {
frame: Frame::from_image(image, scale),
origin_x,
origin_y,
logical_width,
logical_height,
capture_source: "sck".into(),
occluded: false,
warnings,
})
}
fn shareable_with_window(
id: u32,
) -> Result<(Retained<SCShareableContent>, Vec<Retained<SCWindow>>), ComputerError> {
let first_ok = match shareable(true) {
Ok(content) => {
let windows = sc_windows(&content);
if windows.iter().any(|window| window_id(window) == id) {
return Ok((content, windows));
}
true
}
Err(error) if is_terminal(&error) => return Err(error),
Err(_) => false,
};
match shareable(false) {
Ok(content) => {
let windows = sc_windows(&content);
if windows.iter().any(|window| window_id(window) == id) {
return Ok((content, windows));
}
let facts = probe_facts(sc_display_count(&content), windows.len());
if first_ok || image_util::display_block_reason(facts).is_some() {
return Err(error_for_missing_window(facts, id));
}
Err(ComputerError::ExecutionFailed(format!(
"cg window {id} was not listed and a shareable-content query failed"
)))
}
Err(error) => Err(error),
}
}
pub(crate) fn session_display_block() -> Option<ComputerError> {
let facts = DisplayFacts {
screen_locked: session_screen_locked(),
display_asleep: any_display_asleep(),
display_count: None,
shareable_window_count: None,
};
image_util::display_block_reason(facts)
.map(|reason| ComputerError::DisplayUnavailable(reason.into()))
}
fn probe_facts(display_count: usize, window_count: usize) -> DisplayFacts {
DisplayFacts {
screen_locked: session_screen_locked(),
display_asleep: any_display_asleep(),
display_count: Some(display_count),
shareable_window_count: Some(window_count),
}
}
fn session_screen_locked() -> bool {
let Some(dict) = CGSessionCopyCurrentDictionary() else {
return false;
};
let key = CFString::from_str("CGSSessionScreenIsLocked");
let value = unsafe { CFDictionary::value(&dict, &*key as *const CFString as *const c_void) };
if value.is_null() {
return false;
}
let Some(truth) = (unsafe { kCFBooleanTrue }) else {
return false;
};
std::ptr::eq(value, truth as *const CFBoolean as *const c_void)
}
fn any_display_asleep() -> bool {
let mut ids = [0u32; 16];
let mut count = 0u32;
let error = unsafe { CGGetActiveDisplayList(16, ids.as_mut_ptr(), &mut count) };
if error != CGError::Success {
return false;
}
let count = count as usize;
if count > ids.len() {
return false;
}
ids.iter().take(count).any(|id| CGDisplayIsAsleep(*id))
}
fn sc_display_count(content: &SCShareableContent) -> usize {
let displays = unsafe { content.displays() };
displays.count()
}
fn shareable(on_screen_only: bool) -> Result<Retained<SCShareableContent>, ComputerError> {
let (sender, receiver) = mpsc::channel();
let block = RcBlock::new(
move |content: *mut SCShareableContent, error: *mut NSError| {
let result = if !error.is_null() {
Err(nserror_to_computer(unsafe { &*error }))
} else if content.is_null() {
Err(ComputerError::ExecutionFailed(
"SCShareableContent was null".into(),
))
} else {
match unsafe { Retained::retain(content) } {
Some(retained) => Ok(Retained::into_raw(retained)),
None => Err(ComputerError::ExecutionFailed(
"SCShareableContent retain failed".into(),
)),
}
};
let _ = sender.send(result);
},
);
unsafe {
SCShareableContent::getShareableContentExcludingDesktopWindows_onScreenWindowsOnly_completionHandler(
true,
on_screen_only,
&block,
);
}
let ptr = take_or_forget(receiver, block)??;
unsafe { Retained::from_raw(ptr) }
.ok_or_else(|| ComputerError::ExecutionFailed("SCShareableContent pointer was null".into()))
}
fn sc_windows(content: &SCShareableContent) -> Vec<Retained<SCWindow>> {
let array = unsafe { content.windows() };
let mut windows = Vec::with_capacity(array.count());
for index in 0..array.count() {
windows.push(array.objectAtIndex(index));
}
windows
}
fn capture_layer(
window: &SCWindow,
points_w: u32,
points_h: u32,
scale: f64,
) -> Result<image::RgbaImage, ComputerError> {
let config = stream_config(px_len(points_w, scale), px_len(points_h, scale));
if macos_14_or_newer() {
match capture_manager(window, &config) {
Ok(image) => Ok(image),
Err(error) if is_terminal(&error) => Err(error),
Err(manager_error) => match capture_stream(window, &config) {
Ok(image) => Ok(image),
Err(error) if is_terminal(&error) => Err(error),
Err(stream_error) => Err(ComputerError::ExecutionFailed(format!(
"screenshot manager: {manager_error}; stream: {stream_error}"
))),
},
}
} else {
capture_stream(window, &config)
}
}
fn stream_config(width: usize, height: usize) -> Retained<SCStreamConfiguration> {
let config = unsafe { SCStreamConfiguration::new() };
unsafe {
config.setWidth(width.max(1));
config.setHeight(height.max(1));
config.setShowsCursor(false);
config.setCapturesAudio(false);
config.setIgnoreShadowsSingleWindow(true);
config.setPixelFormat(PIXEL_BGRA);
}
config
}
fn capture_manager(
window: &SCWindow,
config: &SCStreamConfiguration,
) -> Result<image::RgbaImage, ComputerError> {
let filter = unsafe {
SCContentFilter::initWithDesktopIndependentWindow(SCContentFilter::alloc(), window)
};
let (sender, receiver) = mpsc::channel();
let block = RcBlock::new(move |image: *mut CGImage, error: *mut NSError| {
let result = if !error.is_null() {
Err(nserror_to_computer(unsafe { &*error }))
} else {
cgimage_result(image)
};
let _ = sender.send(result);
});
unsafe {
SCScreenshotManager::captureImageWithFilter_configuration_completionHandler(
&filter,
config,
Some(&block),
);
}
take_or_forget(receiver, block)?
}
fn cgimage_result(image: *mut CGImage) -> Result<image::RgbaImage, ComputerError> {
let Some(ptr) = NonNull::new(image) else {
return Err(ComputerError::ExecutionFailed("CGImage was null".into()));
};
let image = unsafe { CFRetained::retain(ptr) };
cgimage_to_rgba(&image).map_err(ComputerError::ExecutionFailed)
}
fn cgimage_to_rgba(image: &CGImage) -> Result<image::RgbaImage, String> {
let width = objc2_core_graphics::CGImage::width(Some(image));
let height = objc2_core_graphics::CGImage::height(Some(image));
if width == 0 || height == 0 || width > 32_768 || height > 32_768 {
return Err(format!("CGImage size {width}x{height} is out of range"));
}
let width_u = u32::try_from(width).unwrap_or(u32::MAX);
let height_u = u32::try_from(height).unwrap_or(u32::MAX);
let bytes_per_row = width.saturating_mul(4);
let mut buffer = vec![0u8; bytes_per_row.saturating_mul(height)];
let space = CGColorSpace::new_device_rgb().ok_or("device RGB color space was null")?;
let context = unsafe {
CGBitmapContextCreate(
buffer.as_mut_ptr().cast(),
width,
height,
8,
bytes_per_row,
Some(&space),
0x4001,
)
}
.ok_or("CGBitmapContextCreate failed")?;
let rect = CGRect {
origin: CGPoint { x: 0.0, y: 0.0 },
size: CGSize {
width: width as CGFloat,
height: height as CGFloat,
},
};
CGContext::draw_image(Some(&context), rect, Some(image));
drop(context);
image::RgbaImage::from_raw(width_u, height_u, buffer)
.ok_or_else(|| "RGBA buffer length does not match the CGImage".into())
}
struct FrameSinkIvars {
sender: Mutex<Option<mpsc::Sender<Result<image::RgbaImage, String>>>>,
}
define_class!(
#[unsafe(super = NSObject)]
#[thread_kind = AnyThread]
#[ivars = FrameSinkIvars]
struct FrameSink;
unsafe impl NSObjectProtocol for FrameSink {}
unsafe impl SCStreamOutput for FrameSink {
#[unsafe(method(stream:didOutputSampleBuffer:ofType:))]
unsafe fn stream_did_output_sample_buffer_of_type(
&self,
_stream: &SCStream,
sample_buffer: &CMSampleBuffer,
kind: SCStreamOutputType,
) {
if kind != SCStreamOutputType::Screen {
return;
}
let image = rgba_from_sample(sample_buffer);
let sender = self
.ivars()
.sender
.lock()
.ok()
.and_then(|mut slot| slot.take());
if let Some(sender) = sender {
let _ = sender.send(image);
}
}
}
);
impl FrameSink {
fn new(sender: mpsc::Sender<Result<image::RgbaImage, String>>) -> Retained<Self> {
let this = Self::alloc().set_ivars(FrameSinkIvars {
sender: Mutex::new(Some(sender)),
});
unsafe { msg_send![super(this), init] }
}
}
fn capture_stream(
window: &SCWindow,
config: &SCStreamConfiguration,
) -> Result<image::RgbaImage, ComputerError> {
let filter = unsafe {
SCContentFilter::initWithDesktopIndependentWindow(SCContentFilter::alloc(), window)
};
let (sender, receiver) = mpsc::channel();
let sink = FrameSink::new(sender);
let stream = unsafe {
SCStream::initWithFilter_configuration_delegate(SCStream::alloc(), &filter, config, None)
};
let output = ProtocolObject::from_ref(&*sink);
let queue = DispatchQueue::new("computer-use.sck", DispatchQueueAttr::SERIAL);
unsafe {
stream.addStreamOutput_type_sampleHandlerQueue_error(
output,
SCStreamOutputType::Screen,
Some(&queue),
)
}
.map_err(|error| nserror_to_computer(&error))?;
start_stream(&stream)?;
let image = match wait_channel(receiver) {
Ok(result) => result.map_err(ComputerError::ExecutionFailed),
Err(error) => Err(error),
};
let _ = stop_stream(&stream);
drop(queue);
drop(sink);
image
}
fn start_stream(stream: &SCStream) -> Result<(), ComputerError> {
let (sender, receiver) = mpsc::channel();
let block = RcBlock::new(move |error: *mut NSError| {
let result = if error.is_null() {
Ok(())
} else {
Err(nserror_to_computer(unsafe { &*error }))
};
let _ = sender.send(result);
});
unsafe { stream.startCaptureWithCompletionHandler(Some(&block)) };
take_or_forget(receiver, block)?
}
fn stop_stream(stream: &SCStream) -> Result<(), ComputerError> {
let (sender, receiver) = mpsc::channel();
let block = RcBlock::new(move |error: *mut NSError| {
let result = if error.is_null() {
Ok(())
} else {
Err(nserror_to_computer(unsafe { &*error }))
};
let _ = sender.send(result);
});
unsafe { stream.stopCaptureWithCompletionHandler(Some(&block)) };
take_or_forget(receiver, block)?
}
fn rgba_from_sample(sample: &CMSampleBuffer) -> Result<image::RgbaImage, String> {
unsafe {
let buffer = CMSampleBufferGetImageBuffer(std::ptr::from_ref(sample).cast());
if buffer.is_null() {
return Err("sample had no image buffer".into());
}
let status = CVPixelBufferLockBaseAddress(buffer, 1);
if status != 0 {
return Err(format!("CVPixelBufferLockBaseAddress failed ({status})"));
}
let width = CVPixelBufferGetWidth(buffer);
let height = CVPixelBufferGetHeight(buffer);
let stride = CVPixelBufferGetBytesPerRow(buffer);
let format = CVPixelBufferGetPixelFormatType(buffer);
let base = CVPixelBufferGetBaseAddress(buffer);
let result = copy_locked_bgra(base, width, height, stride, format);
CVPixelBufferUnlockBaseAddress(buffer, 1);
result
}
}
unsafe fn copy_locked_bgra(
base: *mut c_void,
width: usize,
height: usize,
stride: usize,
format: u32,
) -> Result<image::RgbaImage, String> {
if base.is_null() {
return Err("pixel buffer base address was null".into());
}
if format != PIXEL_BGRA {
return Err(format!("unexpected pixel format {format:#x}"));
}
if width == 0 || height == 0 || width > 32_768 || height > 32_768 || stride < width * 4 {
return Err(format!(
"pixel buffer {width}x{height} stride {stride} is out of range"
));
}
let mut rgba = vec![0u8; width * height * 4];
for y in 0..height {
let row =
unsafe { std::slice::from_raw_parts(base.cast::<u8>().add(y * stride), width * 4) };
bgra_row_to_rgba(row, &mut rgba[y * width * 4..(y + 1) * width * 4]);
}
let width_u = u32::try_from(width).unwrap_or(u32::MAX);
let height_u = u32::try_from(height).unwrap_or(u32::MAX);
image::RgbaImage::from_raw(width_u, height_u, rgba)
.ok_or_else(|| "RGBA buffer length does not match the sample".into())
}
fn bgra_row_to_rgba(bgra_row: &[u8], rgba_row: &mut [u8]) {
debug_assert_eq!(bgra_row.len(), rgba_row.len());
debug_assert_eq!(bgra_row.len() % 4, 0);
let pixels = bgra_row.len() / 4;
for pixel in 0..pixels {
let src = pixel * 4;
let dst = pixel * 4;
rgba_row[dst] = bgra_row[src + 2];
rgba_row[dst + 1] = bgra_row[src + 1];
rgba_row[dst + 2] = bgra_row[src];
rgba_row[dst + 3] = bgra_row[src + 3];
}
}
fn fallback_crop(
target: &WindowTarget,
error: &ComputerError,
) -> Result<RawCapture, ComputerError> {
let mut raw = super::super::capture::crop_window(target.bounds)?;
raw.warnings.insert(
0,
format!("ScreenCaptureKit failed ({error}); cropped the desktop image instead"),
);
Ok(raw)
}
fn nserror_to_computer(error: &NSError) -> ComputerError {
let domain = error.domain().to_string();
let code = error.code() as i64;
let message = error.localizedDescription().to_string();
match classify_sck_error(&domain, code, &message) {
CaptureFault::Permission => ComputerError::PermissionRequired(format!(
"Screen Recording is off. System Settings → Privacy & Security → Screen Recording. {message}"
)),
CaptureFault::MissingWindow => ComputerError::WindowNotFound(message),
CaptureFault::Other => {
ComputerError::ExecutionFailed(format!("{domain} {code}: {message}"))
}
}
}
fn is_terminal(error: &ComputerError) -> bool {
matches!(
error,
ComputerError::PermissionRequired(_)
| ComputerError::WindowNotFound(_)
| ComputerError::DisplayUnavailable(_)
)
}
fn window_id(window: &SCWindow) -> u32 {
unsafe { window.windowID() }
}
fn window_pid(window: &SCWindow) -> Option<i32> {
let app = unsafe { window.owningApplication() }?;
Some(unsafe { app.processID() })
}
fn window_frame(window: &SCWindow) -> CGRect {
unsafe { window.frame() }
}
fn frames_close(rect: CGRect, frame: NodeFrame) -> bool {
(rect.origin.x - frame.x).abs() <= 2.0
&& (rect.origin.y - frame.y).abs() <= 2.0
&& (rect.size.width - frame.width).abs() <= 2.0
&& (rect.size.height - frame.height).abs() <= 2.0
}
fn backing_scale(origin_x: i32, origin_y: i32) -> f64 {
let Some(marker) = MainThreadMarker::new() else {
return 1.0;
};
let screens = NSScreen::screens(marker);
if screens.count() == 0 {
return 1.0;
}
let primary_height = primary_screen_height(&screens);
let point_x = origin_x as f64;
let point_y = primary_height - origin_y as f64;
for index in 0..screens.count() {
let screen = screens.objectAtIndex(index);
let frame = screen.frame();
let max_x = frame.origin.x + frame.size.width;
let max_y = frame.origin.y + frame.size.height;
if point_x >= frame.origin.x
&& point_y >= frame.origin.y
&& point_x < max_x
&& point_y < max_y
{
return positive_scale(screen.backingScaleFactor());
}
}
NSScreen::mainScreen(marker)
.map(|screen| positive_scale(screen.backingScaleFactor()))
.unwrap_or(1.0)
}
fn primary_screen_height(screens: &objc2_foundation::NSArray<NSScreen>) -> f64 {
for index in 0..screens.count() {
let frame = screens.objectAtIndex(index).frame();
if frame.origin.x == 0.0 && frame.origin.y == 0.0 {
return frame.size.height;
}
}
screens.objectAtIndex(0).frame().size.height
}
fn positive_scale(scale: CGFloat) -> f64 {
if scale.is_finite() && scale > 0.0 {
scale
} else {
1.0
}
}
fn macos_14_or_newer() -> bool {
NSProcessInfo::processInfo()
.operatingSystemVersion()
.majorVersion
>= 14
}
fn px_len(points: u32, scale: f64) -> usize {
let value = (f64::from(points.max(1)) * scale).round();
if !value.is_finite() || value < 1.0 {
1
} else if value > 32_768.0 {
32_768
} else {
value as usize
}
}
fn positive_points(value: f64) -> u32 {
if !value.is_finite() || value <= 0.0 {
1
} else if value > f64::from(u32::MAX) {
u32::MAX
} else {
value.round() as u32
}
}
fn round_i32(value: f64) -> i32 {
if !value.is_finite() {
0
} else if value > f64::from(i32::MAX) {
i32::MAX
} else if value < f64::from(i32::MIN) {
i32::MIN
} else {
value.round() as i32
}
}
fn take_or_forget<T>(receiver: mpsc::Receiver<T>, block: impl Sized) -> Result<T, ComputerError> {
match wait_channel(receiver) {
Ok(value) => Ok(value),
Err(error) => {
std::mem::forget(block);
Err(error)
}
}
}
fn wait_channel<T>(receiver: mpsc::Receiver<T>) -> Result<T, ComputerError> {
let deadline = Instant::now() + TIMEOUT;
loop {
match receiver.try_recv() {
Ok(value) => return Ok(value),
Err(mpsc::TryRecvError::Disconnected) => {
return Err(ComputerError::ExecutionFailed(
"capture callback dropped".into(),
));
}
Err(mpsc::TryRecvError::Empty) => {
if Instant::now() >= deadline {
return Err(ComputerError::ExecutionFailed(
"screenshot timed out after 5s".into(),
));
}
pump();
}
}
}
}
fn pump() {
if MainThreadMarker::new().is_none() {
thread::sleep(Duration::from_millis(20));
return;
}
let mode = unsafe { kCFRunLoopDefaultMode };
let _ = CFRunLoop::run_in_mode(mode, 0.05, false);
}
#[link(name = "CoreMedia", kind = "framework")]
extern "C" {
fn CMSampleBufferGetImageBuffer(sample: *const c_void) -> *mut c_void;
}
#[link(name = "CoreVideo", kind = "framework")]
extern "C" {
fn CVPixelBufferLockBaseAddress(buffer: *mut c_void, flags: u64) -> i32;
fn CVPixelBufferUnlockBaseAddress(buffer: *mut c_void, flags: u64) -> i32;
fn CVPixelBufferGetBaseAddress(buffer: *mut c_void) -> *mut c_void;
fn CVPixelBufferGetWidth(buffer: *mut c_void) -> usize;
fn CVPixelBufferGetHeight(buffer: *mut c_void) -> usize;
fn CVPixelBufferGetBytesPerRow(buffer: *mut c_void) -> usize;
fn CVPixelBufferGetPixelFormatType(buffer: *mut c_void) -> u32;
}
#[cfg(test)]
mod tests {
use super::bgra_row_to_rgba;
#[test]
fn bgra_row_to_rgba_swaps_red_and_blue_per_pixel() {
let bgra = [
0x10, 0x20, 0x30, 0xFF, 0xAA, 0xBB, 0xCC, 0x40, ];
let mut rgba = [0_u8; 8];
bgra_row_to_rgba(&bgra, &mut rgba);
assert_eq!(
rgba,
[
0x30, 0x20, 0x10, 0xFF, 0xCC, 0xBB, 0xAA, 0x40,
]
);
}
#[test]
fn bgra_row_to_rgba_preserves_alpha() {
let bgra = [0x01, 0x02, 0x03, 0x77];
let mut rgba = [0_u8; 4];
bgra_row_to_rgba(&bgra, &mut rgba);
assert_eq!(rgba, [0x03, 0x02, 0x01, 0x77]);
}
#[test]
fn bgra_row_to_rgba_handles_an_empty_row() {
let bgra: [u8; 0] = [];
let mut rgba: [u8; 0] = [];
bgra_row_to_rgba(&bgra, &mut rgba);
}
#[test]
fn bgra_row_to_rgba_does_not_reorder_rows() {
let row_top: [u8; 4] = [0xFF, 0x00, 0x00, 0xFF]; let row_bottom: [u8; 4] = [0x00, 0xFF, 0x00, 0xFF]; let bgra = [row_top.as_slice(), row_bottom.as_slice()].concat();
let mut rgba_top = [0_u8; 4];
let mut rgba_bottom = [0_u8; 4];
bgra_row_to_rgba(&row_top, &mut rgba_top);
bgra_row_to_rgba(&row_bottom, &mut rgba_bottom);
assert_eq!(rgba_top, [0x00, 0x00, 0xFF, 0xFF]); assert_eq!(rgba_bottom, [0x00, 0xFF, 0x00, 0xFF]); let _ = bgra; }
}