Files
openclaw/apps/macos/Sources/OpenClaw/ScreenSnapshotService.swift
Peter Steinberger 62bd760c0e chore(swift): enforce current formatting and lint rules (#103313)
* style: apply SwiftFormat 0.62.1 rules

Refs #103202

* ci: enforce deterministic Swift lint

Refs #103202

* refactor: keep gateway connect lint-clean

Refs #103202

* style: keep iOS typography checks warning-free

* ci: route MLX Swift changes through pre-push

* fix: preserve native i18n extraction after Swift cleanup

* refactor: keep rebased Swift surfaces lint-clean

* style: format latest Swift additions

* chore: refresh native i18n inventory

* style: keep generated Swift formatter-clean

* fix: preserve node route invalidation callbacks

* fix: keep native translation IDs stable

* fix: retain native translation identifiers

* fix: preserve translations across Swift source moves
2026-07-10 11:54:08 +01:00

161 lines
5.5 KiB
Swift

import AppKit
import Foundation
import OpenClawKit
@preconcurrency import ScreenCaptureKit
struct ScreenSnapshotResult: Sendable {
let data: Data
let format: OpenClawScreenSnapshotFormat
let width: Int
let height: Int
let displayFrameId: String
}
@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
-> ScreenSnapshotResult
{
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 displayFrameId = try Self.displayFrameId(
for: display,
referenceWidth: normalized.maxWidth)
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("screen capture failed")
}
// Geometry is part of the coordinate contract. If it changed while the
// pixels were captured, no stable frame exists to authorize later input.
let finalDisplayFrameId = try Self.displayFrameId(
for: display,
referenceWidth: normalized.maxWidth)
guard displayFrameId == finalDisplayFrameId else {
throw ScreenSnapshotError.captureFailed("display changed during screen capture")
}
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 ScreenSnapshotResult(
data: data,
format: format,
width: cgImage.width,
height: cgImage.height,
displayFrameId: displayFrameId)
}
private static func displayFrameId(
for display: SCDisplay,
referenceWidth: Int) throws -> String
{
let bounds = CGDisplayBounds(display.displayID)
let geometry = OpenClawComputerDisplayGeometry(
originX: bounds.origin.x,
originY: bounds.origin.y,
widthPoints: bounds.width,
heightPoints: bounds.height)
let sourceWidth = Double(display.width)
let sourceHeight = Double(display.height)
guard OpenClawComputerInputGeometry.isValidMappingGeometry(
sourceWidth: sourceWidth,
sourceHeight: sourceHeight,
display: geometry)
else {
throw ScreenSnapshotError.noDisplays
}
return OpenClawComputerInputGeometry.displayFrameId(
displayID: display.displayID,
sourceWidth: sourceWidth,
sourceHeight: sourceHeight,
referenceWidth: referenceWidth,
display: geometry)
}
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)
}
}