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
52#[doc(no_inline)]
53pub use apple_cf::cg::CGImage;
54
55/// Image output format for saving screenshots
56///
57/// # Examples
58///
59/// ```no_run
60/// use screencapturekit::screenshot_manager::ImageFormat;
61///
62/// // PNG for lossless quality
63/// let format = ImageFormat::Png;
64///
65/// // JPEG with 80% quality
66/// let format = ImageFormat::Jpeg(0.8);
67///
68/// // HEIC with 90% quality (smaller file size than JPEG)
69/// let format = ImageFormat::Heic(0.9);
70/// ```
71#[derive(Debug, Clone, Copy, PartialEq)]
72pub enum ImageFormat {
73 /// PNG format (lossless)
74 Png,
75 /// JPEG format with quality (0.0-1.0)
76 Jpeg(f32),
77 /// TIFF format (lossless)
78 Tiff,
79 /// GIF format
80 Gif,
81 /// BMP format
82 Bmp,
83 /// HEIC format with quality (0.0-1.0) - efficient compression
84 Heic(f32),
85}
86
87impl ImageFormat {
88 fn to_format_id(self) -> i32 {
89 match self {
90 Self::Png => 0,
91 Self::Jpeg(_) => 1,
92 Self::Tiff => 2,
93 Self::Gif => 3,
94 Self::Bmp => 4,
95 Self::Heic(_) => 5,
96 }
97 }
98
99 fn quality(self) -> f32 {
100 match self {
101 Self::Jpeg(q) | Self::Heic(q) => q.clamp(0.0, 1.0),
102 _ => 1.0,
103 }
104 }
105
106 /// Get the typical file extension for this format
107 #[must_use]
108 pub const fn extension(&self) -> &'static str {
109 match self {
110 Self::Png => "png",
111 Self::Jpeg(_) => "jpg",
112 Self::Tiff => "tiff",
113 Self::Gif => "gif",
114 Self::Bmp => "bmp",
115 Self::Heic(_) => "heic",
116 }
117 }
118}
119
120/// # Safety
121/// `ptr` must be a non-null retained `CGImageRef` whose +1 ownership is
122/// transferred to the returned wrapper.
123pub(crate) unsafe fn cgimage_from_retained_ptr(ptr: *const c_void) -> CGImage {
124 unsafe { CGImage::from_raw(ptr.cast_mut()) }
125}
126
127extern "C" fn image_callback(
128 image_ptr: *const c_void,
129 error_ptr: *const i8,
130 user_data: *mut c_void,
131) {
132 crate::utils::panic_safe::catch_user_panic("image_callback", move || {
133 if !error_ptr.is_null() {
134 // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
135 let error = unsafe { error_from_cstr(error_ptr) };
136 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
137 unsafe { SyncCompletion::<CGImage>::complete_err(user_data, error) };
138 } else if !image_ptr.is_null() {
139 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
140 unsafe { SyncCompletion::complete_ok(user_data, cgimage_from_retained_ptr(image_ptr)) };
141 } else {
142 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
143 unsafe {
144 SyncCompletion::<CGImage>::complete_err(user_data, "Unknown error".to_string());
145 };
146 }
147 });
148}
149
150extern "C" fn buffer_callback(
151 buffer_ptr: *const c_void,
152 error_ptr: *const i8,
153 user_data: *mut c_void,
154) {
155 crate::utils::panic_safe::catch_user_panic("buffer_callback", move || {
156 if !error_ptr.is_null() {
157 // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
158 let error = unsafe { error_from_cstr(error_ptr) };
159 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
160 unsafe { SyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(user_data, error) };
161 } else if !buffer_ptr.is_null() {
162 // 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.
163 let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(buffer_ptr.cast_mut()) };
164 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
165 unsafe { SyncCompletion::complete_ok(user_data, buffer) };
166 } else {
167 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
168 unsafe {
169 SyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(
170 user_data,
171 "Unknown error".to_string(),
172 );
173 };
174 }
175 });
176}
177
178#[cfg(feature = "macos_26_0")]
179extern "C" fn screenshot_output_callback(
180 output_ptr: *const c_void,
181 error_ptr: *const i8,
182 user_data: *mut c_void,
183) {
184 crate::utils::panic_safe::catch_user_panic("screenshot_output_callback", move || {
185 if !error_ptr.is_null() {
186 // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
187 let error = unsafe { error_from_cstr(error_ptr) };
188 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
189 unsafe { SyncCompletion::<SCScreenshotOutput>::complete_err(user_data, error) };
190 } else if !output_ptr.is_null() {
191 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
192 unsafe {
193 SyncCompletion::complete_ok(user_data, SCScreenshotOutput::from_ptr(output_ptr));
194 };
195 } else {
196 // SAFETY: `user_data` is the one-shot completion context from `SyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
197 unsafe {
198 SyncCompletion::<SCScreenshotOutput>::complete_err(
199 user_data,
200 "Unknown error".to_string(),
201 );
202 };
203 }
204 });
205}
206
207/// Screenshot-specific helpers implemented for the canonical [`CGImage`] type.
208///
209/// Import this trait to access pixel extraction helpers and multi-format file
210/// export on images returned by [`SCScreenshotManager`].
211pub trait CGImageExt {
212 /// Get raw RGBA pixel data.
213 ///
214 /// # Errors
215 /// Returns an error if the pixel data cannot be extracted.
216 fn rgba_data(&self) -> Result<Vec<u8>, SCError>;
217
218 /// Get raw BGRA pixel data.
219 ///
220 /// # Errors
221 /// Returns an error if the pixel data cannot be extracted.
222 fn bgra_data(&self) -> Result<Vec<u8>, SCError>;
223
224 /// Render the image's RGBA bytes into a caller-supplied buffer.
225 ///
226 /// # Errors
227 /// Returns an error if `dest` is too small or the render fails.
228 ///
229 /// # Examples
230 ///
231 /// ```no_run
232 /// # use screencapturekit::screenshot_manager::{CGImageExt, SCScreenshotManager};
233 /// # use screencapturekit::stream::{content_filter::SCContentFilter, configuration::SCStreamConfiguration};
234 /// # use screencapturekit::shareable_content::SCShareableContent;
235 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
236 /// # let content = SCShareableContent::get()?;
237 /// # let display = &content.displays()[0];
238 /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
239 /// # let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
240 /// let mut buffer: Vec<u8> = vec![0; 1920 * 1080 * 4];
241 /// let img = SCScreenshotManager::capture_image(&filter, &config)?;
242 /// img.rgba_data_into(&mut buffer)?;
243 /// # Ok(())
244 /// # }
245 /// ```
246 fn rgba_data_into(&self, dest: &mut [u8]) -> Result<usize, SCError>;
247
248 /// Render the image's BGRA bytes into a caller-supplied buffer.
249 ///
250 /// # Errors
251 /// Returns an error if `dest` is too small or the render fails.
252 fn bgra_data_into(&self, dest: &mut [u8]) -> Result<usize, SCError>;
253
254 /// Render the image's RGBA bytes into a caller-supplied buffer using an
255 /// explicit row stride (`dest_bytes_per_row`).
256 ///
257 /// Unlike [`rgba_data_into`](CGImageExt::rgba_data_into), which assumes
258 /// tightly-packed rows (`width * 4`), this accepts a caller-specified row
259 /// stride so consumers with padded/row-aligned buffers (GPU upload, wgpu)
260 /// aren't forced into tight packing.
261 ///
262 /// Returns the number of bytes spanned (`height * dest_bytes_per_row`).
263 ///
264 /// # Errors
265 /// Returns an error if `dest_bytes_per_row` is smaller than `width * 4`,
266 /// if `dest` cannot hold `height * dest_bytes_per_row` bytes, or if the
267 /// render fails.
268 fn rgba_data_into_strided(
269 &self,
270 dest: &mut [u8],
271 dest_bytes_per_row: usize,
272 ) -> Result<usize, SCError>;
273
274 /// Render the image's BGRA bytes into a caller-supplied buffer using an
275 /// explicit row stride (`dest_bytes_per_row`).
276 ///
277 /// See [`rgba_data_into_strided`](CGImageExt::rgba_data_into_strided) for
278 /// the row-stride semantics.
279 ///
280 /// # Errors
281 /// Returns an error if `dest_bytes_per_row` is smaller than `width * 4`,
282 /// if `dest` cannot hold `height * dest_bytes_per_row` bytes, or if the
283 /// render fails.
284 fn bgra_data_into_strided(
285 &self,
286 dest: &mut [u8],
287 dest_bytes_per_row: usize,
288 ) -> Result<usize, SCError>;
289
290 /// Save the image to a file in the specified format.
291 ///
292 /// # Errors
293 /// Returns an error if the path contains interior null bytes or the export fails.
294 ///
295 /// # Examples
296 ///
297 /// ```no_run
298 /// # use screencapturekit::screenshot_manager::{CGImageExt, ImageFormat, SCScreenshotManager};
299 /// # use screencapturekit::stream::{content_filter::SCContentFilter, configuration::SCStreamConfiguration};
300 /// # use screencapturekit::shareable_content::SCShareableContent;
301 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
302 /// # let content = SCShareableContent::get()?;
303 /// # let display = &content.displays()[0];
304 /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
305 /// # let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
306 /// let image = SCScreenshotManager::capture_image(&filter, &config)?;
307 /// image.save("screenshot.png", ImageFormat::Png)?;
308 /// image.save("screenshot.jpg", ImageFormat::Jpeg(0.85))?;
309 /// image.save("screenshot.heic", ImageFormat::Heic(0.9))?;
310 /// # Ok(())
311 /// # }
312 /// ```
313 fn save(&self, path: &str, format: ImageFormat) -> Result<(), SCError>;
314}
315
316/// Internal selector for the channel ordering passed to the Swift renderer.
317#[derive(Debug, Clone, Copy)]
318enum PixelLayout {
319 Rgba,
320 Bgra,
321}
322
323impl PixelLayout {
324 const fn name(self) -> &'static str {
325 match self {
326 Self::Rgba => "RGBA",
327 Self::Bgra => "BGRA",
328 }
329 }
330
331 /// Dispatch into the matching Swift bridge entry point.
332 ///
333 /// # Safety
334 /// The destination must point to at least `capacity` bytes and `ptr` must
335 /// be a live retained `CGImage`.
336 unsafe fn render(self, ptr: *const c_void, dest: *mut u8, capacity: usize) -> usize {
337 unsafe {
338 match self {
339 Self::Rgba => crate::ffi::cgimage_render_rgba_into(ptr, dest, capacity),
340 Self::Bgra => crate::ffi::cgimage_render_bgra_into(ptr, dest, capacity),
341 }
342 }
343 }
344
345 /// Dispatch into the matching strided Swift bridge entry point.
346 ///
347 /// # Safety
348 /// The destination must point to at least `capacity` bytes, span
349 /// `bytes_per_row` per image row, and `ptr` must be a live retained
350 /// `CGImage`.
351 unsafe fn render_strided(
352 self,
353 ptr: *const c_void,
354 dest: *mut u8,
355 capacity: usize,
356 bytes_per_row: usize,
357 ) -> usize {
358 unsafe {
359 match self {
360 Self::Rgba => {
361 crate::ffi::cgimage_render_rgba_into_strided(ptr, dest, capacity, bytes_per_row)
362 }
363 Self::Bgra => {
364 crate::ffi::cgimage_render_bgra_into_strided(ptr, dest, capacity, bytes_per_row)
365 }
366 }
367 }
368 }
369}
370
371impl CGImageExt for CGImage {
372 fn rgba_data(&self) -> Result<Vec<u8>, SCError> {
373 render_pixel_data(self, PixelLayout::Rgba)
374 }
375
376 fn bgra_data(&self) -> Result<Vec<u8>, SCError> {
377 render_pixel_data(self, PixelLayout::Bgra)
378 }
379
380 fn rgba_data_into(&self, dest: &mut [u8]) -> Result<usize, SCError> {
381 render_pixel_data_into(self, dest, PixelLayout::Rgba)
382 }
383
384 fn bgra_data_into(&self, dest: &mut [u8]) -> Result<usize, SCError> {
385 render_pixel_data_into(self, dest, PixelLayout::Bgra)
386 }
387
388 fn rgba_data_into_strided(
389 &self,
390 dest: &mut [u8],
391 dest_bytes_per_row: usize,
392 ) -> Result<usize, SCError> {
393 render_pixel_data_into_strided(self, dest, dest_bytes_per_row, PixelLayout::Rgba)
394 }
395
396 fn bgra_data_into_strided(
397 &self,
398 dest: &mut [u8],
399 dest_bytes_per_row: usize,
400 ) -> Result<usize, SCError> {
401 render_pixel_data_into_strided(self, dest, dest_bytes_per_row, PixelLayout::Bgra)
402 }
403
404 fn save(&self, path: &str, format: ImageFormat) -> Result<(), SCError> {
405 let c_path = std::ffi::CString::new(path)
406 .map_err(|_| SCError::internal_error("Path contains null bytes"))?;
407
408 let success = unsafe {
409 crate::ffi::cgimage_save_to_file(
410 self.as_ptr(),
411 c_path.as_ptr(),
412 format.to_format_id(),
413 format.quality(),
414 )
415 };
416
417 if success {
418 Ok(())
419 } else {
420 Err(SCError::internal_error(format!(
421 "Failed to save image as {}",
422 format.extension().to_uppercase()
423 )))
424 }
425 }
426}
427
428fn render_pixel_data(image: &CGImage, layout: PixelLayout) -> Result<Vec<u8>, SCError> {
429 let total_bytes = required_byte_size(image)?;
430 if total_bytes == 0 {
431 return Ok(Vec::new());
432 }
433
434 let mut data: Vec<u8> = Vec::with_capacity(total_bytes);
435 let written = unsafe { layout.render(image.as_ptr(), data.as_mut_ptr(), total_bytes) };
436
437 if written != total_bytes {
438 return Err(SCError::internal_error(format!(
439 "Failed to render CGImage into {} buffer",
440 layout.name()
441 )));
442 }
443
444 unsafe { data.set_len(total_bytes) };
445 Ok(data)
446}
447
448fn render_pixel_data_into(
449 image: &CGImage,
450 dest: &mut [u8],
451 layout: PixelLayout,
452) -> Result<usize, SCError> {
453 let total_bytes = required_byte_size(image)?;
454 if dest.len() < total_bytes {
455 return Err(SCError::internal_error(format!(
456 "Destination buffer too small: need {total_bytes} bytes, got {}",
457 dest.len()
458 )));
459 }
460 if total_bytes == 0 {
461 return Ok(0);
462 }
463
464 let written = unsafe { layout.render(image.as_ptr(), dest.as_mut_ptr(), total_bytes) };
465 if written != total_bytes {
466 return Err(SCError::internal_error(format!(
467 "Failed to render CGImage into {} buffer",
468 layout.name()
469 )));
470 }
471 Ok(written)
472}
473
474fn render_pixel_data_into_strided(
475 image: &CGImage,
476 dest: &mut [u8],
477 dest_bytes_per_row: usize,
478 layout: PixelLayout,
479) -> Result<usize, SCError> {
480 let width = image.width();
481 let height = image.height();
482
483 let min_bytes_per_row = width
484 .checked_mul(4)
485 .ok_or_else(|| SCError::internal_error("CGImage row size overflows usize"))?;
486 if dest_bytes_per_row < min_bytes_per_row {
487 return Err(SCError::internal_error(format!(
488 "Destination row stride too small: need at least {min_bytes_per_row} bytes, got {dest_bytes_per_row}"
489 )));
490 }
491
492 let required = height
493 .checked_mul(dest_bytes_per_row)
494 .ok_or_else(|| SCError::internal_error("CGImage strided size overflows usize"))?;
495 if dest.len() < required {
496 return Err(SCError::internal_error(format!(
497 "Destination buffer too small: need {required} bytes, got {}",
498 dest.len()
499 )));
500 }
501 if required == 0 {
502 return Ok(0);
503 }
504
505 let written = unsafe {
506 layout.render_strided(
507 image.as_ptr(),
508 dest.as_mut_ptr(),
509 dest.len(),
510 dest_bytes_per_row,
511 )
512 };
513 if written != required {
514 return Err(SCError::internal_error(format!(
515 "Failed to render CGImage into {} buffer",
516 layout.name()
517 )));
518 }
519 Ok(written)
520}
521
522fn required_byte_size(image: &CGImage) -> Result<usize, SCError> {
523 image
524 .width()
525 .checked_mul(image.height())
526 .and_then(|n| n.checked_mul(4))
527 .ok_or_else(|| SCError::internal_error("CGImage dimensions overflow usize"))
528}
529
530/// Manager for capturing single screenshots
531///
532/// Available on macOS 14.0+. Provides a simpler API than `SCStream` for one-time captures.
533///
534/// # Examples
535///
536/// ```no_run
537/// use screencapturekit::screenshot_manager::SCScreenshotManager;
538/// use screencapturekit::stream::{content_filter::SCContentFilter, configuration::SCStreamConfiguration};
539/// use screencapturekit::shareable_content::SCShareableContent;
540///
541/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
542/// let content = SCShareableContent::get()?;
543/// let display = &content.displays()[0];
544/// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
545/// let config = SCStreamConfiguration::new()
546/// .with_width(1920)
547/// .with_height(1080);
548///
549/// let image = SCScreenshotManager::capture_image(&filter, &config)?;
550/// println!("Captured screenshot: {}x{}", image.width(), image.height());
551/// # Ok(())
552/// # }
553/// ```
554#[derive(Debug)]
555pub struct SCScreenshotManager;
556
557impl SCScreenshotManager {
558 /// Capture a single screenshot as a `CGImage`
559 ///
560 /// # Errors
561 /// Returns an error if:
562 /// - The system is not macOS 14.0+
563 /// - Screen recording permission is not granted
564 /// - The capture fails for any reason
565 ///
566 /// # Panics
567 /// Panics if the internal mutex is poisoned.
568 pub fn capture_image(
569 content_filter: &SCContentFilter,
570 configuration: &SCStreamConfiguration,
571 ) -> Result<CGImage, SCError> {
572 let (completion, context) = SyncCompletion::<CGImage>::new();
573
574 unsafe {
575 crate::ffi::sc_screenshot_manager_capture_image(
576 content_filter.as_ptr(),
577 configuration.as_ptr(),
578 image_callback,
579 context,
580 );
581 }
582
583 completion.wait().map_err(SCError::ScreenshotError)
584 }
585
586 /// Capture a single screenshot as a `CMSampleBuffer`
587 ///
588 /// Returns the sample buffer for advanced processing.
589 ///
590 /// # Errors
591 /// Returns an error if:
592 /// - The system is not macOS 14.0+
593 /// - Screen recording permission is not granted
594 /// - The capture fails for any reason
595 ///
596 /// # Panics
597 /// Panics if the internal mutex is poisoned.
598 pub fn capture_sample_buffer(
599 content_filter: &SCContentFilter,
600 configuration: &SCStreamConfiguration,
601 ) -> Result<crate::cm::CMSampleBuffer, SCError> {
602 let (completion, context) = SyncCompletion::<crate::cm::CMSampleBuffer>::new();
603
604 unsafe {
605 crate::ffi::sc_screenshot_manager_capture_sample_buffer(
606 content_filter.as_ptr(),
607 configuration.as_ptr(),
608 buffer_callback,
609 context,
610 );
611 }
612
613 completion.wait().map_err(SCError::ScreenshotError)
614 }
615
616 /// Capture a screenshot of a specific screen region (macOS 15.2+)
617 ///
618 /// This method captures the content within the specified rectangle,
619 /// which can span multiple displays.
620 ///
621 /// # Arguments
622 /// * `rect` - The rectangle to capture, in screen coordinates (points)
623 ///
624 /// # Errors
625 /// Returns an error if:
626 /// - The system is not macOS 15.2+
627 /// - Screen recording permission is not granted
628 /// - The capture fails for any reason
629 ///
630 /// # Examples
631 /// ```no_run
632 /// use screencapturekit::screenshot_manager::SCScreenshotManager;
633 /// use screencapturekit::cg::CGRect;
634 ///
635 /// fn example() -> Result<(), screencapturekit::utils::error::SCError> {
636 /// let rect = CGRect::new(0.0, 0.0, 1920.0, 1080.0);
637 /// let image = SCScreenshotManager::capture_image_in_rect(rect)?;
638 /// Ok(())
639 /// }
640 /// ```
641 #[cfg(feature = "macos_15_2")]
642 pub fn capture_image_in_rect(rect: CGRect) -> Result<CGImage, SCError> {
643 let (completion, context) = SyncCompletion::<CGImage>::new();
644
645 unsafe {
646 crate::ffi::sc_screenshot_manager_capture_image_in_rect(
647 rect.origin.x,
648 rect.origin.y,
649 rect.size.width,
650 rect.size.height,
651 image_callback,
652 context,
653 );
654 }
655
656 completion.wait().map_err(SCError::ScreenshotError)
657 }
658
659 /// Capture a screenshot with advanced configuration (macOS 26.0+)
660 ///
661 /// This method uses the new `SCScreenshotConfiguration` for more control
662 /// over the screenshot output, including HDR support and file saving.
663 ///
664 /// # Arguments
665 /// * `content_filter` - The content filter specifying what to capture
666 /// * `configuration` - The screenshot configuration
667 ///
668 /// # Errors
669 /// Returns an error if the capture fails
670 ///
671 /// # Examples
672 /// ```no_run
673 /// use screencapturekit::screenshot_manager::{SCScreenshotManager, SCScreenshotConfiguration, SCScreenshotDynamicRange};
674 /// use screencapturekit::stream::content_filter::SCContentFilter;
675 /// use screencapturekit::shareable_content::SCShareableContent;
676 ///
677 /// fn example() -> Option<()> {
678 /// let content = SCShareableContent::get().ok()?;
679 /// let displays = content.displays();
680 /// let display = displays.first()?;
681 /// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
682 /// let config = SCScreenshotConfiguration::new()
683 /// .with_width(1920)
684 /// .with_height(1080)
685 /// .with_dynamic_range(SCScreenshotDynamicRange::BothSDRAndHDR);
686 ///
687 /// let output = SCScreenshotManager::capture_screenshot(&filter, &config).ok()?;
688 /// if let Some(sdr) = output.sdr_image() {
689 /// println!("SDR image: {}x{}", sdr.width(), sdr.height());
690 /// }
691 /// Some(())
692 /// }
693 /// ```
694 #[cfg(feature = "macos_26_0")]
695 pub fn capture_screenshot(
696 content_filter: &SCContentFilter,
697 configuration: &SCScreenshotConfiguration,
698 ) -> Result<SCScreenshotOutput, SCError> {
699 let (completion, context) = SyncCompletion::<SCScreenshotOutput>::new();
700
701 unsafe {
702 crate::ffi::sc_screenshot_manager_capture_screenshot(
703 content_filter.as_ptr(),
704 configuration.as_ptr(),
705 screenshot_output_callback,
706 context,
707 );
708 }
709
710 completion.wait().map_err(SCError::ScreenshotError)
711 }
712
713 /// Capture a screenshot of a specific region with advanced configuration (macOS 26.0+)
714 ///
715 /// # Arguments
716 /// * `rect` - The rectangle to capture, in screen coordinates (points)
717 /// * `configuration` - The screenshot configuration
718 ///
719 /// # Errors
720 /// Returns an error if the capture fails
721 #[cfg(feature = "macos_26_0")]
722 pub fn capture_screenshot_in_rect(
723 rect: crate::cg::CGRect,
724 configuration: &SCScreenshotConfiguration,
725 ) -> Result<SCScreenshotOutput, SCError> {
726 let (completion, context) = SyncCompletion::<SCScreenshotOutput>::new();
727
728 unsafe {
729 crate::ffi::sc_screenshot_manager_capture_screenshot_in_rect(
730 rect.origin.x,
731 rect.origin.y,
732 rect.size.width,
733 rect.size.height,
734 configuration.as_ptr(),
735 screenshot_output_callback,
736 context,
737 );
738 }
739
740 completion.wait().map_err(SCError::ScreenshotError)
741 }
742}
743
744// ============================================================================
745// SCScreenshotConfiguration (macOS 26.0+)
746// ============================================================================
747
748/// Display intent for screenshot rendering (macOS 26.0+)
749#[cfg(feature = "macos_26_0")]
750#[repr(i32)]
751#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
752pub enum SCScreenshotDisplayIntent {
753 /// Render on the canonical display
754 #[default]
755 Canonical = 0,
756 /// Render on the local display
757 Local = 1,
758}
759
760/// Dynamic range for screenshot output (macOS 26.0+)
761#[cfg(feature = "macos_26_0")]
762#[repr(i32)]
763#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
764pub enum SCScreenshotDynamicRange {
765 /// SDR output only
766 #[default]
767 SDR = 0,
768 /// HDR output only
769 HDR = 1,
770 /// Both SDR and HDR output
771 BothSDRAndHDR = 2,
772}
773
774/// Configuration for advanced screenshot capture (macOS 26.0+)
775///
776/// Provides fine-grained control over screenshot output including:
777/// - Output dimensions
778/// - Source and destination rectangles
779/// - Shadow and clipping behavior
780/// - HDR/SDR dynamic range
781/// - File output
782///
783/// # Examples
784///
785/// ```no_run
786/// use screencapturekit::screenshot_manager::{SCScreenshotConfiguration, SCScreenshotDynamicRange};
787///
788/// let config = SCScreenshotConfiguration::new()
789/// .with_width(1920)
790/// .with_height(1080)
791/// .with_shows_cursor(true)
792/// .with_dynamic_range(SCScreenshotDynamicRange::BothSDRAndHDR);
793/// ```
794#[cfg(feature = "macos_26_0")]
795pub struct SCScreenshotConfiguration {
796 ptr: *const c_void,
797}
798
799#[cfg(feature = "macos_26_0")]
800impl SCScreenshotConfiguration {
801 /// Create a new screenshot configuration
802 ///
803 /// # Panics
804 /// Panics if the configuration cannot be created (requires macOS 26.0+)
805 #[must_use]
806 pub fn new() -> Self {
807 let ptr = unsafe { crate::ffi::sc_screenshot_configuration_create() };
808 assert!(!ptr.is_null(), "Failed to create SCScreenshotConfiguration");
809 Self { ptr }
810 }
811
812 /// Set the output width in pixels
813 #[must_use]
814 #[allow(clippy::cast_possible_wrap)]
815 pub fn with_width(self, width: usize) -> Self {
816 unsafe {
817 crate::ffi::sc_screenshot_configuration_set_width(self.ptr, width as isize);
818 }
819 self
820 }
821
822 /// Set the output height in pixels
823 #[must_use]
824 #[allow(clippy::cast_possible_wrap)]
825 pub fn with_height(self, height: usize) -> Self {
826 unsafe {
827 crate::ffi::sc_screenshot_configuration_set_height(self.ptr, height as isize);
828 }
829 self
830 }
831
832 /// Set whether to show the cursor
833 #[must_use]
834 pub fn with_shows_cursor(self, shows_cursor: bool) -> Self {
835 unsafe {
836 crate::ffi::sc_screenshot_configuration_set_shows_cursor(self.ptr, shows_cursor);
837 }
838 self
839 }
840
841 /// Set the source rectangle (subset of capture area)
842 #[must_use]
843 pub fn with_source_rect(self, rect: crate::cg::CGRect) -> Self {
844 unsafe {
845 crate::ffi::sc_screenshot_configuration_set_source_rect(
846 self.ptr,
847 rect.origin.x,
848 rect.origin.y,
849 rect.size.width,
850 rect.size.height,
851 );
852 }
853 self
854 }
855
856 /// Set the destination rectangle (output area)
857 #[must_use]
858 pub fn with_destination_rect(self, rect: crate::cg::CGRect) -> Self {
859 unsafe {
860 crate::ffi::sc_screenshot_configuration_set_destination_rect(
861 self.ptr,
862 rect.origin.x,
863 rect.origin.y,
864 rect.size.width,
865 rect.size.height,
866 );
867 }
868 self
869 }
870
871 /// Set whether to ignore shadows
872 #[must_use]
873 pub fn with_ignore_shadows(self, ignore_shadows: bool) -> Self {
874 unsafe {
875 crate::ffi::sc_screenshot_configuration_set_ignore_shadows(self.ptr, ignore_shadows);
876 }
877 self
878 }
879
880 /// Set whether to ignore clipping
881 #[must_use]
882 pub fn with_ignore_clipping(self, ignore_clipping: bool) -> Self {
883 unsafe {
884 crate::ffi::sc_screenshot_configuration_set_ignore_clipping(self.ptr, ignore_clipping);
885 }
886 self
887 }
888
889 /// Set whether to include child windows
890 #[must_use]
891 pub fn with_include_child_windows(self, include_child_windows: bool) -> Self {
892 unsafe {
893 crate::ffi::sc_screenshot_configuration_set_include_child_windows(
894 self.ptr,
895 include_child_windows,
896 );
897 }
898 self
899 }
900
901 /// Set the display intent
902 #[must_use]
903 pub fn with_display_intent(self, display_intent: SCScreenshotDisplayIntent) -> Self {
904 unsafe {
905 crate::ffi::sc_screenshot_configuration_set_display_intent(
906 self.ptr,
907 display_intent as i32,
908 );
909 }
910 self
911 }
912
913 /// Set the dynamic range
914 #[must_use]
915 pub fn with_dynamic_range(self, dynamic_range: SCScreenshotDynamicRange) -> Self {
916 unsafe {
917 crate::ffi::sc_screenshot_configuration_set_dynamic_range(
918 self.ptr,
919 dynamic_range as i32,
920 );
921 }
922 self
923 }
924
925 /// Set the output file URL
926 ///
927 /// If `path` contains an interior NUL byte it cannot be converted to a C
928 /// string and the call is silently ignored (the configuration is left
929 /// unchanged). Valid file paths never contain NUL bytes.
930 #[must_use]
931 pub fn with_file_path(self, path: &str) -> Self {
932 if let Ok(c_path) = std::ffi::CString::new(path) {
933 unsafe {
934 crate::ffi::sc_screenshot_configuration_set_file_url(self.ptr, c_path.as_ptr());
935 }
936 }
937 self
938 }
939
940 /// Set the content type (output format) using `UTType` identifier
941 ///
942 /// Common identifiers include:
943 /// - `"public.png"` - PNG format
944 /// - `"public.jpeg"` - JPEG format
945 /// - `"public.heic"` - HEIC format
946 /// - `"public.tiff"` - TIFF format
947 ///
948 /// Use [`supported_content_types()`](Self::supported_content_types) to get
949 /// available formats.
950 ///
951 /// If `identifier` contains an interior NUL byte it cannot be converted to a
952 /// C string and the call is silently ignored (the configuration is left
953 /// unchanged). Valid `UTType` identifiers never contain NUL bytes.
954 #[must_use]
955 pub fn with_content_type(self, identifier: &str) -> Self {
956 if let Ok(c_id) = std::ffi::CString::new(identifier) {
957 unsafe {
958 crate::ffi::sc_screenshot_configuration_set_content_type(self.ptr, c_id.as_ptr());
959 }
960 }
961 self
962 }
963
964 /// Get the current content type as `UTType` identifier
965 pub fn content_type(&self) -> Option<String> {
966 let mut buffer = vec![0i8; 256];
967 let success = unsafe {
968 crate::ffi::sc_screenshot_configuration_get_content_type(
969 self.ptr,
970 buffer.as_mut_ptr(),
971 buffer.len(),
972 )
973 };
974 if success {
975 let c_str = unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) };
976 c_str.to_str().ok().map(ToString::to_string)
977 } else {
978 None
979 }
980 }
981
982 /// Get the list of supported content types (`UTType` identifiers)
983 ///
984 /// Returns a list of `UTType` identifiers that can be used with
985 /// [`with_content_type()`](Self::with_content_type).
986 ///
987 /// Common types include:
988 /// - `"public.png"` - PNG format
989 /// - `"public.jpeg"` - JPEG format
990 /// - `"public.heic"` - HEIC format
991 pub fn supported_content_types() -> Vec<String> {
992 let count =
993 unsafe { crate::ffi::sc_screenshot_configuration_get_supported_content_types_count() };
994 let mut result = Vec::with_capacity(count);
995 for i in 0..count {
996 let mut buffer = vec![0i8; 256];
997 let success = unsafe {
998 crate::ffi::sc_screenshot_configuration_get_supported_content_type_at(
999 i,
1000 buffer.as_mut_ptr(),
1001 buffer.len(),
1002 )
1003 };
1004 if success {
1005 let c_str = unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) };
1006 if let Ok(s) = c_str.to_str() {
1007 result.push(s.to_string());
1008 }
1009 }
1010 }
1011 result
1012 }
1013
1014 #[must_use]
1015 pub const fn as_ptr(&self) -> *const c_void {
1016 self.ptr
1017 }
1018}
1019
1020#[cfg(feature = "macos_26_0")]
1021impl std::fmt::Debug for SCScreenshotConfiguration {
1022 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1023 f.debug_struct("SCScreenshotConfiguration")
1024 .field("content_type", &self.content_type())
1025 .finish_non_exhaustive()
1026 }
1027}
1028
1029#[cfg(feature = "macos_26_0")]
1030impl Default for SCScreenshotConfiguration {
1031 fn default() -> Self {
1032 Self::new()
1033 }
1034}
1035
1036#[cfg(feature = "macos_26_0")]
1037crate::utils::retained::sc_retained!(
1038 SCScreenshotConfiguration,
1039 field = ptr,
1040 release = crate::ffi::sc_screenshot_configuration_release,
1041);
1042
1043// SAFETY: `SCScreenshotConfiguration` wraps an Objective-C ScreenCaptureKit
1044// object whose reference counting is atomic; it is safe to send between and
1045// share across threads.
1046#[cfg(feature = "macos_26_0")]
1047unsafe impl Send for SCScreenshotConfiguration {}
1048#[cfg(feature = "macos_26_0")]
1049unsafe impl Sync for SCScreenshotConfiguration {}
1050
1051// ============================================================================
1052// SCScreenshotOutput (macOS 26.0+)
1053// ============================================================================
1054
1055/// Output from advanced screenshot capture (macOS 26.0+)
1056///
1057/// Contains SDR and/or HDR images depending on the configuration,
1058/// and optionally the file URL where the image was saved.
1059#[cfg(feature = "macos_26_0")]
1060pub struct SCScreenshotOutput {
1061 ptr: *const c_void,
1062}
1063
1064#[cfg(feature = "macos_26_0")]
1065impl SCScreenshotOutput {
1066 pub(crate) fn from_ptr(ptr: *const c_void) -> Self {
1067 Self { ptr }
1068 }
1069
1070 /// Get the SDR image if available
1071 #[must_use]
1072 pub fn sdr_image(&self) -> Option<CGImage> {
1073 let ptr = unsafe { crate::ffi::sc_screenshot_output_get_sdr_image(self.ptr) };
1074 if ptr.is_null() {
1075 None
1076 } else {
1077 Some(unsafe { cgimage_from_retained_ptr(ptr) })
1078 }
1079 }
1080
1081 /// Get the HDR image if available
1082 #[must_use]
1083 pub fn hdr_image(&self) -> Option<CGImage> {
1084 let ptr = unsafe { crate::ffi::sc_screenshot_output_get_hdr_image(self.ptr) };
1085 if ptr.is_null() {
1086 None
1087 } else {
1088 Some(unsafe { cgimage_from_retained_ptr(ptr) })
1089 }
1090 }
1091
1092 /// Get the file URL where the image was saved, if applicable
1093 #[must_use]
1094 pub fn file_url(&self) -> Option<String> {
1095 unsafe {
1096 crate::utils::ffi_string::ffi_string_from_buffer(4096, |buffer, len| {
1097 crate::ffi::sc_screenshot_output_get_file_url(self.ptr, buffer, len)
1098 })
1099 }
1100 }
1101}
1102
1103#[cfg(feature = "macos_26_0")]
1104impl std::fmt::Debug for SCScreenshotOutput {
1105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1106 f.debug_struct("SCScreenshotOutput")
1107 .field(
1108 "sdr_image",
1109 &self.sdr_image().map(|i| (i.width(), i.height())),
1110 )
1111 .field(
1112 "hdr_image",
1113 &self.hdr_image().map(|i| (i.width(), i.height())),
1114 )
1115 .field("file_url", &self.file_url())
1116 .finish()
1117 }
1118}
1119
1120#[cfg(feature = "macos_26_0")]
1121crate::utils::retained::sc_retained!(
1122 SCScreenshotOutput,
1123 field = ptr,
1124 release = crate::ffi::sc_screenshot_output_release,
1125);
1126
1127// SAFETY: `SCScreenshotOutput` wraps an immutable Objective-C ScreenCaptureKit
1128// object whose reference counting is atomic; it is safe to send between and
1129// share across threads.
1130#[cfg(feature = "macos_26_0")]
1131unsafe impl Send for SCScreenshotOutput {}
1132#[cfg(feature = "macos_26_0")]
1133unsafe impl Sync for SCScreenshotOutput {}