diff --git a/Sources/Kapture/CaptureController.swift b/Sources/Kapture/CaptureController.swift index 08e3f79..0da1e55 100644 --- a/Sources/Kapture/CaptureController.swift +++ b/Sources/Kapture/CaptureController.swift @@ -86,10 +86,12 @@ final class ScreenCapture { @available(macOS 14.0, *) private func crop(_ image: CGImage, to rect: NSRect, display: SCDisplay) -> CGImage? { - // Scale from the actual captured image dimensions, which reflect the - // stream's pixel output (retina => 2x, non-retina => 1x). + // The captured image may be at any scale (retina 2x, secondary display + // 1x, or trimmed by ScreenCaptureKit). Derive the pixel scale from the + // image we actually got, relative to the display's point frame. let scaleX = CGFloat(image.width) / display.frame.width let scaleY = CGFloat(image.height) / display.frame.height + guard scaleX > 0, scaleY > 0 else { return nil } let cgRect = CGRect( x: rect.minX, y: rect.minY, @@ -102,14 +104,18 @@ final class ScreenCapture { width: cgRect.width * scaleX, height: cgRect.height * scaleY ) + // Guard against negative offsets when the selection reaches beyond the + // display's frame (e.g. secondary monitor coordinate quirks). + let safeRect = pxRect.intersection(CGRect(x: 0, y: 0, width: CGFloat(image.width), height: CGFloat(image.height))) + guard safeRect.width >= 1, safeRect.height >= 1 else { return nil } guard let cropCtx = CGContext( - data: nil, width: Int(pxRect.width), height: Int(pxRect.height), + data: nil, width: Int(safeRect.width), height: Int(safeRect.height), bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) else { return nil } cropCtx.interpolationQuality = .high - cropCtx.draw(image, in: CGRect(x: -pxRect.minX, y: -pxRect.minY, width: CGFloat(image.width), height: CGFloat(image.height))) + cropCtx.draw(image, in: CGRect(x: -safeRect.minX, y: -safeRect.minY, width: CGFloat(image.width), height: CGFloat(image.height))) return cropCtx.makeImage() } @@ -135,13 +141,10 @@ final class ScreenCapture { let excluded = content.windows.filter { excludedSet.contains($0.windowID) } let filter = SCContentFilter(display: display, excludingWindows: excluded) let config = SCStreamConfiguration() - // width/height are in points. Use the display's backing scale so - // retina captures come out at full pixel resolution. - let backingScale = NSScreen.screens.first(where: { $0.frame.contains( - NSPoint(x: rect.midX, y: rect.midY) - ) })?.backingScaleFactor ?? 2 - config.width = Int(display.frame.width * backingScale) - config.height = Int(display.frame.height * backingScale) + // display.width/height are pixels; request the display's full + // native resolution so ScreenCaptureKit gives us every pixel. + config.width = Int(display.width) + config.height = Int(display.height) config.showsCursor = true guard let image = try? await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) else { diff --git a/build-app.sh b/build-app.sh index 8bff8a3..33c77dd 100755 --- a/build-app.sh +++ b/build-app.sh @@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST' CFBundleIconFile Icon CFBundleShortVersionString - 1.1.3 + 1.1.4 CFBundleVersion - 13 + 14 LSUIElement NSHighResolutionCapable