import ScreenCaptureKit import CoreGraphics import ImageIO import UniformTypeIdentifiers import AppKit // NSScreen only, to read backingScaleFactor as a fallback import Foundation public struct CapturedImage: Sendable { public let pngData: Data public let pixelWidth: Int public let pixelHeight: Int public let scale: CGFloat } public struct ScreenCapturer: Sendable { public init() {} /// Does NOT prompt. Reports the current Screen Recording grant. public static var isScreenRecordingGranted: Bool { CGPreflightScreenCaptureAccess() } /// Captures `region` at the display's true backing scale. Throws /// `.screenRecordingNotGranted` when the grant is absent — never requests it. public func capture(_ region: CaptureRegion) async throws -> CapturedImage { guard Self.isScreenRecordingGranted else { throw ShotdeckError.screenRecordingNotGranted } let content: SCShareableContent do { content = try await SCShareableContent.excludingDesktopWindows( false, onScreenWindowsOnly: true ) } catch { throw ShotdeckError.captureFailed(underlying: error.localizedDescription) } guard let display = content.displays.first(where: { $0.displayID == region.displayID }) else { throw ShotdeckError.displayNoLongerConnected(displayID: region.displayID) } // Bounds/clamp: a resolution change often shrinks the display bounds by a few // points without the region Ben picked being meaningfully wrong; 80% keeps a real // recapture usable while a smaller overlap (e.g. a genuinely different display // swapped in) still throws. let rect: CGRect if display.frame.contains(region.rect) { rect = region.rect } else { let overlap = display.frame.intersection(region.rect) let originalArea = region.rect.width * region.rect.height let overlapArea = overlap.width * overlap.height if originalArea > 0 && overlapArea / originalArea >= 0.8 { rect = overlap } else { throw ShotdeckError.displayNoLongerConnected(displayID: region.displayID) } } let localRect = CGRect( x: rect.minX - display.frame.minX, y: rect.minY - display.frame.minY, width: rect.width, height: rect.height ) guard let mode = CGDisplayCopyDisplayMode(region.displayID) else { throw ShotdeckError.captureFailed( underlying: "no display mode for displayID \(region.displayID)" ) } guard mode.width > 0 else { throw ShotdeckError.captureFailed( underlying: "display mode width is 0 for displayID \(region.displayID)" ) } let scale = CGFloat(mode.pixelWidth) / CGFloat(mode.width) let filter = SCContentFilter(display: display, excludingWindows: []) let configuration = SCStreamConfiguration() configuration.sourceRect = localRect configuration.width = Int((localRect.width * scale).rounded()) configuration.height = Int((localRect.height * scale).rounded()) configuration.scalesToFit = false configuration.showsCursor = false let cgImage: CGImage do { cgImage = try await SCScreenshotManager.captureImage( contentFilter: filter, configuration: configuration ) } catch { throw ShotdeckError.captureFailed(underlying: error.localizedDescription) } let data = NSMutableData() guard let dest = CGImageDestinationCreateWithData( data, UTType.png.identifier as CFString, 1, nil ) else { throw ShotdeckError.captureFailed(underlying: "could not create PNG image destination") } CGImageDestinationAddImage(dest, cgImage, nil) guard CGImageDestinationFinalize(dest) else { throw ShotdeckError.captureFailed(underlying: "PNG encoding failed to finalize") } return CapturedImage( pngData: data as Data, pixelWidth: cgImage.width, pixelHeight: cgImage.height, scale: scale ) } }