mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-05-01 06:36:23 +08:00
110 lines
3.7 KiB
Swift
110 lines
3.7 KiB
Swift
import AppKit
|
|
import Foundation
|
|
import OpenClawKit
|
|
@preconcurrency import ScreenCaptureKit
|
|
|
|
@MainActor
|
|
final class ScreenSnapshotService {
|
|
enum ScreenSnapshotError: LocalizedError {
|
|
case noDisplays
|
|
case invalidScreenIndex(Int)
|
|
case captureFailed(String)
|
|
case encodeFailed(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .noDisplays:
|
|
"No displays available for screen snapshot"
|
|
case let .invalidScreenIndex(idx):
|
|
"Invalid screen index \(idx)"
|
|
case let .captureFailed(message):
|
|
message
|
|
case let .encodeFailed(message):
|
|
message
|
|
}
|
|
}
|
|
}
|
|
|
|
func snapshot(
|
|
screenIndex: Int?,
|
|
maxWidth: Int?,
|
|
quality: Double?,
|
|
format: OpenClawScreenSnapshotFormat?) async throws
|
|
-> (data: Data, format: OpenClawScreenSnapshotFormat, width: Int, height: Int)
|
|
{
|
|
let format = format ?? .jpeg
|
|
let normalized = Self.normalize(maxWidth: maxWidth, quality: quality, format: format)
|
|
|
|
let content = try await SCShareableContent.current
|
|
let displays = content.displays.sorted { $0.displayID < $1.displayID }
|
|
guard !displays.isEmpty else {
|
|
throw ScreenSnapshotError.noDisplays
|
|
}
|
|
|
|
let idx = screenIndex ?? 0
|
|
guard idx >= 0, idx < displays.count else {
|
|
throw ScreenSnapshotError.invalidScreenIndex(idx)
|
|
}
|
|
let display = displays[idx]
|
|
|
|
let filter = SCContentFilter(display: display, excludingWindows: [])
|
|
let config = SCStreamConfiguration()
|
|
let targetSize = Self.targetSize(
|
|
width: display.width,
|
|
height: display.height,
|
|
maxWidth: normalized.maxWidth)
|
|
config.width = targetSize.width
|
|
config.height = targetSize.height
|
|
config.showsCursor = true
|
|
|
|
let cgImage: CGImage
|
|
do {
|
|
cgImage = try await SCScreenshotManager.captureImage(
|
|
contentFilter: filter,
|
|
configuration: config)
|
|
} catch {
|
|
throw ScreenSnapshotError.captureFailed(error.localizedDescription)
|
|
}
|
|
|
|
let bitmap = NSBitmapImageRep(cgImage: cgImage)
|
|
let data: Data
|
|
switch format {
|
|
case .png:
|
|
guard let encoded = bitmap.representation(using: .png, properties: [:]) else {
|
|
throw ScreenSnapshotError.encodeFailed("png encode failed")
|
|
}
|
|
data = encoded
|
|
case .jpeg:
|
|
guard let encoded = bitmap.representation(
|
|
using: .jpeg,
|
|
properties: [.compressionFactor: normalized.quality])
|
|
else {
|
|
throw ScreenSnapshotError.encodeFailed("jpeg encode failed")
|
|
}
|
|
data = encoded
|
|
}
|
|
|
|
return (data: data, format: format, width: cgImage.width, height: cgImage.height)
|
|
}
|
|
|
|
private static func normalize(
|
|
maxWidth: Int?,
|
|
quality: Double?,
|
|
format: OpenClawScreenSnapshotFormat)
|
|
-> (maxWidth: Int, quality: Double)
|
|
{
|
|
let resolvedMaxWidth = maxWidth.flatMap { $0 > 0 ? $0 : nil } ?? (format == .png ? 900 : 1600)
|
|
let resolvedQuality = min(1.0, max(0.05, quality ?? 0.72))
|
|
return (maxWidth: resolvedMaxWidth, quality: resolvedQuality)
|
|
}
|
|
|
|
private static func targetSize(width: Int, height: Int, maxWidth: Int) -> (width: Int, height: Int) {
|
|
guard width > 0, height > 0, width > maxWidth else {
|
|
return (width: width, height: height)
|
|
}
|
|
let scale = Double(maxWidth) / Double(width)
|
|
let targetHeight = max(1, Int((Double(height) * scale).rounded()))
|
|
return (width: maxWidth, height: targetHeight)
|
|
}
|
|
}
|