#![allow(
dead_code,
reason = "unused when the mock-loader feature selects the DLL backend instead"
)]
use super::permissions::{self, Permission};
use crate::error::{Error, Result};
use crate::{Point, Rect};
use objc2_core_foundation::{CFData, CFRetained, CGPoint, CGRect, CGSize};
use objc2_core_graphics::{CGDataProvider, CGImage, CGImageAlphaInfo, CGMainDisplayID};
pub(crate) struct Capture {
data: CFRetained<CFData>,
area: Rect,
stride: usize,
depth: usize,
pixel_w: usize,
pixel_h: usize,
bgra: bool,
}
impl Capture {
pub(crate) fn of(area: Rect) -> Result<Self> {
if area.w <= 0 || area.h <= 0 {
return Err(Error::Platform {
operation: "capture an empty screen region",
platform: "macOS",
});
}
permissions::require(Permission::ScreenRecording)?;
let rect = CGRect::new(
CGPoint {
x: f64::from(area.x),
y: f64::from(area.y),
},
CGSize {
width: f64::from(area.w),
height: f64::from(area.h),
},
);
#[allow(
deprecated,
reason = "ScreenCaptureKit has no synchronous single-shot equivalent"
)]
let image = objc2_core_graphics::CGDisplayCreateImageForRect(CGMainDisplayID(), rect)
.ok_or(Error::Platform {
operation: "capture the screen",
platform: "macOS",
})?;
Self::from_image(&image, area)
}
fn from_image(image: &CGImage, area: Rect) -> Result<Self> {
let unreadable = || Error::Platform {
operation: "read back the captured pixels",
platform: "macOS",
};
let provider = CGImage::data_provider(Some(image)).ok_or_else(unreadable)?;
let data = CGDataProvider::data(Some(&provider)).ok_or_else(unreadable)?;
let bits = CGImage::bits_per_pixel(Some(image));
if bits % 8 != 0 || bits < 24 {
return Err(Error::Platform {
operation: "capture the screen in a usable pixel format",
platform: "macOS",
});
}
let bgra = matches!(
CGImage::alpha_info(Some(image)),
CGImageAlphaInfo::First
| CGImageAlphaInfo::NoneSkipFirst
| CGImageAlphaInfo::PremultipliedFirst
);
Ok(Self {
data,
area,
stride: CGImage::bytes_per_row(Some(image)),
depth: bits / 8,
pixel_w: CGImage::width(Some(image)),
pixel_h: CGImage::height(Some(image)),
bgra,
})
}
fn scale(&self) -> usize {
(self.pixel_w / (self.area.w.max(1) as usize)).max(1)
}
pub(crate) fn color_at(&self, p: Point) -> Option<u32> {
if p.x < self.area.x
|| p.y < self.area.y
|| p.x >= self.area.x + self.area.w
|| p.y >= self.area.y + self.area.h
{
return None;
}
let scale = self.scale();
let px = (p.x - self.area.x) as usize * scale;
let py = (p.y - self.area.y) as usize * scale;
self.color_at_pixel(px, py)
}
fn color_at_pixel(&self, px: usize, py: usize) -> Option<u32> {
if px >= self.pixel_w || py >= self.pixel_h {
return None;
}
let offset = py * self.stride + px * self.depth;
let bytes = self.bytes();
let pixel = bytes.get(offset..offset + self.depth)?;
let (r, g, b) = if self.bgra {
(pixel[2], pixel[1], pixel[0])
} else {
(pixel[0], pixel[1], pixel[2])
};
Some((u32::from(r) << 16) | (u32::from(g) << 8) | u32::from(b))
}
fn bytes(&self) -> &[u8] {
let len = self.data.length().max(0) as usize;
let ptr = self.data.byte_ptr();
if ptr.is_null() || len == 0 {
return &[];
}
unsafe { std::slice::from_raw_parts(ptr, len) }
}
pub(crate) fn search(&self, colour: u32, variation: u32, step: u32) -> Option<Point> {
let step = step.max(1) as i32;
let mut y = self.area.y;
while y < self.area.y + self.area.h {
let mut x = self.area.x;
while x < self.area.x + self.area.w {
let p = Point::new(x, y);
if self
.color_at(p)
.is_some_and(|c| within(c, colour, variation))
{
return Some(p);
}
x += step;
}
y += step;
}
None
}
pub(crate) fn checksum(&self, step: u32) -> u32 {
let step = step.max(1) as i32;
let mut hash: u32 = 0x811c_9dc5;
let mut y = self.area.y;
while y < self.area.y + self.area.h {
let mut x = self.area.x;
while x < self.area.x + self.area.w {
let colour = self.color_at(Point::new(x, y)).unwrap_or(0);
for byte in colour.to_le_bytes() {
hash ^= u32::from(byte);
hash = hash.wrapping_mul(0x0100_0193);
}
x += step;
}
y += step;
}
hash
}
}
fn within(a: u32, b: u32, variation: u32) -> bool {
let channel = |c: u32, shift: u32| (c >> shift) & 0xFF;
[16u32, 8, 0]
.iter()
.all(|&s| channel(a, s).abs_diff(channel(b, s)) <= variation)
}
pub(crate) fn color_at(p: Point) -> Result<u32> {
let capture = Capture::of(Rect::new(p.x, p.y, 1, 1))?;
capture.color_at(p).ok_or(Error::Platform {
operation: "read a pixel that lies outside every display",
platform: "macOS",
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shade_variation_is_per_channel_not_a_distance() {
assert!(within(0x40_80_C0, 0x40_80_C0, 0));
assert!(within(0x40_80_C0, 0x45_85_C5, 5));
assert!(!within(0x40_80_C0, 0x40_80_D0, 5));
}
#[test]
fn variation_is_symmetric() {
assert_eq!(
within(0x10_10_10, 0x20_20_20, 16),
within(0x20_20_20, 0x10_10_10, 16)
);
}
#[test]
fn an_empty_region_is_refused_before_any_permission_is_asked_for() {
let result = Capture::of(Rect::new(0, 0, 0, 0));
assert!(matches!(result, Err(Error::Platform { .. })));
}
}