iOS: support jpeg canvas snapshots
This commit is contained in:
@@ -403,9 +403,24 @@ final class NodeAppModel {
|
|||||||
|
|
||||||
case ClawdisCanvasCommand.snapshot.rawValue:
|
case ClawdisCanvasCommand.snapshot.rawValue:
|
||||||
let params = try? Self.decodeParams(ClawdisCanvasSnapshotParams.self, from: req.paramsJSON)
|
let params = try? Self.decodeParams(ClawdisCanvasSnapshotParams.self, from: req.paramsJSON)
|
||||||
let maxWidth = params?.maxWidth.map { CGFloat($0) }
|
let format = params?.format ?? .jpeg
|
||||||
let base64 = try await self.screen.snapshotPNGBase64(maxWidth: maxWidth)
|
let maxWidth: CGFloat? = {
|
||||||
let payload = try Self.encodePayload(["format": "png", "base64": base64])
|
if let raw = params?.maxWidth, raw > 0 { return CGFloat(raw) }
|
||||||
|
// Keep default snapshots comfortably below the gateway client's maxPayload.
|
||||||
|
// For full-res, clients should explicitly request a larger maxWidth.
|
||||||
|
return switch format {
|
||||||
|
case .png: 900
|
||||||
|
case .jpeg: 1600
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
let base64 = try await self.screen.snapshotBase64(
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
format: format,
|
||||||
|
quality: params?.quality)
|
||||||
|
let payload = try Self.encodePayload([
|
||||||
|
"format": format == .jpeg ? "jpeg" : "png",
|
||||||
|
"base64": base64,
|
||||||
|
])
|
||||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||||
|
|
||||||
case ClawdisCanvasA2UICommand.reset.rawValue:
|
case ClawdisCanvasA2UICommand.reset.rawValue:
|
||||||
|
|||||||
@@ -147,11 +147,54 @@ final class ScreenController {
|
|||||||
return data.base64EncodedString()
|
return data.base64EncodedString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func snapshotBase64(
|
||||||
|
maxWidth: CGFloat? = nil,
|
||||||
|
format: ClawdisCanvasSnapshotFormat,
|
||||||
|
quality: Double? = nil) async throws -> String
|
||||||
|
{
|
||||||
|
let config = WKSnapshotConfiguration()
|
||||||
|
if let maxWidth {
|
||||||
|
config.snapshotWidth = NSNumber(value: Double(maxWidth))
|
||||||
|
}
|
||||||
|
let image: UIImage = try await withCheckedThrowingContinuation { cont in
|
||||||
|
self.webView.takeSnapshot(with: config) { image, error in
|
||||||
|
if let error {
|
||||||
|
cont.resume(throwing: error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let image else {
|
||||||
|
cont.resume(throwing: NSError(domain: "Screen", code: 2, userInfo: [
|
||||||
|
NSLocalizedDescriptionKey: "snapshot failed",
|
||||||
|
]))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cont.resume(returning: image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: Data?
|
||||||
|
switch format {
|
||||||
|
case .png:
|
||||||
|
data = image.pngData()
|
||||||
|
case .jpeg:
|
||||||
|
let q = (quality ?? 0.82).clamped(to: 0.1...1.0)
|
||||||
|
data = image.jpegData(compressionQuality: q)
|
||||||
|
}
|
||||||
|
guard let data else {
|
||||||
|
throw NSError(domain: "Screen", code: 1, userInfo: [
|
||||||
|
NSLocalizedDescriptionKey: "snapshot encode failed",
|
||||||
|
])
|
||||||
|
}
|
||||||
|
return data.base64EncodedString()
|
||||||
|
}
|
||||||
|
|
||||||
// SwiftPM flattens resource directories; ensure resource filenames are unique.
|
// SwiftPM flattens resource directories; ensure resource filenames are unique.
|
||||||
private static let canvasScaffoldURL: URL? = ClawdisKitResources.bundle.url(
|
private static let canvasScaffoldURL: URL? = ClawdisKitResources.bundle.url(
|
||||||
forResource: "scaffold",
|
forResource: "scaffold",
|
||||||
withExtension: "html")
|
withExtension: "html")
|
||||||
private static let a2uiIndexURL: URL? = ClawdisKitResources.bundle.url(forResource: "index", withExtension: "html")
|
private static let a2uiIndexURL: URL? = ClawdisKitResources.bundle.url(
|
||||||
|
forResource: "index",
|
||||||
|
withExtension: "html")
|
||||||
|
|
||||||
fileprivate func isTrustedCanvasUIURL(_ url: URL) -> Bool {
|
fileprivate func isTrustedCanvasUIURL(_ url: URL) -> Bool {
|
||||||
guard url.isFileURL else { return false }
|
guard url.isFileURL else { return false }
|
||||||
@@ -170,6 +213,14 @@ final class ScreenController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Double {
|
||||||
|
fileprivate func clamped(to range: ClosedRange<Double>) -> Double {
|
||||||
|
if self < range.lowerBound { return range.lowerBound }
|
||||||
|
if self > range.upperBound { return range.upperBound }
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Navigation Delegate
|
// MARK: - Navigation Delegate
|
||||||
|
|
||||||
/// Handles navigation policy to intercept clawdis:// deep links from canvas
|
/// Handles navigation policy to intercept clawdis:// deep links from canvas
|
||||||
|
|||||||
Reference in New Issue
Block a user