Skip to main content

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 {}