screencapturekit/screenshot_manager.rs
1//! `SCScreenshotManager` - Single-shot screenshot capture
2//!
3//! Available on macOS 14.0+.
4//! Provides high-quality screenshot capture without the overhead of setting up a stream.
5//!
6//! ## When to Use
7//!
8//! Use `SCScreenshotManager` when you need:
9//! - A single screenshot rather than continuous capture
10//! - Quick capture without stream setup/teardown overhead
11//! - Direct saving to image files
12//!
13//! For continuous capture, use [`SCStream`](crate::stream::SCStream) instead.
14//!
15//! ## Example
16//!
17//! ```no_run
18//! use screencapturekit::screenshot_manager::{CGImageExt, ImageFormat, SCScreenshotManager};
19//! use screencapturekit::stream::{content_filter::SCContentFilter, configuration::SCStreamConfiguration};
20//! use screencapturekit::shareable_content::SCShareableContent;
21//!
22//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
23//! let content = SCShareableContent::get()?;
24//! let display = &content.displays()[0];
25//! let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build()?;
26//! let config = SCStreamConfiguration::new()
27//! .with_width(1920)
28//! .with_height(1080);
29//!
30//! // Capture as CGImage
31//! let image = SCScreenshotManager::capture_image(&filter, &config)?;
32//! println!("Screenshot: {}x{}", image.width(), image.height());
33//!
34//! // Save to file
35//! image.save_png("screenshot.png")?;
36//!
37//! // Or save as JPEG with quality
38//! image.save("screenshot.jpg", ImageFormat::Jpeg(0.85))?;
39//! # Ok(())
40//! # }
41//! ```
42
43use crate::error::SCError;
44use crate::stream::configuration::SCStreamConfiguration;
45use crate::stream::content_filter::SCContentFilter;
46use crate::utils::completion::{error_from_cstr, SyncCompletion};
47use std::ffi::c_void;
48
49#[cfg(feature = "macos_15_2")]
50use crate::cg::CGRect;
51#[cfg(feature = "macos_26_0")]
52use crate::stream::configuration::InteriorNulError;
53
54#[doc(no_inline)]
55pub use apple_cf::cg::CGImage;
56
57/// Image output format for saving screenshots
58///
59/// # Examples
60///
61/// ```no_run
62/// use screencapturekit::screenshot_manager::ImageFormat;
63///
64/// // PNG for lossless quality
65/// let format = ImageFormat::Png;
66///
67/// // JPEG with 80% quality
68/// let format = ImageFormat::Jpeg(0.8);
69///
70/// // HEIC with 90% quality (smaller file size than JPEG)
71/// let format = ImageFormat::Heic(0.9);
72/// ```
73#[derive(Debug, Clone, Copy, PartialEq)]
74pub enum ImageFormat {
75 /// PNG format (lossless)
76 Png,
77 /// JPEG format with quality (0.0-1.0)
78 Jpeg(f32),
79 /// TIFF format (lossless)
80 Tiff,
81 /// GIF format
82 Gif,
83 /// BMP format
84 Bmp,
85 /// HEIC format with quality (0.0-1.0) - efficient compression
86 Heic(f32),
87}
88
89impl ImageFormat {
90 fn to_format_id(self) -> i32 {
91 match self {
92 Self::Png => 0,
93 Self::Jpeg(_) => 1,
94 Self::Tiff => 2,
95 Self::Gif => 3,
96 Self::Bmp => 4,
97 Self::Heic(_) => 5,
98 }
99 }
100
101 fn quality(self) -> f32 {
102 match self {
103 Self::Jpeg(q) | Self::Heic(q) => q.clamp(0.0, 1.0),
104 _ => 1.0,
105 }
106 }
107
108 /// Get the typical file extension for this format
109 #[must_use]
110 pub const fn extension(&self) -> &'static str {
111 match self {
112 Self::Png => "png",
113 Self::Jpeg(_) => "jpg",
114 Self::Tiff => "tiff",
115 Self::Gif => "gif",
116 Self::Bmp => "bmp",
117 Self::Heic(_) => "heic",
118 }
119 }
120}
121
122/// # Safety
123/// `ptr` must be a non-null retained `CGImageRef` whose +1 ownership is
124/// transferred to the returned wrapper.
125pub(crate) unsafe fn cgimage_from_retained_ptr(ptr: *const c_void) -> CGImage {
126 unsafe { CGImage::from_raw(ptr.cast_mut()) }
127}
128
129extern "C" fn image_callback(
130 image_ptr: *const c_void,
131 error_ptr: *const i8,
132 user_data: *mut c_void,
133) {
134 crate::utils::panic_safe::catch_user_panic("image_callback", move || {
135 if !error_ptr.is_null() {
136 // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
137 let error = unsafe { error_from_cstr(error_ptr) };
138 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
139 unsafe { SyncCompletion::<CGImage>::complete_err(user_data, error) };
140 } else if !image_ptr.is_null() {
141 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
142 unsafe { SyncCompletion::complete_ok(user_data, cgimage_from_retained_ptr(image_ptr)) };
143 } else {
144 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
145 unsafe {
146 SyncCompletion::<CGImage>::complete_err(user_data, "Unknown error".to_string());
147 };
148 }
149 });
150}
151
152extern "C" fn buffer_callback(
153 buffer_ptr: *const c_void,
154 error_ptr: *const i8,
155 user_data: *mut c_void,
156) {
157 crate::utils::panic_safe::catch_user_panic("buffer_callback", move || {
158 if !error_ptr.is_null() {
159 // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
160 let error = unsafe { error_from_cstr(error_ptr) };
161 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
162 unsafe { SyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(user_data, error) };
163 } else if !buffer_ptr.is_null() {
164 // SAFETY: `buffer_ptr` is non-null (checked above), is a valid `CMSampleBuffer` pointer, and `cast_mut()` is sound because the underlying object is uniquely owned at this point.
165 let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(buffer_ptr.cast_mut()) };
166 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
167 unsafe { SyncCompletion::complete_ok(user_data, buffer) };
168 } else {
169 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
170 unsafe {
171 SyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(
172 user_data,
173 "Unknown error".to_string(),
174 );
175 };
176 }
177 });
178}
179
180#[cfg(feature = "macos_26_0")]
181extern "C" fn screenshot_output_callback(
182 output_ptr: *const c_void,
183 error_ptr: *const i8,
184 user_data: *mut c_void,
185) {
186 crate::utils::panic_safe::catch_user_panic("screenshot_output_callback", move || {
187 if !error_ptr.is_null() {
188 // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
189 let error = unsafe { error_from_cstr(error_ptr) };
190 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
191 unsafe { SyncCompletion::<SCScreenshotOutput>::complete_err(user_data, error) };
192 } else if !output_ptr.is_null() {
193 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
194 unsafe {
195 SyncCompletion::complete_ok(user_data, SCScreenshotOutput::from_ptr(output_ptr));
196 };
197 } else {
198 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
199 unsafe {
200 SyncCompletion::<SCScreenshotOutput>::complete_err(
201 user_data,
202 "Unknown error".to_string(),
203 );
204 };
205 }
206 });
207}
208
209/// Screenshot-specific helpers implemented for the canonical [`CGImage`] type.
210///
211/// Import this trait to access pixel extraction helpers and multi-format file
212/// export on images returned by [`SCScreenshotManager`].
213pub trait CGImageExt {
214 /// Get raw RGBA pixel data.
215 ///
216 /// # Errors
217 /// Returns an error if the pixel data cannot be extracted.
218 fn rgba_data(&self) -> Result<Vec<u8>, SCError>;
219
220 /// Get raw BGRA pixel data.
221 ///
222 /// # Errors
223 /// Returns an error if the pixel data cannot be extracted.
224 fn bgra_data(&self) -> Result<Vec<u8>, SCError>;
225
226 /// Render the image's RGBA bytes into a caller-supplied buffer.
227 ///
228 /// # Errors
229 /// Returns an error if `dest` is too small or the render fails.
230 ///
231 /// # Examples
232 ///
233 /// ```no_run
234 /// # use screencapturekit::screenshot_manager::{CGImageExt, SCScreenshotManager};
235 /// # use screencapturekit::stream::{content_filter::SCContentFilter, configuration::SCStreamConfiguration};
236 /// # use screencapturekit::shareable_content::SCShareableContent;
237 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
238 /// # let content = SCShareableContent::get()?;
239 /// # let display = &content.displays()[0];
240 /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build()?;
241 /// # let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
242 /// let mut buffer: Vec<u8> = vec![0; 1920 * 1080 * 4];
243 /// let img = SCScreenshotManager::capture_image(&filter, &config)?;
244 /// img.rgba_data_into(&mut buffer)?;
245 /// # Ok(())
246 /// # }
247 /// ```
248 fn rgba_data_into(&self, dest: &mut [u8]) -> Result<usize, SCError>;
249
250 /// Render the image's BGRA bytes into a caller-supplied buffer.
251 ///
252 /// # Errors
253 /// Returns an error if `dest` is too small or the render fails.
254 fn bgra_data_into(&self, dest: &mut [u8]) -> Result<usize, SCError>;
255
256 /// Render the image's RGBA bytes into a caller-supplied buffer using an
257 /// explicit row stride (`dest_bytes_per_row`).
258 ///
259 /// Unlike [`rgba_data_into`](CGImageExt::rgba_data_into), which assumes
260 /// tightly-packed rows (`width * 4`), this accepts a caller-specified row
261 /// stride so consumers with padded/row-aligned buffers (GPU upload, wgpu)
262 /// aren't forced into tight packing.
263 ///
264 /// Returns the number of bytes spanned (`height * dest_bytes_per_row`).
265 ///
266 /// # Errors
267 /// Returns an error if `dest_bytes_per_row` is smaller than `width * 4`,
268 /// if `dest` cannot hold `height * dest_bytes_per_row` bytes, or if the
269 /// render fails.
270 fn rgba_data_into_strided(
271 &self,
272 dest: &mut [u8],
273 dest_bytes_per_row: usize,
274 ) -> Result<usize, SCError>;
275
276 /// Render the image's BGRA bytes into a caller-supplied buffer using an
277 /// explicit row stride (`dest_bytes_per_row`).
278 ///
279 /// See [`rgba_data_into_strided`](CGImageExt::rgba_data_into_strided) for
280 /// the row-stride semantics.
281 ///
282 /// # Errors
283 /// Returns an error if `dest_bytes_per_row` is smaller than `width * 4`,
284 /// if `dest` cannot hold `height * dest_bytes_per_row` bytes, or if the
285 /// render fails.
286 fn bgra_data_into_strided(
287 &self,
288 dest: &mut [u8],
289 dest_bytes_per_row: usize,
290 ) -> Result<usize, SCError>;
291
292 /// Save the image to a file in the specified format.
293 ///
294 /// # Errors
295 /// Returns an error if the path contains interior null bytes or the export fails.
296 ///
297 /// # Examples
298 ///
299 /// ```no_run
300 /// # use screencapturekit::screenshot_manager::{CGImageExt, ImageFormat, SCScreenshotManager};
301 /// # use screencapturekit::stream::{content_filter::SCContentFilter, configuration::SCStreamConfiguration};
302 /// # use screencapturekit::shareable_content::SCShareableContent;
303 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
304 /// # let content = SCShareableContent::get()?;
305 /// # let display = &content.displays()[0];
306 /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build()?;
307 /// # let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
308 /// let image = SCScreenshotManager::capture_image(&filter, &config)?;
309 /// image.save("screenshot.png", ImageFormat::Png)?;
310 /// image.save("screenshot.jpg", ImageFormat::Jpeg(0.85))?;
311 /// image.save("screenshot.heic", ImageFormat::Heic(0.9))?;
312 /// # Ok(())
313 /// # }
314 /// ```
315 fn save(&self, path: &str, format: ImageFormat) -> Result<(), SCError>;
316}
317
318/// Internal selector for the channel ordering passed to the Swift renderer.
319#[derive(Debug, Clone, Copy)]
320enum PixelLayout {
321 Rgba,
322 Bgra,
323}
324
325impl PixelLayout {
326 const fn name(self) -> &'static str {
327 match self {
328 Self::Rgba => "RGBA",
329 Self::Bgra => "BGRA",
330 }
331 }
332
333 /// Dispatch into the matching Swift bridge entry point.
334 ///
335 /// # Safety
336 /// The destination must point to at least `capacity` bytes and `ptr` must
337 /// be a live retained `CGImage`.
338 unsafe fn render(self, ptr: *const c_void, dest: *mut u8, capacity: usize) -> usize {
339 unsafe {
340 match self {
341 Self::Rgba => crate::ffi::cgimage_render_rgba_into(ptr, dest, capacity),
342 Self::Bgra => crate::ffi::cgimage_render_bgra_into(ptr, dest, capacity),
343 }
344 }
345 }
346
347 /// Dispatch into the matching strided Swift bridge entry point.
348 ///
349 /// # Safety
350 /// The destination must point to at least `capacity` bytes, span
351 /// `bytes_per_row` per image row, and `ptr` must be a live retained
352 /// `CGImage`.
353 unsafe fn render_strided(
354 self,
355 ptr: *const c_void,
356 dest: *mut u8,
357 capacity: usize,
358 bytes_per_row: usize,
359 ) -> usize {
360 unsafe {
361 match self {
362 Self::Rgba => {
363 crate::ffi::cgimage_render_rgba_into_strided(ptr, dest, capacity, bytes_per_row)
364 }
365 Self::Bgra => {
366 crate::ffi::cgimage_render_bgra_into_strided(ptr, dest, capacity, bytes_per_row)
367 }
368 }
369 }
370 }
371}
372
373impl CGImageExt for CGImage {
374 fn rgba_data(&self) -> Result<Vec<u8>, SCError> {
375 render_pixel_data(self, PixelLayout::Rgba)
376 }
377
378 fn bgra_data(&self) -> Result<Vec<u8>, SCError> {
379 render_pixel_data(self, PixelLayout::Bgra)
380 }
381
382 fn rgba_data_into(&self, dest: &mut [u8]) -> Result<usize, SCError> {
383 render_pixel_data_into(self, dest, PixelLayout::Rgba)
384 }
385
386 fn bgra_data_into(&self, dest: &mut [u8]) -> Result<usize, SCError> {
387 render_pixel_data_into(self, dest, PixelLayout::Bgra)
388 }
389
390 fn rgba_data_into_strided(
391 &self,
392 dest: &mut [u8],
393 dest_bytes_per_row: usize,
394 ) -> Result<usize, SCError> {
395 render_pixel_data_into_strided(self, dest, dest_bytes_per_row, PixelLayout::Rgba)
396 }
397
398 fn bgra_data_into_strided(
399 &self,
400 dest: &mut [u8],
401 dest_bytes_per_row: usize,
402 ) -> Result<usize, SCError> {
403 render_pixel_data_into_strided(self, dest, dest_bytes_per_row, PixelLayout::Bgra)
404 }
405
406 fn save(&self, path: &str, format: ImageFormat) -> Result<(), SCError> {
407 let c_path = std::ffi::CString::new(path)
408 .map_err(|_| SCError::internal_error("Path contains null bytes"))?;
409
410 let success = unsafe {
411 crate::ffi::cgimage_save_to_file(
412 self.as_ptr(),
413 c_path.as_ptr(),
414 format.to_format_id(),
415 format.quality(),
416 )
417 };
418
419 if success {
420 Ok(())
421 } else {
422 Err(SCError::internal_error(format!(
423 "Failed to save image as {}",
424 format.extension().to_uppercase()
425 )))
426 }
427 }
428}
429
430fn render_pixel_data(image: &CGImage, layout: PixelLayout) -> Result<Vec<u8>, SCError> {
431 let total_bytes = required_byte_size(image)?;
432 if total_bytes == 0 {
433 return Ok(Vec::new());
434 }
435
436 let mut data: Vec<u8> = Vec::with_capacity(total_bytes);
437 let written = unsafe { layout.render(image.as_ptr(), data.as_mut_ptr(), total_bytes) };
438
439 if written != total_bytes {
440 return Err(SCError::internal_error(format!(
441 "Failed to render CGImage into {} buffer",
442 layout.name()
443 )));
444 }
445
446 unsafe { data.set_len(total_bytes) };
447 Ok(data)
448}
449
450fn render_pixel_data_into(
451 image: &CGImage,
452 dest: &mut [u8],
453 layout: PixelLayout,
454) -> Result<usize, SCError> {
455 let total_bytes = required_byte_size(image)?;
456 if dest.len() < total_bytes {
457 return Err(SCError::internal_error(format!(
458 "Destination buffer too small: need {total_bytes} bytes, got {}",
459 dest.len()
460 )));
461 }
462 if total_bytes == 0 {
463 return Ok(0);
464 }
465
466 let written = unsafe { layout.render(image.as_ptr(), dest.as_mut_ptr(), total_bytes) };
467 if written != total_bytes {
468 return Err(SCError::internal_error(format!(
469 "Failed to render CGImage into {} buffer",
470 layout.name()
471 )));
472 }
473 Ok(written)
474}
475
476fn render_pixel_data_into_strided(
477 image: &CGImage,
478 dest: &mut [u8],
479 dest_bytes_per_row: usize,
480 layout: PixelLayout,
481) -> Result<usize, SCError> {
482 let width = image.width();
483 let height = image.height();
484
485 let min_bytes_per_row = width
486 .checked_mul(4)
487 .ok_or_else(|| SCError::internal_error("CGImage row size overflows usize"))?;
488 if dest_bytes_per_row < min_bytes_per_row {
489 return Err(SCError::internal_error(format!(
490 "Destination row stride too small: need at least {min_bytes_per_row} bytes, got {dest_bytes_per_row}"
491 )));
492 }
493
494 let required = height
495 .checked_mul(dest_bytes_per_row)
496 .ok_or_else(|| SCError::internal_error("CGImage strided size overflows usize"))?;
497 if dest.len() < required {
498 return Err(SCError::internal_error(format!(
499 "Destination buffer too small: need {required} bytes, got {}",
500 dest.len()
501 )));
502 }
503 if required == 0 {
504 return Ok(0);
505 }
506
507 let written = unsafe {
508 layout.render_strided(
509 image.as_ptr(),
510 dest.as_mut_ptr(),
511 dest.len(),
512 dest_bytes_per_row,
513 )
514 };
515 if written != required {
516 return Err(SCError::internal_error(format!(
517 "Failed to render CGImage into {} buffer",
518 layout.name()
519 )));
520 }
521 Ok(written)
522}
523
524fn required_byte_size(image: &CGImage) -> Result<usize, SCError> {
525 image
526 .width()
527 .checked_mul(image.height())
528 .and_then(|n| n.checked_mul(4))
529 .ok_or_else(|| SCError::internal_error("CGImage dimensions overflow usize"))
530}
531
532/// Manager for capturing single screenshots
533///
534/// Available on macOS 14.0+. Provides a simpler API than `SCStream` for one-time captures.
535///
536/// # Examples
537///
538/// ```no_run
539/// use screencapturekit::screenshot_manager::SCScreenshotManager;
540/// use screencapturekit::stream::{content_filter::SCContentFilter, configuration::SCStreamConfiguration};
541/// use screencapturekit::shareable_content::SCShareableContent;
542///
543/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
544/// let content = SCShareableContent::get()?;
545/// let display = &content.displays()[0];
546/// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build()?;
547/// let config = SCStreamConfiguration::new()
548/// .with_width(1920)
549/// .with_height(1080);
550///
551/// let image = SCScreenshotManager::capture_image(&filter, &config)?;
552/// println!("Captured screenshot: {}x{}", image.width(), image.height());
553/// # Ok(())
554/// # }
555/// ```
556#[derive(Debug)]
557pub struct SCScreenshotManager;
558
559impl SCScreenshotManager {
560 /// Capture a single screenshot as a `CGImage`
561 ///
562 /// # Errors
563 /// Returns an error if:
564 /// - The system is not macOS 14.0+
565 /// - Screen recording permission is not granted
566 /// - The capture fails for any reason
567 ///
568 /// # Panics
569 /// Panics if the internal mutex is poisoned.
570 pub fn capture_image(
571 content_filter: &SCContentFilter,
572 configuration: &SCStreamConfiguration,
573 ) -> Result<CGImage, SCError> {
574 let configuration = configuration.clone();
575 let (completion, context) = SyncCompletion::<CGImage>::new();
576
577 unsafe {
578 crate::ffi::sc_screenshot_manager_capture_image(
579 content_filter.as_ptr(),
580 configuration.as_ptr(),
581 image_callback,
582 context,
583 );
584 }
585
586 completion.wait().map_err(SCError::ScreenshotError)
587 }
588
589 /// Capture a single screenshot as a `CMSampleBuffer`
590 ///
591 /// Returns the sample buffer for advanced processing.
592 ///
593 /// # Errors
594 /// Returns an error if:
595 /// - The system is not macOS 14.0+
596 /// - Screen recording permission is not granted
597 /// - The capture fails for any reason
598 ///
599 /// # Panics
600 /// Panics if the internal mutex is poisoned.
601 pub fn capture_sample_buffer(
602 content_filter: &SCContentFilter,
603 configuration: &SCStreamConfiguration,
604 ) -> Result<crate::cm::CMSampleBuffer, SCError> {
605 let configuration = configuration.clone();
606 let (completion, context) = SyncCompletion::<crate::cm::CMSampleBuffer>::new();
607
608 unsafe {
609 crate::ffi::sc_screenshot_manager_capture_sample_buffer(
610 content_filter.as_ptr(),
611 configuration.as_ptr(),
612 buffer_callback,
613 context,
614 );
615 }
616
617 completion.wait().map_err(SCError::ScreenshotError)
618 }
619
620 /// Capture a screenshot of a specific screen region (macOS 15.2+)
621 ///
622 /// This method captures the content within the specified rectangle,
623 /// which can span multiple displays.
624 ///
625 /// # Arguments
626 /// * `rect` - The rectangle to capture, in screen coordinates (points)
627 ///
628 /// # Errors
629 /// Returns an error if:
630 /// - The system is not macOS 15.2+
631 /// - Screen recording permission is not granted
632 /// - The capture fails for any reason
633 ///
634 /// # Examples
635 /// ```no_run
636 /// use screencapturekit::screenshot_manager::SCScreenshotManager;
637 /// use screencapturekit::cg::CGRect;
638 ///
639 /// fn example() -> Result<(), screencapturekit::utils::error::SCError> {
640 /// let rect = CGRect::new(0.0, 0.0, 1920.0, 1080.0);
641 /// let image = SCScreenshotManager::capture_image_in_rect(rect)?;
642 /// Ok(())
643 /// }
644 /// ```
645 #[cfg(feature = "macos_15_2")]
646 pub fn capture_image_in_rect(rect: CGRect) -> Result<CGImage, SCError> {
647 let (completion, context) = SyncCompletion::<CGImage>::new();
648
649 unsafe {
650 crate::ffi::sc_screenshot_manager_capture_image_in_rect(
651 rect.origin.x,
652 rect.origin.y,
653 rect.size.width,
654 rect.size.height,
655 image_callback,
656 context,
657 );
658 }
659
660 completion.wait().map_err(SCError::ScreenshotError)
661 }
662
663 /// Capture a screenshot with advanced configuration (macOS 26.0+)
664 ///
665 /// This method uses the new `SCScreenshotConfiguration` for more control
666 /// over the screenshot output, including HDR support and file saving.
667 ///
668 /// # Arguments
669 /// * `content_filter` - The content filter specifying what to capture
670 /// * `configuration` - The screenshot configuration
671 ///
672 /// # Errors
673 /// Returns an error if the capture fails
674 ///
675 /// # Examples
676 /// ```no_run
677 /// use screencapturekit::screenshot_manager::{SCScreenshotManager, SCScreenshotConfiguration, SCScreenshotDynamicRange};
678 /// use screencapturekit::stream::content_filter::SCContentFilter;
679 /// use screencapturekit::shareable_content::SCShareableContent;
680 ///
681 /// fn example() -> Option<()> {
682 /// let content = SCShareableContent::get().ok()?;
683 /// let displays = content.displays();
684 /// let display = displays.first()?;
685 /// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build().ok()?;
686 /// let config = SCScreenshotConfiguration::new().ok()?
687 /// .with_width(1920)
688 /// .with_height(1080)
689 /// .with_dynamic_range(SCScreenshotDynamicRange::BothSDRAndHDR);
690 ///
691 /// let output = SCScreenshotManager::capture_screenshot(&filter, &config).ok()?;
692 /// if let Some(sdr) = output.sdr_image() {
693 /// println!("SDR image: {}x{}", sdr.width(), sdr.height());
694 /// }
695 /// Some(())
696 /// }
697 /// ```
698 #[cfg(feature = "macos_26_0")]
699 pub fn capture_screenshot(
700 content_filter: &SCContentFilter,
701 configuration: &SCScreenshotConfiguration,
702 ) -> Result<SCScreenshotOutput, SCError> {
703 let (completion, context) = SyncCompletion::<SCScreenshotOutput>::new();
704
705 unsafe {
706 crate::ffi::sc_screenshot_manager_capture_screenshot(
707 content_filter.as_ptr(),
708 configuration.as_ptr(),
709 screenshot_output_callback,
710 context,
711 );
712 }
713
714 completion.wait().map_err(SCError::ScreenshotError)
715 }
716
717 /// Capture a screenshot of a specific region with advanced configuration (macOS 26.0+)
718 ///
719 /// # Arguments
720 /// * `rect` - The rectangle to capture, in screen coordinates (points)
721 /// * `configuration` - The screenshot configuration
722 ///
723 /// # Errors
724 /// Returns an error if the capture fails
725 #[cfg(feature = "macos_26_0")]
726 pub fn capture_screenshot_in_rect(
727 rect: crate::cg::CGRect,
728 configuration: &SCScreenshotConfiguration,
729 ) -> Result<SCScreenshotOutput, SCError> {
730 let (completion, context) = SyncCompletion::<SCScreenshotOutput>::new();
731
732 unsafe {
733 crate::ffi::sc_screenshot_manager_capture_screenshot_in_rect(
734 rect.origin.x,
735 rect.origin.y,
736 rect.size.width,
737 rect.size.height,
738 configuration.as_ptr(),
739 screenshot_output_callback,
740 context,
741 );
742 }
743
744 completion.wait().map_err(SCError::ScreenshotError)
745 }
746}
747
748// ============================================================================
749// SCScreenshotConfiguration (macOS 26.0+)
750// ============================================================================
751
752/// `UTType` identifiers are reverse-DNS strings; 256 bytes is comfortably
753/// above the longest identifier Apple ships and the bridge reports overflow as
754/// failure rather than truncating.
755#[cfg(feature = "macos_26_0")]
756const UTTYPE_IDENTIFIER_BUFFER: usize = crate::utils::ffi_string::SMALL_BUFFER_SIZE;
757
758/// Decode a bridge-owned C string into a `PathBuf` without going through
759/// `String`: file system paths are arbitrary non-NUL bytes on Darwin, and
760/// lossy UTF-8 conversion would silently rename them.
761#[cfg(feature = "macos_26_0")]
762fn owned_path<F: FnOnce() -> *mut i8>(ffi_call: F) -> Option<std::path::PathBuf> {
763 use std::os::unix::ffi::OsStrExt;
764
765 let ptr = ffi_call();
766 if ptr.is_null() {
767 return None;
768 }
769 // SAFETY: the bridge returns a `strdup`-allocated NUL-terminated buffer
770 // that ownership of transfers to us here.
771 let bytes = unsafe { std::ffi::CStr::from_ptr(ptr) }.to_bytes().to_vec();
772 unsafe { crate::ffi::sc_free_string(ptr) };
773 if bytes.is_empty() {
774 return None;
775 }
776 Some(std::path::PathBuf::from(std::ffi::OsStr::from_bytes(
777 &bytes,
778 )))
779}
780
781/// Drive one of the bridge's four-out-param rect getters.
782#[cfg(feature = "macos_26_0")]
783fn read_rect<F>(ffi_call: F) -> crate::cg::CGRect
784where
785 F: FnOnce(*mut f64, *mut f64, *mut f64, *mut f64),
786{
787 let mut x = 0.0;
788 let mut y = 0.0;
789 let mut width = 0.0;
790 let mut height = 0.0;
791 ffi_call(&raw mut x, &raw mut y, &raw mut width, &raw mut height);
792 crate::cg::CGRect::new(x, y, width, height)
793}
794
795/// Display intent for screenshot rendering (macOS 26.0+)
796#[cfg(feature = "macos_26_0")]
797#[repr(i32)]
798#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
799pub enum SCScreenshotDisplayIntent {
800 /// Render on the canonical display
801 #[default]
802 Canonical = 0,
803 /// Render on the local display
804 Local = 1,
805}
806
807#[cfg(feature = "macos_26_0")]
808impl SCScreenshotDisplayIntent {
809 pub const fn from_raw(raw: i32) -> Option<Self> {
810 match raw {
811 0 => Some(Self::Canonical),
812 1 => Some(Self::Local),
813 _ => None,
814 }
815 }
816}
817
818/// Dynamic range for screenshot output (macOS 26.0+)
819#[cfg(feature = "macos_26_0")]
820#[repr(i32)]
821#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
822pub enum SCScreenshotDynamicRange {
823 /// SDR output only
824 #[default]
825 SDR = 0,
826 /// HDR output only
827 HDR = 1,
828 /// Both SDR and HDR output
829 BothSDRAndHDR = 2,
830}
831
832#[cfg(feature = "macos_26_0")]
833impl SCScreenshotDynamicRange {
834 pub const fn from_raw(raw: i32) -> Option<Self> {
835 match raw {
836 0 => Some(Self::SDR),
837 1 => Some(Self::HDR),
838 2 => Some(Self::BothSDRAndHDR),
839 _ => None,
840 }
841 }
842}
843
844/// Why a screenshot file path could not be represented by Foundation.
845#[cfg(feature = "macos_26_0")]
846#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
847pub enum InvalidScreenshotPath {
848 /// Foundation file URLs require a valid UTF-8 path.
849 NotUtf8,
850 /// C strings cannot contain an interior NUL byte.
851 InteriorNul,
852}
853
854#[cfg(feature = "macos_26_0")]
855impl std::fmt::Display for InvalidScreenshotPath {
856 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
857 match self {
858 Self::NotUtf8 => f.write_str("screenshot path is not valid UTF-8"),
859 Self::InteriorNul => f.write_str("screenshot path contains an interior NUL byte"),
860 }
861 }
862}
863
864#[cfg(feature = "macos_26_0")]
865impl std::error::Error for InvalidScreenshotPath {}
866
867/// Configuration for advanced screenshot capture (macOS 26.0+)
868///
869/// Provides fine-grained control over screenshot output including:
870/// - Output dimensions
871/// - Source and destination rectangles
872/// - Shadow and clipping behavior
873/// - HDR/SDR dynamic range
874/// - File output
875///
876/// # Examples
877///
878/// ```no_run
879/// use screencapturekit::screenshot_manager::{SCScreenshotConfiguration, SCScreenshotDynamicRange};
880///
881/// let config = SCScreenshotConfiguration::new().expect("create screenshot configuration")
882/// .with_width(1920)
883/// .with_height(1080)
884/// .with_shows_cursor(true)
885/// .with_dynamic_range(SCScreenshotDynamicRange::BothSDRAndHDR);
886/// ```
887#[cfg(feature = "macos_26_0")]
888pub struct SCScreenshotConfiguration {
889 ptr: *const c_void,
890}
891
892#[cfg(feature = "macos_26_0")]
893impl SCScreenshotConfiguration {
894 /// Create a new screenshot configuration
895 ///
896 /// # Errors
897 /// Returns [`SCError::FeatureNotAvailable`] if the configuration cannot be
898 /// created (requires macOS 26.0+)
899 pub fn new() -> Result<Self, SCError> {
900 let ptr = unsafe { crate::ffi::sc_screenshot_configuration_create() };
901 if ptr.is_null() {
902 return Err(SCError::feature_not_available(
903 "SCScreenshotConfiguration",
904 "26.0",
905 ));
906 }
907 Ok(Self { ptr })
908 }
909
910 /// Set the output width in pixels
911 #[must_use]
912 #[allow(clippy::cast_possible_wrap)]
913 pub fn with_width(self, width: usize) -> Self {
914 unsafe {
915 crate::ffi::sc_screenshot_configuration_set_width(self.ptr, width as isize);
916 }
917 self
918 }
919
920 /// Set the output height in pixels
921 #[must_use]
922 #[allow(clippy::cast_possible_wrap)]
923 pub fn with_height(self, height: usize) -> Self {
924 unsafe {
925 crate::ffi::sc_screenshot_configuration_set_height(self.ptr, height as isize);
926 }
927 self
928 }
929
930 /// Set whether to show the cursor
931 #[must_use]
932 pub fn with_shows_cursor(self, shows_cursor: bool) -> Self {
933 unsafe {
934 crate::ffi::sc_screenshot_configuration_set_shows_cursor(self.ptr, shows_cursor);
935 }
936 self
937 }
938
939 /// Set the source rectangle (subset of capture area)
940 #[must_use]
941 pub fn with_source_rect(self, rect: crate::cg::CGRect) -> Self {
942 unsafe {
943 crate::ffi::sc_screenshot_configuration_set_source_rect(
944 self.ptr,
945 rect.origin.x,
946 rect.origin.y,
947 rect.size.width,
948 rect.size.height,
949 );
950 }
951 self
952 }
953
954 /// Set the destination rectangle (output area)
955 #[must_use]
956 pub fn with_destination_rect(self, rect: crate::cg::CGRect) -> Self {
957 unsafe {
958 crate::ffi::sc_screenshot_configuration_set_destination_rect(
959 self.ptr,
960 rect.origin.x,
961 rect.origin.y,
962 rect.size.width,
963 rect.size.height,
964 );
965 }
966 self
967 }
968
969 /// Set whether to ignore shadows
970 #[must_use]
971 pub fn with_ignore_shadows(self, ignore_shadows: bool) -> Self {
972 unsafe {
973 crate::ffi::sc_screenshot_configuration_set_ignore_shadows(self.ptr, ignore_shadows);
974 }
975 self
976 }
977
978 /// Set whether to ignore clipping
979 #[must_use]
980 pub fn with_ignore_clipping(self, ignore_clipping: bool) -> Self {
981 unsafe {
982 crate::ffi::sc_screenshot_configuration_set_ignore_clipping(self.ptr, ignore_clipping);
983 }
984 self
985 }
986
987 /// Set whether to include child windows
988 #[must_use]
989 pub fn with_include_child_windows(self, include_child_windows: bool) -> Self {
990 unsafe {
991 crate::ffi::sc_screenshot_configuration_set_include_child_windows(
992 self.ptr,
993 include_child_windows,
994 );
995 }
996 self
997 }
998
999 /// Set the display intent
1000 #[must_use]
1001 pub fn with_display_intent(self, display_intent: SCScreenshotDisplayIntent) -> Self {
1002 unsafe {
1003 crate::ffi::sc_screenshot_configuration_set_display_intent(
1004 self.ptr,
1005 display_intent as i32,
1006 );
1007 }
1008 self
1009 }
1010
1011 /// Set the dynamic range
1012 #[must_use]
1013 pub fn with_dynamic_range(self, dynamic_range: SCScreenshotDynamicRange) -> Self {
1014 unsafe {
1015 crate::ffi::sc_screenshot_configuration_set_dynamic_range(
1016 self.ptr,
1017 dynamic_range as i32,
1018 );
1019 }
1020 self
1021 }
1022
1023 /// Set the output file path.
1024 ///
1025 /// Accepts anything path-like (`&str`, `String`, `&Path`, `PathBuf`).
1026 ///
1027 /// # Errors
1028 ///
1029 /// Returns [`InvalidScreenshotPath`] and leaves the configuration unchanged
1030 /// if Foundation cannot represent the path.
1031 pub fn set_file_path(
1032 &mut self,
1033 path: impl AsRef<std::path::Path>,
1034 ) -> Result<&mut Self, InvalidScreenshotPath> {
1035 let path = path
1036 .as_ref()
1037 .to_str()
1038 .ok_or(InvalidScreenshotPath::NotUtf8)?;
1039 let c_path =
1040 std::ffi::CString::new(path).map_err(|_| InvalidScreenshotPath::InteriorNul)?;
1041 unsafe {
1042 crate::ffi::sc_screenshot_configuration_set_file_url(self.ptr, c_path.as_ptr());
1043 }
1044 Ok(self)
1045 }
1046
1047 /// Set the output file path (builder pattern).
1048 ///
1049 /// See [`set_file_path`](Self::set_file_path) for how invalid paths are
1050 /// handled.
1051 #[allow(clippy::missing_errors_doc)]
1052 pub fn with_file_path(
1053 mut self,
1054 path: impl AsRef<std::path::Path>,
1055 ) -> Result<Self, InvalidScreenshotPath> {
1056 self.set_file_path(path)?;
1057 Ok(self)
1058 }
1059
1060 /// Clear any previously configured output file path.
1061 #[must_use]
1062 pub fn without_file_path(self) -> Self {
1063 unsafe { crate::ffi::sc_screenshot_configuration_clear_file_url(self.ptr) };
1064 self
1065 }
1066
1067 /// Clear any previously configured output file path.
1068 pub fn clear_file_path(&mut self) -> &mut Self {
1069 unsafe { crate::ffi::sc_screenshot_configuration_clear_file_url(self.ptr) };
1070 self
1071 }
1072
1073 /// Get the configured output file path, if one was set.
1074 #[must_use]
1075 pub fn file_path(&self) -> Option<std::path::PathBuf> {
1076 owned_path(|| unsafe {
1077 crate::ffi::sc_screenshot_configuration_get_file_path_owned(self.ptr)
1078 })
1079 }
1080
1081 /// Get the configured output width in pixels.
1082 #[must_use]
1083 pub fn width(&self) -> usize {
1084 let width = unsafe { crate::ffi::sc_screenshot_configuration_get_width(self.ptr) };
1085 usize::try_from(width).unwrap_or(0)
1086 }
1087
1088 /// Get the configured output height in pixels.
1089 #[must_use]
1090 pub fn height(&self) -> usize {
1091 let height = unsafe { crate::ffi::sc_screenshot_configuration_get_height(self.ptr) };
1092 usize::try_from(height).unwrap_or(0)
1093 }
1094
1095 /// Whether the cursor will be drawn into the screenshot.
1096 #[must_use]
1097 pub fn shows_cursor(&self) -> bool {
1098 unsafe { crate::ffi::sc_screenshot_configuration_get_shows_cursor(self.ptr) }
1099 }
1100
1101 /// Get the source rectangle (the subset of the capture area to read).
1102 #[must_use]
1103 pub fn source_rect(&self) -> crate::cg::CGRect {
1104 read_rect(|x, y, w, h| unsafe {
1105 crate::ffi::sc_screenshot_configuration_get_source_rect(self.ptr, x, y, w, h);
1106 })
1107 }
1108
1109 /// Get the destination rectangle (where the source is drawn in the output).
1110 #[must_use]
1111 pub fn destination_rect(&self) -> crate::cg::CGRect {
1112 read_rect(|x, y, w, h| unsafe {
1113 crate::ffi::sc_screenshot_configuration_get_destination_rect(self.ptr, x, y, w, h);
1114 })
1115 }
1116
1117 /// Whether window shadows are excluded from the screenshot.
1118 #[must_use]
1119 pub fn ignore_shadows(&self) -> bool {
1120 unsafe { crate::ffi::sc_screenshot_configuration_get_ignore_shadows(self.ptr) }
1121 }
1122
1123 /// Whether clipping to the window bounds is ignored.
1124 #[must_use]
1125 pub fn ignore_clipping(&self) -> bool {
1126 unsafe { crate::ffi::sc_screenshot_configuration_get_ignore_clipping(self.ptr) }
1127 }
1128
1129 /// Whether child windows are included in the screenshot.
1130 #[must_use]
1131 pub fn include_child_windows(&self) -> bool {
1132 unsafe { crate::ffi::sc_screenshot_configuration_get_include_child_windows(self.ptr) }
1133 }
1134
1135 /// Get the display intent.
1136 ///
1137 /// Returns [`SCError::UnknownValue`] if the framework reported an intent
1138 /// this crate does not know about (a newer macOS adding a case).
1139 #[allow(clippy::missing_errors_doc)]
1140 pub fn display_intent(&self) -> Result<SCScreenshotDisplayIntent, SCError> {
1141 let mut raw = 0_i32;
1142 if !unsafe {
1143 crate::ffi::sc_screenshot_configuration_get_display_intent(self.ptr, &raw mut raw)
1144 } {
1145 return Err(SCError::feature_not_available(
1146 "SCScreenshotConfiguration.displayIntent",
1147 "26.0",
1148 ));
1149 }
1150 SCScreenshotDisplayIntent::from_raw(raw).ok_or_else(|| SCError::UnknownValue {
1151 type_name: "SCScreenshotDisplayIntent",
1152 raw: i64::from(raw),
1153 })
1154 }
1155
1156 /// Get the dynamic range.
1157 ///
1158 /// Returns [`SCError::UnknownValue`] if the framework reported a range
1159 /// this crate does not know about (a newer macOS adding a case).
1160 #[allow(clippy::missing_errors_doc)]
1161 pub fn dynamic_range(&self) -> Result<SCScreenshotDynamicRange, SCError> {
1162 let mut raw = 0_i32;
1163 if !unsafe {
1164 crate::ffi::sc_screenshot_configuration_get_dynamic_range(self.ptr, &raw mut raw)
1165 } {
1166 return Err(SCError::feature_not_available(
1167 "SCScreenshotConfiguration.dynamicRange",
1168 "26.0",
1169 ));
1170 }
1171 SCScreenshotDynamicRange::from_raw(raw).ok_or_else(|| SCError::UnknownValue {
1172 type_name: "SCScreenshotDynamicRange",
1173 raw: i64::from(raw),
1174 })
1175 }
1176
1177 /// Set the content type (output format) using `UTType` identifier
1178 ///
1179 /// Common identifiers include:
1180 /// - `"public.png"` - PNG format
1181 /// - `"public.jpeg"` - JPEG format
1182 /// - `"public.heic"` - HEIC format
1183 /// - `"public.tiff"` - TIFF format
1184 ///
1185 /// Use [`supported_content_types()`](Self::supported_content_types) to get
1186 /// available formats.
1187 ///
1188 /// # Errors
1189 ///
1190 /// Returns [`InteriorNulError`] if `identifier` contains an interior NUL
1191 /// byte. Valid `UTType` identifiers never contain NUL bytes.
1192 pub fn with_content_type(self, identifier: &str) -> Result<Self, InteriorNulError> {
1193 let c_id = std::ffi::CString::new(identifier).map_err(|_| InteriorNulError)?;
1194 unsafe {
1195 crate::ffi::sc_screenshot_configuration_set_content_type(self.ptr, c_id.as_ptr());
1196 }
1197 Ok(self)
1198 }
1199
1200 /// Get the current content type as `UTType` identifier
1201 ///
1202 /// Returns `None` when no content type is set or the identifier does not
1203 /// fit in the transfer buffer; the bridge reports overflow as failure
1204 /// rather than handing back a truncated identifier.
1205 #[must_use]
1206 pub fn content_type(&self) -> Option<String> {
1207 unsafe {
1208 crate::utils::ffi_string::ffi_string_from_buffer(
1209 UTTYPE_IDENTIFIER_BUFFER,
1210 |buffer, len| {
1211 crate::ffi::sc_screenshot_configuration_get_content_type(
1212 self.ptr,
1213 buffer,
1214 usize::try_from(len).unwrap_or(0),
1215 )
1216 },
1217 )
1218 }
1219 }
1220
1221 /// Get the list of supported content types (`UTType` identifiers)
1222 ///
1223 /// Returns a list of `UTType` identifiers that can be used with
1224 /// [`with_content_type()`](Self::with_content_type).
1225 ///
1226 /// Common types include:
1227 /// - `"public.png"` - PNG format
1228 /// - `"public.jpeg"` - JPEG format
1229 /// - `"public.heic"` - HEIC format
1230 #[must_use]
1231 pub fn supported_content_types() -> Vec<String> {
1232 let count =
1233 unsafe { crate::ffi::sc_screenshot_configuration_get_supported_content_types_count() };
1234 (0..count)
1235 .filter_map(|i| unsafe {
1236 crate::utils::ffi_string::ffi_string_from_buffer(
1237 UTTYPE_IDENTIFIER_BUFFER,
1238 |buffer, len| {
1239 crate::ffi::sc_screenshot_configuration_get_supported_content_type_at(
1240 i,
1241 buffer,
1242 usize::try_from(len).unwrap_or(0),
1243 )
1244 },
1245 )
1246 })
1247 .collect()
1248 }
1249
1250 #[must_use]
1251 pub const fn as_ptr(&self) -> *const c_void {
1252 self.ptr
1253 }
1254}
1255
1256#[cfg(feature = "macos_26_0")]
1257impl std::fmt::Debug for SCScreenshotConfiguration {
1258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1259 f.debug_struct("SCScreenshotConfiguration")
1260 .field("content_type", &self.content_type())
1261 .finish_non_exhaustive()
1262 }
1263}
1264
1265#[cfg(feature = "macos_26_0")]
1266crate::utils::retained::sc_retained!(
1267 SCScreenshotConfiguration,
1268 field = ptr,
1269 release = crate::ffi::sc_screenshot_configuration_release,
1270);
1271
1272// SAFETY: `SCScreenshotConfiguration` wraps a mutable Objective-C object, but
1273// the wrapper owns it exclusively: there is no `Clone` and no constructor that
1274// hands out a second handle to the same instance. Mutation is therefore
1275// confined to `&mut self` / by-value builder methods, which Rust already makes
1276// exclusive, and the `&self` methods are pure getters. Objective-C reference
1277// counting is atomic, so `Drop` is safe from any thread.
1278#[cfg(feature = "macos_26_0")]
1279unsafe impl Send for SCScreenshotConfiguration {}
1280#[cfg(feature = "macos_26_0")]
1281unsafe impl Sync for SCScreenshotConfiguration {}
1282
1283// ============================================================================
1284// SCScreenshotOutput (macOS 26.0+)
1285// ============================================================================
1286
1287/// Output from advanced screenshot capture (macOS 26.0+)
1288///
1289/// Contains SDR and/or HDR images depending on the configuration,
1290/// and optionally the file URL where the image was saved.
1291#[cfg(feature = "macos_26_0")]
1292pub struct SCScreenshotOutput {
1293 ptr: *const c_void,
1294}
1295
1296#[cfg(feature = "macos_26_0")]
1297impl SCScreenshotOutput {
1298 pub(crate) fn from_ptr(ptr: *const c_void) -> Self {
1299 Self { ptr }
1300 }
1301
1302 /// Get the SDR image if available
1303 #[must_use]
1304 pub fn sdr_image(&self) -> Option<CGImage> {
1305 let ptr = unsafe { crate::ffi::sc_screenshot_output_get_sdr_image(self.ptr) };
1306 if ptr.is_null() {
1307 None
1308 } else {
1309 Some(unsafe { cgimage_from_retained_ptr(ptr) })
1310 }
1311 }
1312
1313 /// Get the HDR image if available
1314 #[must_use]
1315 pub fn hdr_image(&self) -> Option<CGImage> {
1316 let ptr = unsafe { crate::ffi::sc_screenshot_output_get_hdr_image(self.ptr) };
1317 if ptr.is_null() {
1318 None
1319 } else {
1320 Some(unsafe { cgimage_from_retained_ptr(ptr) })
1321 }
1322 }
1323
1324 /// Get the path the image was written to, if the configuration asked for
1325 /// file output.
1326 ///
1327 /// Read as an owned path from the bridge, so there is no fixed-size buffer
1328 /// to truncate long values.
1329 #[must_use]
1330 pub fn file_path(&self) -> Option<std::path::PathBuf> {
1331 owned_path(|| unsafe { crate::ffi::sc_screenshot_output_get_file_path_owned(self.ptr) })
1332 }
1333}
1334
1335#[cfg(feature = "macos_26_0")]
1336impl std::fmt::Debug for SCScreenshotOutput {
1337 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1338 f.debug_struct("SCScreenshotOutput")
1339 .field(
1340 "sdr_image",
1341 &self.sdr_image().map(|i| (i.width(), i.height())),
1342 )
1343 .field(
1344 "hdr_image",
1345 &self.hdr_image().map(|i| (i.width(), i.height())),
1346 )
1347 .field("file_path", &self.file_path())
1348 .finish()
1349 }
1350}
1351
1352#[cfg(feature = "macos_26_0")]
1353crate::utils::retained::sc_retained!(
1354 SCScreenshotOutput,
1355 field = ptr,
1356 release = crate::ffi::sc_screenshot_output_release,
1357);
1358
1359// SAFETY: `SCScreenshotOutput` wraps an immutable Objective-C ScreenCaptureKit
1360// object whose reference counting is atomic; it is safe to send between and
1361// share across threads.
1362#[cfg(feature = "macos_26_0")]
1363unsafe impl Send for SCScreenshotOutput {}
1364#[cfg(feature = "macos_26_0")]
1365unsafe impl Sync for SCScreenshotOutput {}
1366
1367#[cfg(all(test, feature = "macos_26_0"))]
1368mod tests {
1369 use super::{SCScreenshotConfiguration, SCScreenshotDisplayIntent, SCScreenshotDynamicRange};
1370
1371 #[test]
1372 fn bridge_rejects_unknown_screenshot_raw_values() {
1373 let config = SCScreenshotConfiguration::new()
1374 .expect("macOS 26.0 or later")
1375 .with_display_intent(SCScreenshotDisplayIntent::Local)
1376 .with_dynamic_range(SCScreenshotDynamicRange::HDR);
1377 for raw in [3, -1, i32::MAX, i32::MIN] {
1378 let intent_applied = unsafe {
1379 crate::ffi::sc_screenshot_configuration_set_display_intent(config.ptr, raw)
1380 };
1381 let range_applied = unsafe {
1382 crate::ffi::sc_screenshot_configuration_set_dynamic_range(config.ptr, raw)
1383 };
1384 assert!(!intent_applied, "the bridge accepted display intent {raw}");
1385 assert!(!range_applied, "the bridge accepted dynamic range {raw}");
1386 assert_eq!(
1387 config.display_intent(),
1388 Ok(SCScreenshotDisplayIntent::Local)
1389 );
1390 assert_eq!(config.dynamic_range(), Ok(SCScreenshotDynamicRange::HDR));
1391 }
1392 }
1393}