use crate::error::SCError;
use crate::stream::configuration::SCStreamConfiguration;
use crate::stream::content_filter::SCContentFilter;
use crate::utils::completion::{error_from_cstr, SyncCompletion};
use std::ffi::c_void;
#[cfg(feature = "macos_15_2")]
use crate::cg::CGRect;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ImageFormat {
Png,
Jpeg(f32),
Tiff,
Gif,
Bmp,
Heic(f32),
}
impl ImageFormat {
fn to_format_id(self) -> i32 {
match self {
Self::Png => 0,
Self::Jpeg(_) => 1,
Self::Tiff => 2,
Self::Gif => 3,
Self::Bmp => 4,
Self::Heic(_) => 5,
}
}
fn quality(self) -> f32 {
match self {
Self::Jpeg(q) | Self::Heic(q) => q.clamp(0.0, 1.0),
_ => 1.0,
}
}
#[must_use]
pub const fn extension(&self) -> &'static str {
match self {
Self::Png => "png",
Self::Jpeg(_) => "jpg",
Self::Tiff => "tiff",
Self::Gif => "gif",
Self::Bmp => "bmp",
Self::Heic(_) => "heic",
}
}
}
extern "C" fn image_callback(
image_ptr: *const c_void,
error_ptr: *const i8,
user_data: *mut c_void,
) {
if !error_ptr.is_null() {
let error = unsafe { error_from_cstr(error_ptr) };
unsafe { SyncCompletion::<CGImage>::complete_err(user_data, error) };
} else if !image_ptr.is_null() {
unsafe { SyncCompletion::complete_ok(user_data, CGImage::from_ptr(image_ptr)) };
} else {
unsafe { SyncCompletion::<CGImage>::complete_err(user_data, "Unknown error".to_string()) };
}
}
extern "C" fn buffer_callback(
buffer_ptr: *const c_void,
error_ptr: *const i8,
user_data: *mut c_void,
) {
if !error_ptr.is_null() {
let error = unsafe { error_from_cstr(error_ptr) };
unsafe { SyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(user_data, error) };
} else if !buffer_ptr.is_null() {
let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(buffer_ptr.cast_mut()) };
unsafe { SyncCompletion::complete_ok(user_data, buffer) };
} else {
unsafe {
SyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(
user_data,
"Unknown error".to_string(),
);
};
}
}
#[cfg(feature = "macos_26_0")]
extern "C" fn screenshot_output_callback(
output_ptr: *const c_void,
error_ptr: *const i8,
user_data: *mut c_void,
) {
if !error_ptr.is_null() {
let error = unsafe { error_from_cstr(error_ptr) };
unsafe { SyncCompletion::<SCScreenshotOutput>::complete_err(user_data, error) };
} else if !output_ptr.is_null() {
unsafe {
SyncCompletion::complete_ok(user_data, SCScreenshotOutput::from_ptr(output_ptr));
};
} else {
unsafe {
SyncCompletion::<SCScreenshotOutput>::complete_err(
user_data,
"Unknown error".to_string(),
);
};
}
}
pub struct CGImage {
ptr: *const c_void,
}
impl CGImage {
pub(crate) fn from_ptr(ptr: *const c_void) -> Self {
Self { ptr }
}
#[must_use]
pub fn width(&self) -> usize {
unsafe { crate::ffi::cgimage_get_width(self.ptr) }
}
#[must_use]
pub fn height(&self) -> usize {
unsafe { crate::ffi::cgimage_get_height(self.ptr) }
}
#[must_use]
pub fn as_ptr(&self) -> *const c_void {
self.ptr
}
pub fn rgba_data(&self) -> Result<Vec<u8>, SCError> {
let mut data_ptr: *const u8 = std::ptr::null();
let mut data_length: usize = 0;
let success = unsafe {
crate::ffi::cgimage_get_data(
self.ptr,
std::ptr::addr_of_mut!(data_ptr),
std::ptr::addr_of_mut!(data_length),
)
};
if !success || data_ptr.is_null() {
return Err(SCError::internal_error(
"Failed to extract pixel data from CGImage",
));
}
let data = unsafe { std::slice::from_raw_parts(data_ptr, data_length).to_vec() };
unsafe {
crate::ffi::cgimage_free_data(data_ptr.cast_mut());
}
Ok(data)
}
pub fn save_png(&self, path: &str) -> Result<(), SCError> {
self.save(path, ImageFormat::Png)
}
pub fn save(&self, path: &str, format: ImageFormat) -> Result<(), SCError> {
let c_path = std::ffi::CString::new(path)
.map_err(|_| SCError::internal_error("Path contains null bytes"))?;
let success = unsafe {
crate::ffi::cgimage_save_to_file(
self.ptr,
c_path.as_ptr(),
format.to_format_id(),
format.quality(),
)
};
if success {
Ok(())
} else {
Err(SCError::internal_error(format!(
"Failed to save image as {}",
format.extension().to_uppercase()
)))
}
}
}
impl Drop for CGImage {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe {
crate::ffi::cgimage_release(self.ptr);
}
}
}
}
impl std::fmt::Debug for CGImage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CGImage")
.field("width", &self.width())
.field("height", &self.height())
.finish()
}
}
unsafe impl Send for CGImage {}
unsafe impl Sync for CGImage {}
pub struct SCScreenshotManager;
impl SCScreenshotManager {
pub fn capture_image(
content_filter: &SCContentFilter,
configuration: &SCStreamConfiguration,
) -> Result<CGImage, SCError> {
let (completion, context) = SyncCompletion::<CGImage>::new();
unsafe {
crate::ffi::sc_screenshot_manager_capture_image(
content_filter.as_ptr(),
configuration.as_ptr(),
image_callback,
context,
);
}
completion.wait().map_err(SCError::ScreenshotError)
}
pub fn capture_sample_buffer(
content_filter: &SCContentFilter,
configuration: &SCStreamConfiguration,
) -> Result<crate::cm::CMSampleBuffer, SCError> {
let (completion, context) = SyncCompletion::<crate::cm::CMSampleBuffer>::new();
unsafe {
crate::ffi::sc_screenshot_manager_capture_sample_buffer(
content_filter.as_ptr(),
configuration.as_ptr(),
buffer_callback,
context,
);
}
completion.wait().map_err(SCError::ScreenshotError)
}
#[cfg(feature = "macos_15_2")]
pub fn capture_image_in_rect(rect: CGRect) -> Result<CGImage, SCError> {
let (completion, context) = SyncCompletion::<CGImage>::new();
unsafe {
crate::ffi::sc_screenshot_manager_capture_image_in_rect(
rect.x,
rect.y,
rect.width,
rect.height,
image_callback,
context,
);
}
completion.wait().map_err(SCError::ScreenshotError)
}
#[cfg(feature = "macos_26_0")]
pub fn capture_screenshot(
content_filter: &SCContentFilter,
configuration: &SCScreenshotConfiguration,
) -> Result<SCScreenshotOutput, SCError> {
let (completion, context) = SyncCompletion::<SCScreenshotOutput>::new();
unsafe {
crate::ffi::sc_screenshot_manager_capture_screenshot(
content_filter.as_ptr(),
configuration.as_ptr(),
screenshot_output_callback,
context,
);
}
completion.wait().map_err(SCError::ScreenshotError)
}
#[cfg(feature = "macos_26_0")]
pub fn capture_screenshot_in_rect(
rect: crate::cg::CGRect,
configuration: &SCScreenshotConfiguration,
) -> Result<SCScreenshotOutput, SCError> {
let (completion, context) = SyncCompletion::<SCScreenshotOutput>::new();
unsafe {
crate::ffi::sc_screenshot_manager_capture_screenshot_in_rect(
rect.x,
rect.y,
rect.width,
rect.height,
configuration.as_ptr(),
screenshot_output_callback,
context,
);
}
completion.wait().map_err(SCError::ScreenshotError)
}
}
#[cfg(feature = "macos_26_0")]
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SCScreenshotDisplayIntent {
#[default]
Canonical = 0,
Local = 1,
}
#[cfg(feature = "macos_26_0")]
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SCScreenshotDynamicRange {
#[default]
SDR = 0,
HDR = 1,
BothSDRAndHDR = 2,
}
#[cfg(feature = "macos_26_0")]
pub struct SCScreenshotConfiguration {
ptr: *const c_void,
}
#[cfg(feature = "macos_26_0")]
impl SCScreenshotConfiguration {
#[must_use]
pub fn new() -> Self {
let ptr = unsafe { crate::ffi::sc_screenshot_configuration_create() };
assert!(!ptr.is_null(), "Failed to create SCScreenshotConfiguration");
Self { ptr }
}
#[must_use]
#[allow(clippy::cast_possible_wrap)]
pub fn with_width(self, width: usize) -> Self {
unsafe {
crate::ffi::sc_screenshot_configuration_set_width(self.ptr, width as isize);
}
self
}
#[must_use]
#[allow(clippy::cast_possible_wrap)]
pub fn with_height(self, height: usize) -> Self {
unsafe {
crate::ffi::sc_screenshot_configuration_set_height(self.ptr, height as isize);
}
self
}
#[must_use]
pub fn with_shows_cursor(self, shows_cursor: bool) -> Self {
unsafe {
crate::ffi::sc_screenshot_configuration_set_shows_cursor(self.ptr, shows_cursor);
}
self
}
#[must_use]
pub fn with_source_rect(self, rect: crate::cg::CGRect) -> Self {
unsafe {
crate::ffi::sc_screenshot_configuration_set_source_rect(
self.ptr,
rect.x,
rect.y,
rect.width,
rect.height,
);
}
self
}
#[must_use]
pub fn with_destination_rect(self, rect: crate::cg::CGRect) -> Self {
unsafe {
crate::ffi::sc_screenshot_configuration_set_destination_rect(
self.ptr,
rect.x,
rect.y,
rect.width,
rect.height,
);
}
self
}
#[must_use]
pub fn with_ignore_shadows(self, ignore_shadows: bool) -> Self {
unsafe {
crate::ffi::sc_screenshot_configuration_set_ignore_shadows(self.ptr, ignore_shadows);
}
self
}
#[must_use]
pub fn with_ignore_clipping(self, ignore_clipping: bool) -> Self {
unsafe {
crate::ffi::sc_screenshot_configuration_set_ignore_clipping(self.ptr, ignore_clipping);
}
self
}
#[must_use]
pub fn with_include_child_windows(self, include_child_windows: bool) -> Self {
unsafe {
crate::ffi::sc_screenshot_configuration_set_include_child_windows(
self.ptr,
include_child_windows,
);
}
self
}
#[must_use]
pub fn with_display_intent(self, display_intent: SCScreenshotDisplayIntent) -> Self {
unsafe {
crate::ffi::sc_screenshot_configuration_set_display_intent(
self.ptr,
display_intent as i32,
);
}
self
}
#[must_use]
pub fn with_dynamic_range(self, dynamic_range: SCScreenshotDynamicRange) -> Self {
unsafe {
crate::ffi::sc_screenshot_configuration_set_dynamic_range(
self.ptr,
dynamic_range as i32,
);
}
self
}
#[must_use]
pub fn with_file_path(self, path: &str) -> Self {
let c_path = std::ffi::CString::new(path).expect("path should not contain null bytes");
unsafe {
crate::ffi::sc_screenshot_configuration_set_file_url(self.ptr, c_path.as_ptr());
}
self
}
#[must_use]
pub fn with_content_type(self, identifier: &str) -> Self {
let c_id =
std::ffi::CString::new(identifier).expect("identifier should not contain null bytes");
unsafe {
crate::ffi::sc_screenshot_configuration_set_content_type(self.ptr, c_id.as_ptr());
}
self
}
pub fn content_type(&self) -> Option<String> {
let mut buffer = vec![0i8; 256];
let success = unsafe {
crate::ffi::sc_screenshot_configuration_get_content_type(
self.ptr,
buffer.as_mut_ptr(),
buffer.len(),
)
};
if success {
let c_str = unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) };
c_str.to_str().ok().map(ToString::to_string)
} else {
None
}
}
pub fn supported_content_types() -> Vec<String> {
let count =
unsafe { crate::ffi::sc_screenshot_configuration_get_supported_content_types_count() };
let mut result = Vec::with_capacity(count);
for i in 0..count {
let mut buffer = vec![0i8; 256];
let success = unsafe {
crate::ffi::sc_screenshot_configuration_get_supported_content_type_at(
i,
buffer.as_mut_ptr(),
buffer.len(),
)
};
if success {
let c_str = unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) };
if let Ok(s) = c_str.to_str() {
result.push(s.to_string());
}
}
}
result
}
#[must_use]
pub const fn as_ptr(&self) -> *const c_void {
self.ptr
}
}
#[cfg(feature = "macos_26_0")]
impl std::fmt::Debug for SCScreenshotConfiguration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SCScreenshotConfiguration")
.field("content_type", &self.content_type())
.finish_non_exhaustive()
}
}
#[cfg(feature = "macos_26_0")]
impl Default for SCScreenshotConfiguration {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "macos_26_0")]
impl Drop for SCScreenshotConfiguration {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe {
crate::ffi::sc_screenshot_configuration_release(self.ptr);
}
}
}
}
#[cfg(feature = "macos_26_0")]
unsafe impl Send for SCScreenshotConfiguration {}
#[cfg(feature = "macos_26_0")]
unsafe impl Sync for SCScreenshotConfiguration {}
#[cfg(feature = "macos_26_0")]
pub struct SCScreenshotOutput {
ptr: *const c_void,
}
#[cfg(feature = "macos_26_0")]
impl SCScreenshotOutput {
pub(crate) fn from_ptr(ptr: *const c_void) -> Self {
Self { ptr }
}
#[must_use]
pub fn sdr_image(&self) -> Option<CGImage> {
let ptr = unsafe { crate::ffi::sc_screenshot_output_get_sdr_image(self.ptr) };
if ptr.is_null() {
None
} else {
Some(CGImage::from_ptr(ptr))
}
}
#[must_use]
pub fn hdr_image(&self) -> Option<CGImage> {
let ptr = unsafe { crate::ffi::sc_screenshot_output_get_hdr_image(self.ptr) };
if ptr.is_null() {
None
} else {
Some(CGImage::from_ptr(ptr))
}
}
#[must_use]
#[allow(clippy::cast_possible_wrap)]
pub fn file_url(&self) -> Option<String> {
let mut buffer = vec![0i8; 4096];
let success = unsafe {
crate::ffi::sc_screenshot_output_get_file_url(
self.ptr,
buffer.as_mut_ptr(),
buffer.len() as isize,
)
};
if success {
let c_str = unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) };
c_str.to_str().ok().map(String::from)
} else {
None
}
}
}
#[cfg(feature = "macos_26_0")]
impl std::fmt::Debug for SCScreenshotOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SCScreenshotOutput")
.field(
"sdr_image",
&self.sdr_image().map(|i| (i.width(), i.height())),
)
.field(
"hdr_image",
&self.hdr_image().map(|i| (i.width(), i.height())),
)
.field("file_url", &self.file_url())
.finish()
}
}
#[cfg(feature = "macos_26_0")]
impl Drop for SCScreenshotOutput {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe {
crate::ffi::sc_screenshot_output_release(self.ptr);
}
}
}
}
#[cfg(feature = "macos_26_0")]
unsafe impl Send for SCScreenshotOutput {}
#[cfg(feature = "macos_26_0")]
unsafe impl Sync for SCScreenshotOutput {}