Compare commits

..
5 Commits
Author SHA1 Message Date
TZGyn 07642ca408 Auto-detect window corner radius; anti-alias the mask
Measure the picked window's real corner radius from the capture itself
(the crop's corner pixels are the desktop background; app content starts
where the color departs), correct for arc anti-aliasing, and fall back to
10pt when ambiguous. The clip mask is supersampled 3x for smooth edges.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-09 15:20:21 +08:00
TZGyn 924a692f2b Round window-capture corners with a clip instead of masking()
The masking() call could silently fail against ScreenCaptureKit's pixel
format, leaving sharp corners. Clip to a rounded path and redraw instead,
verified headlessly for corner clipping and orientation.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-09 15:15:21 +08:00
TZGyn ae054d70b9 Treat any same-app window as popup risk for window capture
Owned popups composite their parent even when the windows do not
overlap, so the intersection test missed DBeaver-style floating dialogs.
Any other window from the same app now routes the pick to the screen
crop.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-09 15:09:09 +08:00
TZGyn 3078713f8f Use screen crop for owned popup windows
desktopIndependentWindow composites owned popups together with their
parent window and scales the group to the popup's size. Detect the case
at pick time — another window from the same app overlapping the picked
one — and capture the screen crop with a rounded mask instead, which
contains only the popup.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-09 15:05:34 +08:00
TZGyn 265571d7e2 Reject window captures that composite extra windows
ScreenCaptureKit's desktopIndependentWindow filter can include owned
child windows (e.g. an app dialog plus its parent), producing a capture
much larger than the picked window. Detect the overflow by comparing the
returned image size against the window's frame and fall back to the
screen crop with rounded mask. Hover changes are logged with the picked
window id and tracked count for diagnosis.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-09 14:55:40 +08:00
2 changed files with 103 additions and 26 deletions
+101 -24
View File
@@ -14,9 +14,9 @@ final class CaptureController: NSObject {
// Selection state (driven by the event tap; AppKit global coordinates) // Selection state (driven by the event tap; AppKit global coordinates)
private var marqueeStart: NSPoint? private var marqueeStart: NSPoint?
private(set) var marqueeRect: NSRect? private(set) var marqueeRect: NSRect?
private(set) var hoverWindow: (id: CGWindowID, frame: NSRect)? private(set) var hoverWindow: (id: CGWindowID, frame: NSRect, pid: Int)?
var hoverFrame: NSRect? { hoverWindow?.frame } var hoverFrame: NSRect? { hoverWindow?.frame }
private var trackedWindows: [(id: CGWindowID, frame: NSRect)] = [] private var trackedWindows: [(id: CGWindowID, frame: NSRect, pid: Int)] = []
private var lastWindowScan = Date.distantPast private var lastWindowScan = Date.distantPast
private var eventTap: CFMachPort? private var eventTap: CFMachPort?
@@ -105,13 +105,17 @@ final class CaptureController: NSObject {
/// A window was picked from hover — capture it directly (real rounded /// A window was picked from hover — capture it directly (real rounded
/// corners come from the window's own alpha). /// corners come from the window's own alpha).
private func finishWindowPick(_ windowID: CGWindowID, _ rect: NSRect) { private func finishWindowPick(_ windowID: CGWindowID, _ rect: NSRect, ownedRisk: Bool) {
let ids = overlayIDs let ids = overlayIDs
hideOverlays() hideOverlays()
isSelecting = false isSelecting = false
restorePreviousApp() restorePreviousApp()
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
capture(rect, excludingWindowIDs: ids, windowID: windowID) if ownedRisk {
capture(rect, excludingWindowIDs: ids, roundedCorners: true)
} else {
capture(rect, excludingWindowIDs: ids, windowID: windowID)
}
} }
} }
@@ -219,11 +223,12 @@ final class CaptureController: NSObject {
) as? [[String: Any]] else { trackedWindows = []; return } ) as? [[String: Any]] else { trackedWindows = []; return }
let ownIDs = Set(overlayIDs) let ownIDs = Set(overlayIDs)
var found: [(id: CGWindowID, frame: NSRect)] = [] var found: [(id: CGWindowID, frame: NSRect, pid: Int)] = []
for info in list { for info in list {
guard let cid = info[kCGWindowNumber as String] as? CGWindowID, guard let cid = info[kCGWindowNumber as String] as? CGWindowID,
let bounds = info[kCGWindowBounds as String] as? [String: CGFloat], let bounds = info[kCGWindowBounds as String] as? [String: CGFloat],
let layer = info[kCGWindowLayer as String] as? Int, let layer = info[kCGWindowLayer as String] as? Int,
let pid = info[kCGWindowOwnerPID as String] as? Int,
layer == 0 layer == 0
else { continue } else { continue }
if ownIDs.contains(Int(cid)) { continue } if ownIDs.contains(Int(cid)) { continue }
@@ -235,7 +240,7 @@ final class CaptureController: NSObject {
) )
guard cgRect.width >= 80, cgRect.height >= 60 else { continue } guard cgRect.width >= 80, cgRect.height >= 60 else { continue }
let rect = Self.cgToAppKit(rect: cgRect) let rect = Self.cgToAppKit(rect: cgRect)
found.append((cid, rect)) found.append((id: cid, frame: rect, pid: pid))
} }
trackedWindows = found trackedWindows = found
} }
@@ -248,7 +253,7 @@ final class CaptureController: NSObject {
return NSRect(x: rect.origin.x, y: y, width: rect.width, height: rect.height) return NSRect(x: rect.origin.x, y: y, width: rect.width, height: rect.height)
} }
private func topmostWindowContaining(_ appKitPoint: NSPoint) -> (id: CGWindowID, frame: NSRect)? { private func topmostWindowContaining(_ appKitPoint: NSPoint) -> (id: CGWindowID, frame: NSRect, pid: Int)? {
for entry in trackedWindows { for entry in trackedWindows {
if entry.frame.contains(appKitPoint) { if entry.frame.contains(appKitPoint) {
return entry return entry
@@ -287,6 +292,11 @@ final class CaptureController: NSObject {
let window = topmostWindowContaining(appKitPoint) let window = topmostWindowContaining(appKitPoint)
if window?.frame != hoverFrame { if window?.frame != hoverFrame {
hoverWindow = window hoverWindow = window
if let w = window {
CaptureController.log("hover: picked id=\(w.id) frame=\(w.frame) (tracked=\(trackedWindows.count))")
} else {
CaptureController.log("hover: none (tracked=\(trackedWindows.count))")
}
renderOverlays() renderOverlays()
} }
return event return event
@@ -316,8 +326,17 @@ final class CaptureController: NSObject {
CaptureController.log("tap: marquee complete \(rect)") CaptureController.log("tap: marquee complete \(rect)")
finishSelection(rect) finishSelection(rect)
} else if let hover = hoverWindow { } else if let hover = hoverWindow {
CaptureController.log("tap: window pick id=\(hover.id) \(hover.frame)") // An overlapping window from the same app means this is an
finishWindowPick(hover.id, hover.frame) // owned popup — desktopIndependentWindow would composite the
// parent too, so capture the screen crop instead.
// Owned popups composite their parent even when the windows
// don't overlap, so ANY other same-app window on screen means
// crop (correct region) instead of the group composite.
let ownedRisk = trackedWindows.contains {
$0.id != hover.id && $0.pid == hover.pid
}
CaptureController.log("tap: window pick id=\(hover.id) pid=\(hover.pid) ownedRisk=\(ownedRisk) \(hover.frame)")
finishWindowPick(hover.id, hover.frame, ownedRisk: ownedRisk)
} else { } else {
CaptureController.log("tap: click with no hover/marquee — cancel") CaptureController.log("tap: click with no hover/marquee — cancel")
cancelSelection() cancelSelection()
@@ -353,6 +372,41 @@ final class ScreenCapture {
private init() {} 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..<limit {
let p = axis == 0 ? pixel(x: 0, yTop: i) : pixel(x: i, yTop: 0)
if diffFromBg(p) > 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, *) @available(macOS 14.0, *)
private func crop(_ image: CGImage, to cgRect: CGRect, display: SCDisplay, roundedCorners: Bool = false) -> CGImage? { 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). // cgRect is in SCDisplay space (bottom-left origin, Y flipped vs AppKit).
@@ -383,23 +437,34 @@ final class ScreenCapture {
// Apply a rounded-corner alpha mask for window captures, matching // Apply a rounded-corner alpha mask for window captures, matching
// macOS window chrome (≈10pt radius, scaled to image pixels). // macOS window chrome (≈10pt radius, scaled to image pixels).
if roundedCorners { if roundedCorners {
let radius = 10 * scaleX // The screen crop contains the app's real rounded corners against
let maskRect = CGRect(x: 0, y: 0, width: result.width, height: result.height) // the desktop — measure the radius from the corner pixels, then
guard let maskCtx = CGContext( // 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, data: nil, width: result.width, height: result.height,
bitsPerComponent: 8, bytesPerRow: 0, bitsPerComponent: 8, bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(), space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else { return result } ) else { return result }
maskCtx.setFillColor(CGColor(gray: 1, alpha: 1)) downCtx.interpolationQuality = .high
maskCtx.fill(maskRect) downCtx.draw(hi, in: CGRect(x: 0, y: 0, width: result.width, height: result.height))
maskCtx.setBlendMode(.clear) if let masked = downCtx.makeImage() {
maskCtx.setFillColor(CGColor(gray: 0, alpha: 1))
maskCtx.addPath(CGPath(roundedRect: maskRect, cornerWidth: radius, cornerHeight: radius, transform: nil))
maskCtx.fillPath()
maskCtx.setBlendMode(.normal)
if let mask = maskCtx.makeImage(),
let masked = result.masking(mask) {
result = masked result = masked
} }
} }
@@ -463,9 +528,21 @@ final class ScreenCapture {
contentFilter: SCContentFilter(desktopIndependentWindow: scWindow), contentFilter: SCContentFilter(desktopIndependentWindow: scWindow),
configuration: windowConfig configuration: windowConfig
) { ) {
CaptureController.log("capture: WINDOW id=\(windowID) image=\(windowImage.width)x\(windowImage.height) (native alpha corners)") let expectedW = CGFloat(scWindow.frame.width) * backingScale
completion(windowImage) let expectedH = CGFloat(scWindow.frame.height) * backingScale
return let overflow = max(
CGFloat(windowImage.width) / max(expectedW, 1),
CGFloat(windowImage.height) / max(expectedH, 1)
)
if overflow > 1.35 {
// SCKit composited extra windows (owned popups etc.)
// — reject and use the screen crop instead.
CaptureController.log("capture: WINDOW id=\(windowID) overflow=\(overflow) (\(windowImage.width)x\(windowImage.height) vs \(Int(expectedW))x\(Int(expectedH))) — rejecting, using crop+mask")
} else {
CaptureController.log("capture: WINDOW id=\(windowID) image=\(windowImage.width)x\(windowImage.height) (native alpha corners)")
completion(windowImage)
return
}
} }
CaptureController.log("capture: window filter failed for id=\(windowID) — falling back to crop+mask") CaptureController.log("capture: window filter failed for id=\(windowID) — falling back to crop+mask")
} }
+2 -2
View File
@@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>Icon</string> <string>Icon</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.8.0</string> <string>1.8.5</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>80</string> <string>85</string>
<key>LSUIElement</key> <key>LSUIElement</key>
<true/> <true/>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>