






















1+import AppKit
2+import Foundation
3+import OpenClawKit
4+@preconcurrency import ScreenCaptureKit
5+6+@MainActor
7+final class ScreenSnapshotService {
8+enum ScreenSnapshotError: LocalizedError {
9+case noDisplays
10+case invalidScreenIndex(Int)
11+case captureFailed(String)
12+case encodeFailed(String)
13+14+var errorDescription: String? {
15+switch self {
16+case .noDisplays:
17+"No displays available for screen snapshot"
18+case let .invalidScreenIndex(idx):
19+"Invalid screen index \(idx)"
20+case let .captureFailed(message):
21+ message
22+case let .encodeFailed(message):
23+ message
24+}
25+}
26+}
27+28+func snapshot(
29+ screenIndex: Int?,
30+ maxWidth: Int?,
31+ quality: Double?,
32+ format: OpenClawScreenSnapshotFormat?) async throws
33+-> (data: Data, format: OpenClawScreenSnapshotFormat, width: Int, height: Int)
34+{
35+let format = format ?? .jpeg
36+let normalized = Self.normalize(maxWidth: maxWidth, quality: quality, format: format)
37+38+let content = try await SCShareableContent.current
39+let displays = content.displays.sorted { $0.displayID < $1.displayID }
40+guard !displays.isEmpty else {
41+throw ScreenSnapshotError.noDisplays
42+}
43+44+let idx = screenIndex ?? 0
45+guard idx >= 0, idx < displays.count else {
46+throw ScreenSnapshotError.invalidScreenIndex(idx)
47+}
48+let display = displays[idx]
49+50+let filter = SCContentFilter(display: display, excludingWindows: [])
51+let config = SCStreamConfiguration()
52+let targetSize = Self.targetSize(
53+ width: display.width,
54+ height: display.height,
55+ maxWidth: normalized.maxWidth)
56+ config.width = targetSize.width
57+ config.height = targetSize.height
58+ config.showsCursor = true
59+60+let cgImage: CGImage
61+do {
62+ cgImage = try await SCScreenshotManager.captureImage(
63+ contentFilter: filter,
64+ configuration: config)
65+} catch {
66+throw ScreenSnapshotError.captureFailed(error.localizedDescription)
67+}
68+69+let bitmap = NSBitmapImageRep(cgImage: cgImage)
70+let data: Data
71+switch format {
72+case .png:
73+guard let encoded = bitmap.representation(using: .png, properties: [:]) else {
74+throw ScreenSnapshotError.encodeFailed("png encode failed")
75+}
76+ data = encoded
77+case .jpeg:
78+guard let encoded = bitmap.representation(
79+ using: .jpeg,
80+ properties: [.compressionFactor: normalized.quality])
81+else {
82+throw ScreenSnapshotError.encodeFailed("jpeg encode failed")
83+}
84+ data = encoded
85+}
86+87+return (data: data, format: format, width: cgImage.width, height: cgImage.height)
88+}
89+90+private static func normalize(
91+ maxWidth: Int?,
92+ quality: Double?,
93+ format: OpenClawScreenSnapshotFormat)
94+-> (maxWidth: Int, quality: Double)
95+{
96+let resolvedMaxWidth = maxWidth.flatMap { $0 > 0 ? $0 : nil } ?? (format == .png ? 900 : 1600)
97+let resolvedQuality = min(1.0, max(0.05, quality ?? 0.72))
98+return (maxWidth: resolvedMaxWidth, quality: resolvedQuality)
99+}
100+101+private static func targetSize(width: Int, height: Int, maxWidth: Int) -> (width: Int, height: Int) {
102+guard width > 0, height > 0, width > maxWidth else {
103+return (width: width, height: height)
104+}
105+let scale = Double(maxWidth) / Double(width)
106+let targetHeight = max(1, Int((Double(height) * scale).rounded()))
107+return (width: maxWidth, height: targetHeight)
108+}
109+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。