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
203impl CGImage {
204    pub(crate) fn from_ptr(ptr: *const c_void) -> Self {
205        Self { ptr }
206    }
207
208    /// Get image width in pixels
209    ///
210    /// # Examples
211    ///
212    /// ```no_run
213    /// # use screencapturekit::screenshot_manager::SCScreenshotManager;
214    /// # use screencapturekit::stream::{content_filter::SCContentFilter, configuration::SCStreamConfiguration};
215    /// # use screencapturekit::shareable_content::SCShareableContent;
216    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
217    /// # let content = SCShareableContent::get()?;
218    /// # let display = &content.displays()[0];
219    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
220    /// # let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
221    /// let image = SCScreenshotManager::capture_image(&filter, &config)?;
222    /// let width = image.width();
223    /// println!("Width: {}", width);
224    /// # Ok(())
225    /// # }
226    /// ```
227    #[must_use]
228    pub fn width(&self) -> usize {
229        unsafe { crate::ffi::cgimage_get_width(self.ptr) }
230    }
231
232    /// Get image height in pixels
233    #[must_use]
234    pub fn height(&self) -> usize {
235        unsafe { crate::ffi::cgimage_get_height(self.ptr) }
236    }
237
238    #[must_use]
239    pub fn as_ptr(&self) -> *const c_void {
240        self.ptr
241    }
242
243    /// Get raw RGBA pixel data
244    ///
245    /// Returns a vector containing RGBA bytes (4 bytes per pixel).
246    /// The data is in row-major order.
247    ///
248    /// # Errors
249    /// Returns an error if the pixel data cannot be extracted
250    pub fn rgba_data(&self) -> Result<Vec<u8>, SCError> {
251        let mut data_ptr: *const u8 = std::ptr::null();
252        let mut data_length: usize = 0;
253
254        let success = unsafe {
255            crate::ffi::cgimage_get_data(
256                self.ptr,
257                std::ptr::addr_of_mut!(data_ptr),
258                std::ptr::addr_of_mut!(data_length),
259            )
260        };
261
262        if !success || data_ptr.is_null() {
263            return Err(SCError::internal_error(
264                "Failed to extract pixel data from CGImage",
265            ));
266        }
267
268        // Copy the data into a Vec
269        let data = unsafe { std::slice::from_raw_parts(data_ptr, data_length).to_vec() };
270
271        // Free the allocated data
272        unsafe {
273            crate::ffi::cgimage_free_data(data_ptr.cast_mut());
274        }
275
276        Ok(data)
277    }
278
279    /// Save the image to a PNG file
280    ///
281    /// # Arguments
282    /// * `path` - The file path to save the PNG to
283    ///
284    /// # Errors
285    /// Returns an error if the image cannot be saved
286    ///
287    /// # Examples
288    ///
289    /// ```no_run
290    /// # use screencapturekit::screenshot_manager::SCScreenshotManager;
291    /// # use screencapturekit::stream::{content_filter::SCContentFilter, configuration::SCStreamConfiguration};
292    /// # use screencapturekit::shareable_content::SCShareableContent;
293    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
294    /// # let content = SCShareableContent::get()?;
295    /// # let display = &content.displays()[0];
296    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
297    /// # let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
298    /// let image = SCScreenshotManager::capture_image(&filter, &config)?;
299    /// image.save_png("/tmp/screenshot.png")?;
300    /// # Ok(())
301    /// # }
302    /// ```
303    pub fn save_png(&self, path: &str) -> Result<(), SCError> {
304        self.save(path, ImageFormat::Png)
305    }
306
307    /// Save the image to a file in the specified format
308    ///
309    /// # Arguments
310    /// * `path` - The file path to save the image to
311    /// * `format` - The output format (PNG, JPEG, TIFF, GIF, BMP, or HEIC)
312    ///
313    /// # Errors
314    /// Returns an error if the image cannot be saved
315    ///
316    /// # Examples
317    ///
318    /// ```no_run
319    /// # use screencapturekit::screenshot_manager::{SCScreenshotManager, ImageFormat};
320    /// # use screencapturekit::stream::{content_filter::SCContentFilter, configuration::SCStreamConfiguration};
321    /// # use screencapturekit::shareable_content::SCShareableContent;
322    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
323    /// # let content = SCShareableContent::get()?;
324    /// # let display = &content.displays()[0];
325    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
326    /// # let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
327    /// let image = SCScreenshotManager::capture_image(&filter, &config)?;
328    ///
329    /// // Save as PNG (lossless)
330    /// image.save("/tmp/screenshot.png", ImageFormat::Png)?;
331    ///
332    /// // Save as JPEG with 85% quality
333    /// image.save("/tmp/screenshot.jpg", ImageFormat::Jpeg(0.85))?;
334    ///
335    /// // Save as HEIC with 90% quality (smaller file size)
336    /// image.save("/tmp/screenshot.heic", ImageFormat::Heic(0.9))?;
337    /// # Ok(())
338    /// # }
339    /// ```
340    pub fn save(&self, path: &str, format: ImageFormat) -> Result<(), SCError> {
341        let c_path = std::ffi::CString::new(path)
342            .map_err(|_| SCError::internal_error("Path contains null bytes"))?;
343
344        let success = unsafe {
345            crate::ffi::cgimage_save_to_file(
346                self.ptr,
347                c_path.as_ptr(),
348                format.to_format_id(),
349                format.quality(),
350            )
351        };
352
353        if success {
354            Ok(())
355        } else {
356            Err(SCError::internal_error(format!(
357                "Failed to save image as {}",
358                format.extension().to_uppercase()
359            )))
360        }
361    }
362}
363
364impl Drop for CGImage {
365    fn drop(&mut self) {
366        if !self.ptr.is_null() {
367            unsafe {
368                crate::ffi::cgimage_release(self.ptr);
369            }
370        }
371    }
372}
373
374impl std::fmt::Debug for CGImage {
375    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376        f.debug_struct("CGImage")
377            .field("width", &self.width())
378            .field("height", &self.height())
379            .finish()
380    }
381}
382
383unsafe impl Send for CGImage {}
384unsafe impl Sync for CGImage {}
385
386/// Manager for capturing single screenshots
387///
388/// Available on macOS 14.0+. Provides a simpler API than `SCStream` for one-time captures.
389///
390/// # Examples
391///
392/// ```no_run
393/// use screencapturekit::screenshot_manager::SCScreenshotManager;
394/// use screencapturekit::stream::{content_filter::SCContentFilter, configuration::SCStreamConfiguration};
395/// use screencapturekit::shareable_content::SCShareableContent;
396///
397/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
398/// let content = SCShareableContent::get()?;
399/// let display = &content.displays()[0];
400/// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
401/// let config = SCStreamConfiguration::new()
402///     .with_width(1920)
403///     .with_height(1080);
404///
405/// let image = SCScreenshotManager::capture_image(&filter, &config)?;
406/// println!("Captured screenshot: {}x{}", image.width(), image.height());
407/// # Ok(())
408/// # }
409/// ```
410pub struct SCScreenshotManager;
411
412impl SCScreenshotManager {
413    /// Capture a single screenshot as a `CGImage`
414    ///
415    /// # Errors
416    /// Returns an error if:
417    /// - The system is not macOS 14.0+
418    /// - Screen recording permission is not granted
419    /// - The capture fails for any reason
420    ///
421    /// # Panics
422    /// Panics if the internal mutex is poisoned.
423    pub fn capture_image(
424        content_filter: &SCContentFilter,
425        configuration: &SCStreamConfiguration,
426    ) -> Result<CGImage, SCError> {
427        let (completion, context) = SyncCompletion::<CGImage>::new();
428
429        unsafe {
430            crate::ffi::sc_screenshot_manager_capture_image(
431                content_filter.as_ptr(),
432                configuration.as_ptr(),
433                image_callback,
434                context,
435            );
436        }
437
438        completion.wait().map_err(SCError::ScreenshotError)
439    }
440
441    /// Capture a single screenshot as a `CMSampleBuffer`
442    ///
443    /// Returns the sample buffer for advanced processing.
444    ///
445    /// # Errors
446    /// Returns an error if:
447    /// - The system is not macOS 14.0+
448    /// - Screen recording permission is not granted
449    /// - The capture fails for any reason
450    ///
451    /// # Panics
452    /// Panics if the internal mutex is poisoned.
453    pub fn capture_sample_buffer(
454        content_filter: &SCContentFilter,
455        configuration: &SCStreamConfiguration,
456    ) -> Result<crate::cm::CMSampleBuffer, SCError> {
457        let (completion, context) = SyncCompletion::<crate::cm::CMSampleBuffer>::new();
458
459        unsafe {
460            crate::ffi::sc_screenshot_manager_capture_sample_buffer(
461                content_filter.as_ptr(),
462                configuration.as_ptr(),
463                buffer_callback,
464                context,
465            );
466        }
467
468        completion.wait().map_err(SCError::ScreenshotError)
469    }
470
471    /// Capture a screenshot of a specific screen region (macOS 15.2+)
472    ///
473    /// This method captures the content within the specified rectangle,
474    /// which can span multiple displays.
475    ///
476    /// # Arguments
477    /// * `rect` - The rectangle to capture, in screen coordinates (points)
478    ///
479    /// # Errors
480    /// Returns an error if:
481    /// - The system is not macOS 15.2+
482    /// - Screen recording permission is not granted
483    /// - The capture fails for any reason
484    ///
485    /// # Examples
486    /// ```no_run
487    /// use screencapturekit::screenshot_manager::SCScreenshotManager;
488    /// use screencapturekit::cg::CGRect;
489    ///
490    /// fn example() -> Result<(), screencapturekit::utils::error::SCError> {
491    ///     let rect = CGRect::new(0.0, 0.0, 1920.0, 1080.0);
492    ///     let image = SCScreenshotManager::capture_image_in_rect(rect)?;
493    ///     Ok(())
494    /// }
495    /// ```
496    #[cfg(feature = "macos_15_2")]
497    pub fn capture_image_in_rect(rect: CGRect) -> Result<CGImage, SCError> {
498        let (completion, context) = SyncCompletion::<CGImage>::new();
499
500        unsafe {
501            crate::ffi::sc_screenshot_manager_capture_image_in_rect(
502                rect.x,
503                rect.y,
504                rect.width,
505                rect.height,
506                image_callback,
507                context,
508            );
509        }
510
511        completion.wait().map_err(SCError::ScreenshotError)
512    }
513
514    /// Capture a screenshot with advanced configuration (macOS 26.0+)
515    ///
516    /// This method uses the new `SCScreenshotConfiguration` for more control
517    /// over the screenshot output, including HDR support and file saving.
518    ///
519    /// # Arguments
520    /// * `content_filter` - The content filter specifying what to capture
521    /// * `configuration` - The screenshot configuration
522    ///
523    /// # Errors
524    /// Returns an error if the capture fails
525    ///
526    /// # Examples
527    /// ```no_run
528    /// use screencapturekit::screenshot_manager::{SCScreenshotManager, SCScreenshotConfiguration, SCScreenshotDynamicRange};
529    /// use screencapturekit::stream::content_filter::SCContentFilter;
530    /// use screencapturekit::shareable_content::SCShareableContent;
531    ///
532    /// fn example() -> Option<()> {
533    ///     let content = SCShareableContent::get().ok()?;
534    ///     let displays = content.displays();
535    ///     let display = displays.first()?;
536    ///     let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
537    ///     let config = SCScreenshotConfiguration::new()
538    ///         .with_width(1920)
539    ///         .with_height(1080)
540    ///         .with_dynamic_range(SCScreenshotDynamicRange::BothSDRAndHDR);
541    ///
542    ///     let output = SCScreenshotManager::capture_screenshot(&filter, &config).ok()?;
543    ///     if let Some(sdr) = output.sdr_image() {
544    ///         println!("SDR image: {}x{}", sdr.width(), sdr.height());
545    ///     }
546    ///     Some(())
547    /// }
548    /// ```
549    #[cfg(feature = "macos_26_0")]
550    pub fn capture_screenshot(
551        content_filter: &SCContentFilter,
552        configuration: &SCScreenshotConfiguration,
553    ) -> Result<SCScreenshotOutput, SCError> {
554        let (completion, context) = SyncCompletion::<SCScreenshotOutput>::new();
555
556        unsafe {
557            crate::ffi::sc_screenshot_manager_capture_screenshot(
558                content_filter.as_ptr(),
559                configuration.as_ptr(),
560                screenshot_output_callback,
561                context,
562            );
563        }
564
565        completion.wait().map_err(SCError::ScreenshotError)
566    }
567
568    /// Capture a screenshot of a specific region with advanced configuration (macOS 26.0+)
569    ///
570    /// # Arguments
571    /// * `rect` - The rectangle to capture, in screen coordinates (points)
572    /// * `configuration` - The screenshot configuration
573    ///
574    /// # Errors
575    /// Returns an error if the capture fails
576    #[cfg(feature = "macos_26_0")]
577    pub fn capture_screenshot_in_rect(
578        rect: crate::cg::CGRect,
579        configuration: &SCScreenshotConfiguration,
580    ) -> Result<SCScreenshotOutput, SCError> {
581        let (completion, context) = SyncCompletion::<SCScreenshotOutput>::new();
582
583        unsafe {
584            crate::ffi::sc_screenshot_manager_capture_screenshot_in_rect(
585                rect.x,
586                rect.y,
587                rect.width,
588                rect.height,
589                configuration.as_ptr(),
590                screenshot_output_callback,
591                context,
592            );
593        }
594
595        completion.wait().map_err(SCError::ScreenshotError)
596    }
597}
598
599// ============================================================================
600// SCScreenshotConfiguration (macOS 26.0+)
601// ============================================================================
602
603/// Display intent for screenshot rendering (macOS 26.0+)
604#[cfg(feature = "macos_26_0")]
605#[repr(i32)]
606#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
607pub enum SCScreenshotDisplayIntent {
608    /// Render on the canonical display
609    #[default]
610    Canonical = 0,
611    /// Render on the local display
612    Local = 1,
613}
614
615/// Dynamic range for screenshot output (macOS 26.0+)
616#[cfg(feature = "macos_26_0")]
617#[repr(i32)]
618#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
619pub enum SCScreenshotDynamicRange {
620    /// SDR output only
621    #[default]
622    SDR = 0,
623    /// HDR output only
624    HDR = 1,
625    /// Both SDR and HDR output
626    BothSDRAndHDR = 2,
627}
628
629/// Configuration for advanced screenshot capture (macOS 26.0+)
630///
631/// Provides fine-grained control over screenshot output including:
632/// - Output dimensions
633/// - Source and destination rectangles
634/// - Shadow and clipping behavior
635/// - HDR/SDR dynamic range
636/// - File output
637///
638/// # Examples
639///
640/// ```no_run
641/// use screencapturekit::screenshot_manager::{SCScreenshotConfiguration, SCScreenshotDynamicRange};
642///
643/// let config = SCScreenshotConfiguration::new()
644///     .with_width(1920)
645///     .with_height(1080)
646///     .with_shows_cursor(true)
647///     .with_dynamic_range(SCScreenshotDynamicRange::BothSDRAndHDR);
648/// ```
649#[cfg(feature = "macos_26_0")]
650pub struct SCScreenshotConfiguration {
651    ptr: *const c_void,
652}
653
654#[cfg(feature = "macos_26_0")]
655impl SCScreenshotConfiguration {
656    /// Create a new screenshot configuration
657    ///
658    /// # Panics
659    /// Panics if the configuration cannot be created (requires macOS 26.0+)
660    #[must_use]
661    pub fn new() -> Self {
662        let ptr = unsafe { crate::ffi::sc_screenshot_configuration_create() };
663        assert!(!ptr.is_null(), "Failed to create SCScreenshotConfiguration");
664        Self { ptr }
665    }
666
667    /// Set the output width in pixels
668    #[must_use]
669    #[allow(clippy::cast_possible_wrap)]
670    pub fn with_width(self, width: usize) -> Self {
671        unsafe {
672            crate::ffi::sc_screenshot_configuration_set_width(self.ptr, width as isize);
673        }
674        self
675    }
676
677    /// Set the output height in pixels
678    #[must_use]
679    #[allow(clippy::cast_possible_wrap)]
680    pub fn with_height(self, height: usize) -> Self {
681        unsafe {
682            crate::ffi::sc_screenshot_configuration_set_height(self.ptr, height as isize);
683        }
684        self
685    }
686
687    /// Set whether to show the cursor
688    #[must_use]
689    pub fn with_shows_cursor(self, shows_cursor: bool) -> Self {
690        unsafe {
691            crate::ffi::sc_screenshot_configuration_set_shows_cursor(self.ptr, shows_cursor);
692        }
693        self
694    }
695
696    /// Set the source rectangle (subset of capture area)
697    #[must_use]
698    pub fn with_source_rect(self, rect: crate::cg::CGRect) -> Self {
699        unsafe {
700            crate::ffi::sc_screenshot_configuration_set_source_rect(
701                self.ptr,
702                rect.x,
703                rect.y,
704                rect.width,
705                rect.height,
706            );
707        }
708        self
709    }
710
711    /// Set the destination rectangle (output area)
712    #[must_use]
713    pub fn with_destination_rect(self, rect: crate::cg::CGRect) -> Self {
714        unsafe {
715            crate::ffi::sc_screenshot_configuration_set_destination_rect(
716                self.ptr,
717                rect.x,
718                rect.y,
719                rect.width,
720                rect.height,
721            );
722        }
723        self
724    }
725
726    /// Set whether to ignore shadows
727    #[must_use]
728    pub fn with_ignore_shadows(self, ignore_shadows: bool) -> Self {
729        unsafe {
730            crate::ffi::sc_screenshot_configuration_set_ignore_shadows(self.ptr, ignore_shadows);
731        }
732        self
733    }
734
735    /// Set whether to ignore clipping
736    #[must_use]
737    pub fn with_ignore_clipping(self, ignore_clipping: bool) -> Self {
738        unsafe {
739            crate::ffi::sc_screenshot_configuration_set_ignore_clipping(self.ptr, ignore_clipping);
740        }
741        self
742    }
743
744    /// Set whether to include child windows
745    #[must_use]
746    pub fn with_include_child_windows(self, include_child_windows: bool) -> Self {
747        unsafe {
748            crate::ffi::sc_screenshot_configuration_set_include_child_windows(
749                self.ptr,
750                include_child_windows,
751            );
752        }
753        self
754    }
755
756    /// Set the display intent
757    #[must_use]
758    pub fn with_display_intent(self, display_intent: SCScreenshotDisplayIntent) -> Self {
759        unsafe {
760            crate::ffi::sc_screenshot_configuration_set_display_intent(
761                self.ptr,
762                display_intent as i32,
763            );
764        }
765        self
766    }
767
768    /// Set the dynamic range
769    #[must_use]
770    pub fn with_dynamic_range(self, dynamic_range: SCScreenshotDynamicRange) -> Self {
771        unsafe {
772            crate::ffi::sc_screenshot_configuration_set_dynamic_range(
773                self.ptr,
774                dynamic_range as i32,
775            );
776        }
777        self
778    }
779
780    /// Set the output file URL
781    ///
782    /// # Panics
783    /// Panics if the path contains null bytes
784    #[must_use]
785    pub fn with_file_path(self, path: &str) -> Self {
786        let c_path = std::ffi::CString::new(path).expect("path should not contain null bytes");
787        unsafe {
788            crate::ffi::sc_screenshot_configuration_set_file_url(self.ptr, c_path.as_ptr());
789        }
790        self
791    }
792
793    /// Set the content type (output format) using `UTType` identifier
794    ///
795    /// Common identifiers include:
796    /// - `"public.png"` - PNG format
797    /// - `"public.jpeg"` - JPEG format
798    /// - `"public.heic"` - HEIC format
799    /// - `"public.tiff"` - TIFF format
800    ///
801    /// Use [`supported_content_types()`](Self::supported_content_types) to get
802    /// available formats.
803    ///
804    /// # Panics
805    /// Panics if the identifier contains null bytes
806    #[must_use]
807    pub fn with_content_type(self, identifier: &str) -> Self {
808        let c_id =
809            std::ffi::CString::new(identifier).expect("identifier should not contain null bytes");
810        unsafe {
811            crate::ffi::sc_screenshot_configuration_set_content_type(self.ptr, c_id.as_ptr());
812        }
813        self
814    }
815
816    /// Get the current content type as `UTType` identifier
817    pub fn content_type(&self) -> Option<String> {
818        let mut buffer = vec![0i8; 256];
819        let success = unsafe {
820            crate::ffi::sc_screenshot_configuration_get_content_type(
821                self.ptr,
822                buffer.as_mut_ptr(),
823                buffer.len(),
824            )
825        };
826        if success {
827            let c_str = unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) };
828            c_str.to_str().ok().map(ToString::to_string)
829        } else {
830            None
831        }
832    }
833
834    /// Get the list of supported content types (`UTType` identifiers)
835    ///
836    /// Returns a list of `UTType` identifiers that can be used with
837    /// [`with_content_type()`](Self::with_content_type).
838    ///
839    /// Common types include:
840    /// - `"public.png"` - PNG format
841    /// - `"public.jpeg"` - JPEG format
842    /// - `"public.heic"` - HEIC format
843    pub fn supported_content_types() -> Vec<String> {
844        let count =
845            unsafe { crate::ffi::sc_screenshot_configuration_get_supported_content_types_count() };
846        let mut result = Vec::with_capacity(count);
847        for i in 0..count {
848            let mut buffer = vec![0i8; 256];
849            let success = unsafe {
850                crate::ffi::sc_screenshot_configuration_get_supported_content_type_at(
851                    i,
852                    buffer.as_mut_ptr(),
853                    buffer.len(),
854                )
855            };
856            if success {
857                let c_str = unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) };
858                if let Ok(s) = c_str.to_str() {
859                    result.push(s.to_string());
860                }
861            }
862        }
863        result
864    }
865
866    #[must_use]
867    pub const fn as_ptr(&self) -> *const c_void {
868        self.ptr
869    }
870}
871
872#[cfg(feature = "macos_26_0")]
873impl std::fmt::Debug for SCScreenshotConfiguration {
874    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
875        f.debug_struct("SCScreenshotConfiguration")
876            .field("content_type", &self.content_type())
877            .finish_non_exhaustive()
878    }
879}
880
881#[cfg(feature = "macos_26_0")]
882impl Default for SCScreenshotConfiguration {
883    fn default() -> Self {
884        Self::new()
885    }
886}
887
888#[cfg(feature = "macos_26_0")]
889impl Drop for SCScreenshotConfiguration {
890    fn drop(&mut self) {
891        if !self.ptr.is_null() {
892            unsafe {
893                crate::ffi::sc_screenshot_configuration_release(self.ptr);
894            }
895        }
896    }
897}
898
899#[cfg(feature = "macos_26_0")]
900unsafe impl Send for SCScreenshotConfiguration {}
901#[cfg(feature = "macos_26_0")]
902unsafe impl Sync for SCScreenshotConfiguration {}
903
904// ============================================================================
905// SCScreenshotOutput (macOS 26.0+)
906// ============================================================================
907
908/// Output from advanced screenshot capture (macOS 26.0+)
909///
910/// Contains SDR and/or HDR images depending on the configuration,
911/// and optionally the file URL where the image was saved.
912#[cfg(feature = "macos_26_0")]
913pub struct SCScreenshotOutput {
914    ptr: *const c_void,
915}
916
917#[cfg(feature = "macos_26_0")]
918impl SCScreenshotOutput {
919    pub(crate) fn from_ptr(ptr: *const c_void) -> Self {
920        Self { ptr }
921    }
922
923    /// Get the SDR image if available
924    #[must_use]
925    pub fn sdr_image(&self) -> Option<CGImage> {
926        let ptr = unsafe { crate::ffi::sc_screenshot_output_get_sdr_image(self.ptr) };
927        if ptr.is_null() {
928            None
929        } else {
930            Some(CGImage::from_ptr(ptr))
931        }
932    }
933
934    /// Get the HDR image if available
935    #[must_use]
936    pub fn hdr_image(&self) -> Option<CGImage> {
937        let ptr = unsafe { crate::ffi::sc_screenshot_output_get_hdr_image(self.ptr) };
938        if ptr.is_null() {
939            None
940        } else {
941            Some(CGImage::from_ptr(ptr))
942        }
943    }
944
945    /// Get the file URL where the image was saved, if applicable
946    #[must_use]
947    #[allow(clippy::cast_possible_wrap)]
948    pub fn file_url(&self) -> Option<String> {
949        let mut buffer = vec![0i8; 4096];
950        let success = unsafe {
951            crate::ffi::sc_screenshot_output_get_file_url(
952                self.ptr,
953                buffer.as_mut_ptr(),
954                buffer.len() as isize,
955            )
956        };
957        if success {
958            let c_str = unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) };
959            c_str.to_str().ok().map(String::from)
960        } else {
961            None
962        }
963    }
964}
965
966#[cfg(feature = "macos_26_0")]
967impl std::fmt::Debug for SCScreenshotOutput {
968    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
969        f.debug_struct("SCScreenshotOutput")
970            .field(
971                "sdr_image",
972                &self.sdr_image().map(|i| (i.width(), i.height())),
973            )
974            .field(
975                "hdr_image",
976                &self.hdr_image().map(|i| (i.width(), i.height())),
977            )
978            .field("file_url", &self.file_url())
979            .finish()
980    }
981}
982
983#[cfg(feature = "macos_26_0")]
984impl Drop for SCScreenshotOutput {
985    fn drop(&mut self) {
986        if !self.ptr.is_null() {
987            unsafe {
988                crate::ffi::sc_screenshot_output_release(self.ptr);
989            }
990        }
991    }
992}
993
994#[cfg(feature = "macos_26_0")]
995unsafe impl Send for SCScreenshotOutput {}
996#[cfg(feature = "macos_26_0")]
997unsafe impl Sync for SCScreenshotOutput {}