#![allow(deprecated)]
use objc2_core_foundation::{CFRetained, CGPoint, CGRect, CGSize};
use objc2_core_graphics::{
CGBitmapInfo, CGDataProvider, CGDirectDisplayID, CGDisplayCreateImage, CGImage,
CGImageAlphaInfo, CGMainDisplayID, CGWindowImageOption, CGWindowListCreateImage,
CGWindowListOption,
};
use crate::{Frame, MonitorId, NativeFrame, PixelFormat, PlatformError, Rect, Result};
pub(crate) fn capture_screen(monitor: MonitorId) -> Result<Frame> {
let native = capture_screen_native(monitor)?;
Ok(native_to_packed_rgba(native))
}
pub(crate) fn capture_screen_native(monitor: MonitorId) -> Result<NativeFrame> {
let monitor_bounds = bounds_and_scale_for(monitor).0;
let image: CFRetained<CGImage> = match overlay_window_id_for(monitor) {
Some(wid) => {
let bounds = CGRect {
origin: CGPoint {
x: monitor_bounds.x as f64,
y: monitor_bounds.y as f64,
},
size: CGSize {
width: monitor_bounds.w as f64,
height: monitor_bounds.h as f64,
},
};
CGWindowListCreateImage(
bounds,
CGWindowListOption::OptionOnScreenBelowWindow,
wid,
CGWindowImageOption::Default,
)
.ok_or(PlatformError::Unsupported {
what: "macos: CGWindowListCreateImage returned null \
(Screen Recording permission may not be granted)",
})?
}
None => {
let display_id = cg_display_id_for(monitor);
CGDisplayCreateImage(display_id).ok_or(PlatformError::Unsupported {
what: "macos: CGDisplayCreateImage returned null \
(Screen Recording permission may not be granted)",
})?
}
};
let width = CGImage::width(Some(&image)) as u32;
let height = CGImage::height(Some(&image)) as u32;
let stride = CGImage::bytes_per_row(Some(&image)) as u32;
let bitmap = CGImage::bitmap_info(Some(&image));
let alpha = CGImage::alpha_info(Some(&image));
let format = pixel_format_from_cg(bitmap, alpha);
let provider = CGImage::data_provider(Some(&image)).ok_or(PlatformError::Unsupported {
what: "macos: CGImage::data_provider returned null",
})?;
let data: CFRetained<objc2_core_foundation::CFData> = CGDataProvider::data(Some(&provider))
.ok_or(PlatformError::Unsupported {
what: "macos: CGDataProvider::data returned null",
})?;
let len = data.length() as usize;
let ptr = data.byte_ptr();
if ptr.is_null() {
return Err(PlatformError::Unsupported {
what: "macos: CFDataGetBytePtr returned null",
});
}
let pixels = unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec();
let (bounds, scale_factor) = bounds_and_scale_for(monitor);
Ok(NativeFrame {
width,
height,
stride,
format,
bounds,
scale_factor,
pixels,
})
}
fn cg_display_id_for(monitor: MonitorId) -> CGDirectDisplayID {
use objc2::MainThreadMarker;
use objc2_app_kit::NSScreen;
use objc2_foundation::NSString;
let mtm = match MainThreadMarker::new() {
Some(m) => m,
None => return CGMainDisplayID(),
};
let screens = NSScreen::screens(mtm);
let idx = monitor.0 as usize;
let Some(screen) = screens.iter().nth(idx) else {
return CGMainDisplayID();
};
let desc = screen.deviceDescription();
let key = NSString::from_str("NSScreenNumber");
let value = desc.objectForKey(&key);
let Some(value) = value else {
return CGMainDisplayID();
};
let id: u32 = unsafe { objc2::msg_send![&*value, unsignedIntValue] };
if id == 0 { CGMainDisplayID() } else { id }
}
fn overlay_window_id_for(monitor: MonitorId) -> Option<u32> {
super::app::run_on_main_sync(move || {
super::with_main_state(|s| {
s.overlays.get(&monitor).and_then(|o| {
o.window.isVisible().then(|| o.window.windowNumber() as u32)
})
})
})
}
fn bounds_and_scale_for(monitor: MonitorId) -> (Rect, f64) {
let monitors = super::monitor::monitors().unwrap_or_default();
monitors
.into_iter()
.find(|m| m.id == monitor)
.map(|m| (m.bounds, m.scale_factor))
.unwrap_or((Rect::default(), 1.0))
}
fn pixel_format_from_cg(bitmap: CGBitmapInfo, alpha: CGImageAlphaInfo) -> PixelFormat {
let little_endian =
(bitmap.0 & CGBitmapInfo::ByteOrder32Little.0) == CGBitmapInfo::ByteOrder32Little.0;
let alpha_first = matches!(
alpha,
CGImageAlphaInfo::PremultipliedFirst
| CGImageAlphaInfo::First
| CGImageAlphaInfo::NoneSkipFirst
);
let has_alpha = !matches!(
alpha,
CGImageAlphaInfo::NoneSkipFirst | CGImageAlphaInfo::NoneSkipLast
);
match (little_endian, alpha_first, has_alpha) {
(true, true, true) => PixelFormat::Bgra8,
(true, true, false) => PixelFormat::Bgrx8,
(true, false, true) => PixelFormat::Rgba8,
(true, false, false) => PixelFormat::Rgbx8,
(false, true, true) => PixelFormat::Xrgb8,
(false, true, false) => PixelFormat::Xrgb8,
(false, false, true) => PixelFormat::Xbgr8,
(false, false, false) => PixelFormat::Xbgr8,
}
}
fn native_to_packed_rgba(native: NativeFrame) -> Frame {
let w = native.width as usize;
let h = native.height as usize;
let stride = native.stride as usize;
let row_used = w * 4;
let mut out = Vec::with_capacity(w * h * 4);
let swizzle = matches!(
native.format,
PixelFormat::Bgra8 | PixelFormat::Bgrx8 | PixelFormat::Xbgr8
);
let drop_alpha_skip_first = matches!(native.format, PixelFormat::Xrgb8);
for row in 0..h {
let row_start = row * stride;
let row_end = row_start + row_used;
if row_end > native.pixels.len() {
break;
}
let src = &native.pixels[row_start..row_end];
if swizzle {
for chunk in src.chunks_exact(4) {
out.extend_from_slice(&[chunk[2], chunk[1], chunk[0], chunk[3]]);
}
} else if drop_alpha_skip_first {
for chunk in src.chunks_exact(4) {
out.extend_from_slice(&[chunk[1], chunk[2], chunk[3], chunk[0]]);
}
} else {
out.extend_from_slice(src);
}
}
Frame {
width: native.width,
height: native.height,
scale_factor: native.scale_factor,
bounds: native.bounds,
pixels: out,
}
}