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