diff --git a/Sources/Kapture/CaptureController.swift b/Sources/Kapture/CaptureController.swift index 1f7cccb..32f2ea1 100644 --- a/Sources/Kapture/CaptureController.swift +++ b/Sources/Kapture/CaptureController.swift @@ -372,6 +372,41 @@ final class ScreenCapture { private init() {} + /// Estimate the window's corner radius (in image pixels) by scanning the + /// top-left corner: the crop's corner pixels are the desktop background; + /// app content starts where the color departs from it. + static func detectCornerRadius(of image: CGImage) -> CGFloat? { + let w = image.width, h = image.height + guard w > 30, h > 30 else { return nil } + var px = [UInt8](repeating: 0, count: w * h * 4) + guard let c = CGContext(data: &px, width: w, height: h, bitsPerComponent: 8, bytesPerRow: w * 4, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil } + c.draw(image, in: CGRect(x: 0, y: 0, width: w, height: h)) + + func pixel(x: Int, yTop: Int) -> (r: Int, g: Int, b: Int) { + let row = yTop * w * 4 + x * 4 + return (Int(px[row]), Int(px[row + 1]), Int(px[row + 2])) + } + let bg = pixel(x: 0, yTop: 0) + func diffFromBg(_ p: (r: Int, g: Int, b: Int)) -> Int { + abs(p.r - bg.r) + abs(p.g - bg.g) + abs(p.b - bg.b) + } + var radii: [CGFloat] = [] + for (limit, axis) in [(h / 3, 0), (w / 3, 1)] { + for i in 0.. 24 { + radii.append(CGFloat(i)) + break + } + } + } + guard radii.count == 2 else { return nil } + let r = (radii[0] + radii[1]) / 2 + return (r >= 3 && r <= 40) ? r : nil + } + @available(macOS 14.0, *) private func crop(_ image: CGImage, to cgRect: CGRect, display: SCDisplay, roundedCorners: Bool = false) -> CGImage? { // cgRect is in SCDisplay space (bottom-left origin, Y flipped vs AppKit). @@ -402,19 +437,34 @@ final class ScreenCapture { // Apply a rounded-corner alpha mask for window captures, matching // macOS window chrome (≈10pt radius, scaled to image pixels). if roundedCorners { - // Clip to a rounded rect and redraw — deterministic corner alpha - // (masking() can fail depending on the source pixel format). - let radius = 10 * scaleX - guard let ctx = CGContext( + // The screen crop contains the app's real rounded corners against + // the desktop — measure the radius from the corner pixels, then + // mask with it (supersampled for smooth edges). + let detected = Self.detectCornerRadius(of: result) + let radius = min(max((detected.map { $0 * 1.23 + 2.6 }) ?? (10 * scaleX), 4), 40) + CaptureController.log("corners: detected=\(detected.map { String(format: "%.1f", $0) } ?? "nil") using=\(Int(radius))px") + let ss = 3 + let W = result.width * ss, H = result.height * ss + guard let hiCtx = CGContext( + data: nil, width: W, height: H, + bitsPerComponent: 8, bytesPerRow: 0, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { return result } + hiCtx.addPath(CGPath(roundedRect: CGRect(x: 0, y: 0, width: W, height: H), cornerWidth: radius * CGFloat(ss), cornerHeight: radius * CGFloat(ss), transform: nil)) + hiCtx.clip() + hiCtx.interpolationQuality = .high + hiCtx.draw(result, in: CGRect(x: 0, y: 0, width: W, height: H)) + guard let hi = hiCtx.makeImage(), + let downCtx = CGContext( data: nil, width: result.width, height: result.height, bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) else { return result } - ctx.addPath(CGPath(roundedRect: CGRect(x: 0, y: 0, width: result.width, height: result.height), cornerWidth: radius, cornerHeight: radius, transform: nil)) - ctx.clip() - ctx.draw(result, in: CGRect(x: 0, y: 0, width: result.width, height: result.height)) - if let masked = ctx.makeImage() { + downCtx.interpolationQuality = .high + downCtx.draw(hi, in: CGRect(x: 0, y: 0, width: result.width, height: result.height)) + if let masked = downCtx.makeImage() { result = masked } } diff --git a/build-app.sh b/build-app.sh index e895133..3676233 100755 --- a/build-app.sh +++ b/build-app.sh @@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST' CFBundleIconFile Icon CFBundleShortVersionString - 1.8.4 + 1.8.5 CFBundleVersion - 84 + 85 LSUIElement NSHighResolutionCapable