Initial commit

This commit is contained in:
Alex E. Tafoya
2026-09-17 15:15:45 -05:00
commit 87fe7927c4
36 changed files with 5113 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
// CaptureEngine.swift
// GreenshotMac
//
// ScreenCaptureKit replacement for Greenshot's WindowCapture / GDI capture code.
import AppKit
import ScreenCaptureKit
/// A frozen snapshot of one display, used as the backdrop of the selection overlay.
struct DisplaySnapshot {
let screen: NSScreen
let display: SCDisplay
let image: CGImage
/// Display bounds in CoreGraphics global coordinates (top-left origin, points).
let cgFrame: CGRect
var scale: CGFloat { CGFloat(image.width) / cgFrame.width }
}
/// A window that can be picked in window capture mode.
struct WindowInfo {
let window: SCWindow
/// Window frame in CoreGraphics global coordinates (top-left origin, points).
var cgFrame: CGRect { window.frame }
var title: String { window.title ?? "" }
var applicationName: String { window.owningApplication?.applicationName ?? "" }
var bundleID: String? { window.owningApplication?.bundleIdentifier }
}
enum CaptureEngine {
// MARK: - Permission
@discardableResult
static func ensurePermission() -> Bool {
if CGPreflightScreenCaptureAccess() { return true }
CGRequestScreenCaptureAccess()
return CGPreflightScreenCaptureAccess()
}
// MARK: - Shareable content
static func shareableContent() async throws -> SCShareableContent {
do {
return try await SCShareableContent.excludingDesktopWindows(true, onScreenWindowsOnly: true)
} catch {
throw CaptureError.permissionDenied
}
}
/// Windows worth offering to the user: on screen, reasonably sized, not our own overlay.
static func selectableWindows(from content: SCShareableContent) -> [WindowInfo] {
let ownPID = ProcessInfo.processInfo.processIdentifier
return content.windows
.filter { window in
window.isOnScreen &&
window.frame.width > 40 && window.frame.height > 40 &&
window.owningApplication?.processID != ownPID
}
.map(WindowInfo.init)
}
// MARK: - Snapshots
/// Grabs every display so the selection overlay can show a frozen desktop.
static func snapshotAllDisplays() async throws -> [DisplaySnapshot] {
let content = try await shareableContent()
guard !content.displays.isEmpty else { throw CaptureError.noContent }
var snapshots: [DisplaySnapshot] = []
for display in content.displays {
guard let screen = ScreenGeometry.screen(for: display.displayID) else { continue }
let image = try await capture(display: display, content: content, sourceRect: nil, screen: screen)
snapshots.append(DisplaySnapshot(screen: screen,
display: display,
image: image,
cgFrame: CGRect(x: CGFloat(display.frame.origin.x),
y: CGFloat(display.frame.origin.y),
width: CGFloat(display.width),
height: CGFloat(display.height))))
}
guard !snapshots.isEmpty else { throw CaptureError.noContent }
return snapshots
}
private static func capture(display: SCDisplay,
content: SCShareableContent,
sourceRect: CGRect?,
screen: NSScreen) async throws -> CGImage {
let configuration = SCStreamConfiguration()
let scale = screen.backingScaleFactor
let rect = sourceRect ?? CGRect(x: 0, y: 0, width: display.width, height: display.height)
configuration.sourceRect = rect
configuration.width = Int((rect.width * scale).rounded())
configuration.height = Int((rect.height * scale).rounded())
configuration.scalesToFit = false
configuration.showsCursor = Configuration.shared.captureMousepointer
configuration.captureResolution = .best
if #available(macOS 14.0, *) {
configuration.ignoreShadowsSingleWindow = true
}
let ownWindows = content.windows.filter {
$0.owningApplication?.processID == ProcessInfo.processInfo.processIdentifier
}
let filter = SCContentFilter(display: display, excludingWindows: ownWindows)
do {
return try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration)
} catch {
throw CaptureError.captureFailed(error.localizedDescription)
}
}
// MARK: - Public capture entry points
/// Full screen capture of the display the mouse is on, or of a given screen.
static func captureFullscreen(screen requested: NSScreen? = nil) async throws -> Capture {
let content = try await shareableContent()
let mouseLocation = NSEvent.mouseLocation
let screen = requested ?? ScreenGeometry.screen(containing: mouseLocation) ?? NSScreen.main
guard let screen,
let displayID = ScreenGeometry.displayID(for: screen),
let display = content.displays.first(where: { $0.displayID == displayID })
else { throw CaptureError.noContent }
let image = try await capture(display: display, content: content, sourceRect: nil, screen: screen)
var details = CaptureDetails()
details.captureMode = .fullscreen
details.title = screen.localizedName
details.scaleFactor = screen.backingScaleFactor
details.screenBounds = CGRect(x: CGFloat(display.frame.origin.x),
y: CGFloat(display.frame.origin.y),
width: CGFloat(display.width),
height: CGFloat(display.height))
return Capture(image: image, details: details)
}
/// Captures one window on its own, without whatever overlaps it.
static func captureWindow(_ info: WindowInfo) async throws -> Capture {
let configuration = SCStreamConfiguration()
let screen = ScreenGeometry.screen(containing: ScreenGeometry.appKitRect(fromCG: info.cgFrame).center)
?? NSScreen.main
let scale = screen?.backingScaleFactor ?? 2
configuration.width = Int((info.cgFrame.width * scale).rounded())
configuration.height = Int((info.cgFrame.height * scale).rounded())
configuration.showsCursor = false
configuration.scalesToFit = false
configuration.captureResolution = .best
if #available(macOS 14.0, *) {
configuration.ignoreShadowsSingleWindow = Configuration.shared.windowCaptureMode != .windowWithShadow
}
let filter = SCContentFilter(desktopIndependentWindow: info.window)
let image: CGImage
do {
image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration)
} catch {
throw CaptureError.captureFailed(error.localizedDescription)
}
var details = CaptureDetails()
details.captureMode = .window
details.title = info.title.isEmpty ? info.applicationName : info.title
details.applicationName = info.applicationName
details.applicationBundleID = info.bundleID
details.scaleFactor = scale
details.screenBounds = info.cgFrame
return Capture(image: image, details: details)
}
/// Crops a region out of an already frozen display snapshot.
static func crop(snapshot: DisplaySnapshot, cgRect: CGRect, mode: CaptureMode) throws -> Capture {
let local = CGRect(x: cgRect.origin.x - snapshot.cgFrame.origin.x,
y: cgRect.origin.y - snapshot.cgFrame.origin.y,
width: cgRect.width,
height: cgRect.height)
guard let cropped = ScreenGeometry.crop(snapshot.image, toPointRect: local, scale: snapshot.scale) else {
throw CaptureError.captureFailed("The selected region was empty.")
}
var details = CaptureDetails()
details.captureMode = mode
details.title = snapshot.screen.localizedName
details.scaleFactor = snapshot.scale
details.screenBounds = cgRect
return Capture(image: cropped, details: details)
}
// MARK: - Feedback
@MainActor
static func playFeedback(on rect: CGRect?) {
if Configuration.shared.playCameraSound {
NSSound(named: NSSound.Name("Grab"))?.play()
}
if Configuration.shared.showFlashAnimation, let rect {
FlashOverlay.flash(appKitRect: ScreenGeometry.appKitRect(fromCG: rect))
}
}
}
extension CGRect {
var center: CGPoint { CGPoint(x: midX, y: midY) }
}
/// Short white flash over the captured area, the equivalent of Greenshot's capture flash.
@MainActor
final class FlashOverlay {
private static var windows: [NSWindow] = []
static func flash(appKitRect rect: CGRect) {
guard rect.width > 1, rect.height > 1 else { return }
let window = NSWindow(contentRect: rect, styleMask: .borderless, backing: .buffered, defer: false)
window.isOpaque = false
window.backgroundColor = .white
window.alphaValue = 0.7
window.level = .screenSaver
window.ignoresMouseEvents = true
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
window.orderFrontRegardless()
windows.append(window)
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.18
window.animator().alphaValue = 0
} completionHandler: {
// Run on the main thread by contract, so main actor isolation can be assumed.
MainActor.assumeIsolated {
window.orderOut(nil)
FlashOverlay.windows.removeAll { $0 === window }
}
}
}
}