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;
#[cfg(feature = "macos_26_0")]
use crate::stream::configuration::InteriorNulError;
#[doc(no_inline)]
pub use apple_cf::cg::CGImage;
#[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",
}
}
}
pub(crate) unsafe fn cgimage_from_retained_ptr(ptr: *const c_void) -> CGImage {
unsafe { CGImage::from_raw(ptr.cast_mut()) }
}
extern "C" fn image_callback(
image_ptr: *const c_void,
error_ptr: *const i8,
user_data: *mut c_void,
) {
crate::utils::panic_safe::catch_user_panic("image_callback", move || {
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_retained_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,
) {
crate::utils::panic_safe::catch_user_panic("buffer_callback", move || {
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,
) {
crate::utils::panic_safe::catch_user_panic("screenshot_output_callback", move || {
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 trait CGImageExt {
fn rgba_data(&self) -> Result<Vec<u8>, SCError>;
fn bgra_data(&self) -> Result<Vec<u8>, SCError>;
fn rgba_data_into(&self, dest: &mut [u8]) -> Result<usize, SCError>;
fn bgra_data_into(&self, dest: &mut [u8]) -> Result<usize, SCError>;
fn rgba_data_into_strided(
&self,
dest: &mut [u8],
dest_bytes_per_row: usize,
) -> Result<usize, SCError>;
fn bgra_data_into_strided(
&self,
dest: &mut [u8],
dest_bytes_per_row: usize,
) -> Result<usize, SCError>;
fn save(&self, path: &str, format: ImageFormat) -> Result<(), SCError>;
}
#[derive(Debug, Clone, Copy)]
enum PixelLayout {
Rgba,
Bgra,
}
impl PixelLayout {
const fn name(self) -> &'static str {
match self {
Self::Rgba => "RGBA",
Self::Bgra => "BGRA",
}
}
unsafe fn render(self, ptr: *const c_void, dest: *mut u8, capacity: usize) -> usize {
unsafe {
match self {
Self::Rgba => crate::ffi::cgimage_render_rgba_into(ptr, dest, capacity),
Self::Bgra => crate::ffi::cgimage_render_bgra_into(ptr, dest, capacity),
}
}
}
unsafe fn render_strided(
self,
ptr: *const c_void,
dest: *mut u8,
capacity: usize,
bytes_per_row: usize,
) -> usize {
unsafe {
match self {
Self::Rgba => {
crate::ffi::cgimage_render_rgba_into_strided(ptr, dest, capacity, bytes_per_row)
}
Self::Bgra => {
crate::ffi::cgimage_render_bgra_into_strided(ptr, dest, capacity, bytes_per_row)
}
}
}
}
}
impl CGImageExt for CGImage {
fn rgba_data(&self) -> Result<Vec<u8>, SCError> {
render_pixel_data(self, PixelLayout::Rgba)
}
fn bgra_data(&self) -> Result<Vec<u8>, SCError> {
render_pixel_data(self, PixelLayout::Bgra)
}
fn rgba_data_into(&self, dest: &mut [u8]) -> Result<usize, SCError> {
render_pixel_data_into(self, dest, PixelLayout::Rgba)
}
fn bgra_data_into(&self, dest: &mut [u8]) -> Result<usize, SCError> {
render_pixel_data_into(self, dest, PixelLayout::Bgra)
}
fn rgba_data_into_strided(
&self,
dest: &mut [u8],
dest_bytes_per_row: usize,
) -> Result<usize, SCError> {
render_pixel_data_into_strided(self, dest, dest_bytes_per_row, PixelLayout::Rgba)
}
fn bgra_data_into_strided(
&self,
dest: &mut [u8],
dest_bytes_per_row: usize,
) -> Result<usize, SCError> {
render_pixel_data_into_strided(self, dest, dest_bytes_per_row, PixelLayout::Bgra)
}
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.as_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()
)))
}
}
}
fn render_pixel_data(image: &CGImage, layout: PixelLayout) -> Result<Vec<u8>, SCError> {
let total_bytes = required_byte_size(image)?;
if total_bytes == 0 {
return Ok(Vec::new());
}
let mut data: Vec<u8> = Vec::with_capacity(total_bytes);
let written = unsafe { layout.render(image.as_ptr(), data.as_mut_ptr(), total_bytes) };
if written != total_bytes {
return Err(SCError::internal_error(format!(
"Failed to render CGImage into {} buffer",
layout.name()
)));
}
unsafe { data.set_len(total_bytes) };
Ok(data)
}
fn render_pixel_data_into(
image: &CGImage,
dest: &mut [u8],
layout: PixelLayout,
) -> Result<usize, SCError> {
let total_bytes = required_byte_size(image)?;
if dest.len() < total_bytes {
return Err(SCError::internal_error(format!(
"Destination buffer too small: need {total_bytes} bytes, got {}",
dest.len()
)));
}
if total_bytes == 0 {
return Ok(0);
}
let written = unsafe { layout.render(image.as_ptr(), dest.as_mut_ptr(), total_bytes) };
if written != total_bytes {
return Err(SCError::internal_error(format!(
"Failed to render CGImage into {} buffer",
layout.name()
)));
}
Ok(written)
}
fn render_pixel_data_into_strided(
image: &CGImage,
dest: &mut [u8],
dest_bytes_per_row: usize,
layout: PixelLayout,
) -> Result<usize, SCError> {
let width = image.width();
let height = image.height();
let min_bytes_per_row = width
.checked_mul(4)
.ok_or_else(|| SCError::internal_error("CGImage row size overflows usize"))?;
if dest_bytes_per_row < min_bytes_per_row {
return Err(SCError::internal_error(format!(
"Destination row stride too small: need at least {min_bytes_per_row} bytes, got {dest_bytes_per_row}"
)));
}
let required = height
.checked_mul(dest_bytes_per_row)
.ok_or_else(|| SCError::internal_error("CGImage strided size overflows usize"))?;
if dest.len() < required {
return Err(SCError::internal_error(format!(
"Destination buffer too small: need {required} bytes, got {}",
dest.len()
)));
}
if required == 0 {
return Ok(0);
}
let written = unsafe {
layout.render_strided(
image.as_ptr(),
dest.as_mut_ptr(),
dest.len(),
dest_bytes_per_row,
)
};
if written != required {
return Err(SCError::internal_error(format!(
"Failed to render CGImage into {} buffer",
layout.name()
)));
}
Ok(written)
}
fn required_byte_size(image: &CGImage) -> Result<usize, SCError> {
image
.width()
.checked_mul(image.height())
.and_then(|n| n.checked_mul(4))
.ok_or_else(|| SCError::internal_error("CGImage dimensions overflow usize"))
}
#[derive(Debug)]
pub struct SCScreenshotManager;
impl SCScreenshotManager {
pub fn capture_image(
content_filter: &SCContentFilter,
configuration: &SCStreamConfiguration,
) -> Result<CGImage, SCError> {
let configuration = configuration.clone();
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 configuration = configuration.clone();
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.origin.x,
rect.origin.y,
rect.size.width,
rect.size.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.origin.x,
rect.origin.y,
rect.size.width,
rect.size.height,
configuration.as_ptr(),
screenshot_output_callback,
context,
);
}
completion.wait().map_err(SCError::ScreenshotError)
}
}
#[cfg(feature = "macos_26_0")]
const UTTYPE_IDENTIFIER_BUFFER: usize = crate::utils::ffi_string::SMALL_BUFFER_SIZE;
#[cfg(feature = "macos_26_0")]
fn owned_path<F: FnOnce() -> *mut i8>(ffi_call: F) -> Option<std::path::PathBuf> {
use std::os::unix::ffi::OsStrExt;
let ptr = ffi_call();
if ptr.is_null() {
return None;
}
let bytes = unsafe { std::ffi::CStr::from_ptr(ptr) }.to_bytes().to_vec();
unsafe { crate::ffi::sc_free_string(ptr) };
if bytes.is_empty() {
return None;
}
Some(std::path::PathBuf::from(std::ffi::OsStr::from_bytes(
&bytes,
)))
}
#[cfg(feature = "macos_26_0")]
fn read_rect<F>(ffi_call: F) -> crate::cg::CGRect
where
F: FnOnce(*mut f64, *mut f64, *mut f64, *mut f64),
{
let mut x = 0.0;
let mut y = 0.0;
let mut width = 0.0;
let mut height = 0.0;
ffi_call(&raw mut x, &raw mut y, &raw mut width, &raw mut height);
crate::cg::CGRect::new(x, y, width, height)
}
#[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")]
impl SCScreenshotDisplayIntent {
pub const fn from_raw(raw: i32) -> Option<Self> {
match raw {
0 => Some(Self::Canonical),
1 => Some(Self::Local),
_ => None,
}
}
}
#[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")]
impl SCScreenshotDynamicRange {
pub const fn from_raw(raw: i32) -> Option<Self> {
match raw {
0 => Some(Self::SDR),
1 => Some(Self::HDR),
2 => Some(Self::BothSDRAndHDR),
_ => None,
}
}
}
#[cfg(feature = "macos_26_0")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InvalidScreenshotPath {
NotUtf8,
InteriorNul,
}
#[cfg(feature = "macos_26_0")]
impl std::fmt::Display for InvalidScreenshotPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotUtf8 => f.write_str("screenshot path is not valid UTF-8"),
Self::InteriorNul => f.write_str("screenshot path contains an interior NUL byte"),
}
}
}
#[cfg(feature = "macos_26_0")]
impl std::error::Error for InvalidScreenshotPath {}
#[cfg(feature = "macos_26_0")]
pub struct SCScreenshotConfiguration {
ptr: *const c_void,
}
#[cfg(feature = "macos_26_0")]
impl SCScreenshotConfiguration {
pub fn new() -> Result<Self, SCError> {
let ptr = unsafe { crate::ffi::sc_screenshot_configuration_create() };
if ptr.is_null() {
return Err(SCError::feature_not_available(
"SCScreenshotConfiguration",
"26.0",
));
}
Ok(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.origin.x,
rect.origin.y,
rect.size.width,
rect.size.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.origin.x,
rect.origin.y,
rect.size.width,
rect.size.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
}
pub fn set_file_path(
&mut self,
path: impl AsRef<std::path::Path>,
) -> Result<&mut Self, InvalidScreenshotPath> {
let path = path
.as_ref()
.to_str()
.ok_or(InvalidScreenshotPath::NotUtf8)?;
let c_path =
std::ffi::CString::new(path).map_err(|_| InvalidScreenshotPath::InteriorNul)?;
unsafe {
crate::ffi::sc_screenshot_configuration_set_file_url(self.ptr, c_path.as_ptr());
}
Ok(self)
}
#[allow(clippy::missing_errors_doc)]
pub fn with_file_path(
mut self,
path: impl AsRef<std::path::Path>,
) -> Result<Self, InvalidScreenshotPath> {
self.set_file_path(path)?;
Ok(self)
}
#[must_use]
pub fn without_file_path(self) -> Self {
unsafe { crate::ffi::sc_screenshot_configuration_clear_file_url(self.ptr) };
self
}
pub fn clear_file_path(&mut self) -> &mut Self {
unsafe { crate::ffi::sc_screenshot_configuration_clear_file_url(self.ptr) };
self
}
#[must_use]
pub fn file_path(&self) -> Option<std::path::PathBuf> {
owned_path(|| unsafe {
crate::ffi::sc_screenshot_configuration_get_file_path_owned(self.ptr)
})
}
#[must_use]
pub fn width(&self) -> usize {
let width = unsafe { crate::ffi::sc_screenshot_configuration_get_width(self.ptr) };
usize::try_from(width).unwrap_or(0)
}
#[must_use]
pub fn height(&self) -> usize {
let height = unsafe { crate::ffi::sc_screenshot_configuration_get_height(self.ptr) };
usize::try_from(height).unwrap_or(0)
}
#[must_use]
pub fn shows_cursor(&self) -> bool {
unsafe { crate::ffi::sc_screenshot_configuration_get_shows_cursor(self.ptr) }
}
#[must_use]
pub fn source_rect(&self) -> crate::cg::CGRect {
read_rect(|x, y, w, h| unsafe {
crate::ffi::sc_screenshot_configuration_get_source_rect(self.ptr, x, y, w, h);
})
}
#[must_use]
pub fn destination_rect(&self) -> crate::cg::CGRect {
read_rect(|x, y, w, h| unsafe {
crate::ffi::sc_screenshot_configuration_get_destination_rect(self.ptr, x, y, w, h);
})
}
#[must_use]
pub fn ignore_shadows(&self) -> bool {
unsafe { crate::ffi::sc_screenshot_configuration_get_ignore_shadows(self.ptr) }
}
#[must_use]
pub fn ignore_clipping(&self) -> bool {
unsafe { crate::ffi::sc_screenshot_configuration_get_ignore_clipping(self.ptr) }
}
#[must_use]
pub fn include_child_windows(&self) -> bool {
unsafe { crate::ffi::sc_screenshot_configuration_get_include_child_windows(self.ptr) }
}
#[allow(clippy::missing_errors_doc)]
pub fn display_intent(&self) -> Result<SCScreenshotDisplayIntent, SCError> {
let mut raw = 0_i32;
if !unsafe {
crate::ffi::sc_screenshot_configuration_get_display_intent(self.ptr, &raw mut raw)
} {
return Err(SCError::feature_not_available(
"SCScreenshotConfiguration.displayIntent",
"26.0",
));
}
SCScreenshotDisplayIntent::from_raw(raw).ok_or_else(|| SCError::UnknownValue {
type_name: "SCScreenshotDisplayIntent",
raw: i64::from(raw),
})
}
#[allow(clippy::missing_errors_doc)]
pub fn dynamic_range(&self) -> Result<SCScreenshotDynamicRange, SCError> {
let mut raw = 0_i32;
if !unsafe {
crate::ffi::sc_screenshot_configuration_get_dynamic_range(self.ptr, &raw mut raw)
} {
return Err(SCError::feature_not_available(
"SCScreenshotConfiguration.dynamicRange",
"26.0",
));
}
SCScreenshotDynamicRange::from_raw(raw).ok_or_else(|| SCError::UnknownValue {
type_name: "SCScreenshotDynamicRange",
raw: i64::from(raw),
})
}
pub fn with_content_type(self, identifier: &str) -> Result<Self, InteriorNulError> {
let c_id = std::ffi::CString::new(identifier).map_err(|_| InteriorNulError)?;
unsafe {
crate::ffi::sc_screenshot_configuration_set_content_type(self.ptr, c_id.as_ptr());
}
Ok(self)
}
#[must_use]
pub fn content_type(&self) -> Option<String> {
unsafe {
crate::utils::ffi_string::ffi_string_from_buffer(
UTTYPE_IDENTIFIER_BUFFER,
|buffer, len| {
crate::ffi::sc_screenshot_configuration_get_content_type(
self.ptr,
buffer,
usize::try_from(len).unwrap_or(0),
)
},
)
}
}
#[must_use]
pub fn supported_content_types() -> Vec<String> {
let count =
unsafe { crate::ffi::sc_screenshot_configuration_get_supported_content_types_count() };
(0..count)
.filter_map(|i| unsafe {
crate::utils::ffi_string::ffi_string_from_buffer(
UTTYPE_IDENTIFIER_BUFFER,
|buffer, len| {
crate::ffi::sc_screenshot_configuration_get_supported_content_type_at(
i,
buffer,
usize::try_from(len).unwrap_or(0),
)
},
)
})
.collect()
}
#[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")]
crate::utils::retained::sc_retained!(
SCScreenshotConfiguration,
field = ptr,
release = crate::ffi::sc_screenshot_configuration_release,
);
#[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(unsafe { cgimage_from_retained_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(unsafe { cgimage_from_retained_ptr(ptr) })
}
}
#[must_use]
pub fn file_path(&self) -> Option<std::path::PathBuf> {
owned_path(|| unsafe { crate::ffi::sc_screenshot_output_get_file_path_owned(self.ptr) })
}
}
#[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_path", &self.file_path())
.finish()
}
}
#[cfg(feature = "macos_26_0")]
crate::utils::retained::sc_retained!(
SCScreenshotOutput,
field = ptr,
release = crate::ffi::sc_screenshot_output_release,
);
#[cfg(feature = "macos_26_0")]
unsafe impl Send for SCScreenshotOutput {}
#[cfg(feature = "macos_26_0")]
unsafe impl Sync for SCScreenshotOutput {}
#[cfg(all(test, feature = "macos_26_0"))]
mod tests {
use super::{SCScreenshotConfiguration, SCScreenshotDisplayIntent, SCScreenshotDynamicRange};
#[test]
fn bridge_rejects_unknown_screenshot_raw_values() {
let config = SCScreenshotConfiguration::new()
.expect("macOS 26.0 or later")
.with_display_intent(SCScreenshotDisplayIntent::Local)
.with_dynamic_range(SCScreenshotDynamicRange::HDR);
for raw in [3, -1, i32::MAX, i32::MIN] {
let intent_applied = unsafe {
crate::ffi::sc_screenshot_configuration_set_display_intent(config.ptr, raw)
};
let range_applied = unsafe {
crate::ffi::sc_screenshot_configuration_set_dynamic_range(config.ptr, raw)
};
assert!(!intent_applied, "the bridge accepted display intent {raw}");
assert!(!range_applied, "the bridge accepted dynamic range {raw}");
assert_eq!(
config.display_intent(),
Ok(SCScreenshotDisplayIntent::Local)
);
assert_eq!(config.dynamic_range(), Ok(SCScreenshotDynamicRange::HDR));
}
}
}