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