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