dt_automate/vendor/github.com/kbinani/screenshot/darwin.go
2025-02-19 18:30:19 +08:00

252 lines
7.7 KiB
Go

//go:build cgo && darwin
package screenshot
/*
#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > MAC_OS_VERSION_14_4
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework CoreGraphics -framework CoreFoundation -framework ScreenCaptureKit
#include <ScreenCaptureKit/ScreenCaptureKit.h>
#else
#cgo LDFLAGS: -framework CoreGraphics -framework CoreFoundation
#endif
#include <CoreGraphics/CoreGraphics.h>
static CGImageRef capture(CGDirectDisplayID id, CGRect diIntersectDisplayLocal, CGColorSpaceRef colorSpace) {
#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > MAC_OS_VERSION_14_4
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block CGImageRef result = nil;
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent* content, NSError* error) {
@autoreleasepool {
if (error) {
dispatch_semaphore_signal(semaphore);
return;
}
SCDisplay* target = nil;
for (SCDisplay *display in content.displays) {
if (display.displayID == id) {
target = display;
break;
}
}
if (!target) {
dispatch_semaphore_signal(semaphore);
return;
}
SCContentFilter* filter = [[SCContentFilter alloc] initWithDisplay:target excludingWindows:@[]];
SCStreamConfiguration* config = [[SCStreamConfiguration alloc] init];
config.sourceRect = diIntersectDisplayLocal;
config.width = diIntersectDisplayLocal.size.width;
config.height = diIntersectDisplayLocal.size.height;
[SCScreenshotManager captureImageWithFilter:filter
configuration:config
completionHandler:^(CGImageRef img, NSError* error) {
if (!error) {
result = CGImageCreateCopyWithColorSpace(img, colorSpace);
}
dispatch_semaphore_signal(semaphore);
}];
}
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_release(semaphore);
return result;
#else
CGImageRef img = CGDisplayCreateImageForRect(id, diIntersectDisplayLocal);
if (!img) {
return nil;
}
CGImageRef copy = CGImageCreateCopyWithColorSpace(img, colorSpace);
CGImageRelease(img);
if (!copy) {
return nil;
}
return copy;
#endif
}
*/
import "C"
import (
"errors"
"image"
"unsafe"
)
func Capture(x, y, width, height int) (*image.RGBA, error) {
if width <= 0 || height <= 0 {
return nil, errors.New("width or height should be > 0")
}
rect := image.Rect(0, 0, width, height)
img, err := createImage(rect)
if err != nil {
return nil, err
}
// cg: CoreGraphics coordinate (origin: lower-left corner of primary display, x-axis: rightward, y-axis: upward)
// win: Windows coordinate (origin: upper-left corner of primary display, x-axis: rightward, y-axis: downward)
// di: Display local coordinate (origin: upper-left corner of the display, x-axis: rightward, y-axis: downward)
cgMainDisplayBounds := getCoreGraphicsCoordinateOfDisplay(C.CGMainDisplayID())
winBottomLeft := C.CGPointMake(C.CGFloat(x), C.CGFloat(y+height))
cgBottomLeft := getCoreGraphicsCoordinateFromWindowsCoordinate(winBottomLeft, cgMainDisplayBounds)
cgCaptureBounds := C.CGRectMake(cgBottomLeft.x, cgBottomLeft.y, C.CGFloat(width), C.CGFloat(height))
ids := activeDisplayList()
ctx := createBitmapContext(width, height, (*C.uint32_t)(unsafe.Pointer(&img.Pix[0])), img.Stride)
if ctx == 0 {
return nil, errors.New("cannot create bitmap context")
}
colorSpace := createColorspace()
if colorSpace == 0 {
return nil, errors.New("cannot create colorspace")
}
defer C.CGColorSpaceRelease(colorSpace)
for _, id := range ids {
cgBounds := getCoreGraphicsCoordinateOfDisplay(id)
cgIntersect := C.CGRectIntersection(cgBounds, cgCaptureBounds)
if C.CGRectIsNull(cgIntersect) {
continue
}
if cgIntersect.size.width <= 0 || cgIntersect.size.height <= 0 {
continue
}
// CGDisplayCreateImageForRect potentially fail in case width/height is odd number.
if int(cgIntersect.size.width)%2 != 0 {
cgIntersect.size.width = C.CGFloat(int(cgIntersect.size.width) + 1)
}
if int(cgIntersect.size.height)%2 != 0 {
cgIntersect.size.height = C.CGFloat(int(cgIntersect.size.height) + 1)
}
diIntersectDisplayLocal := C.CGRectMake(cgIntersect.origin.x-cgBounds.origin.x,
cgBounds.origin.y+cgBounds.size.height-(cgIntersect.origin.y+cgIntersect.size.height),
cgIntersect.size.width, cgIntersect.size.height)
image := C.capture(id, diIntersectDisplayLocal, colorSpace)
if unsafe.Pointer(image) == nil {
return nil, errors.New("cannot capture display")
}
defer C.CGImageRelease(image)
cgDrawRect := C.CGRectMake(cgIntersect.origin.x-cgCaptureBounds.origin.x, cgIntersect.origin.y-cgCaptureBounds.origin.y,
cgIntersect.size.width, cgIntersect.size.height)
C.CGContextDrawImage(ctx, cgDrawRect, image)
}
i := 0
for iy := 0; iy < height; iy++ {
j := i
for ix := 0; ix < width; ix++ {
// ARGB => RGBA, and set A to 255
img.Pix[j], img.Pix[j+1], img.Pix[j+2], img.Pix[j+3] = img.Pix[j+1], img.Pix[j+2], img.Pix[j+3], 255
j += 4
}
i += img.Stride
}
return img, nil
}
func NumActiveDisplays() int {
var count C.uint32_t = 0
if C.CGGetActiveDisplayList(0, nil, &count) == C.kCGErrorSuccess {
return int(count)
} else {
return 0
}
}
func GetDisplayBounds(displayIndex int) image.Rectangle {
id := getDisplayId(displayIndex)
main := C.CGMainDisplayID()
var rect image.Rectangle
bounds := getCoreGraphicsCoordinateOfDisplay(id)
rect.Min.X = int(bounds.origin.x)
if main == id {
rect.Min.Y = 0
} else {
mainBounds := getCoreGraphicsCoordinateOfDisplay(main)
mainHeight := mainBounds.size.height
rect.Min.Y = int(mainHeight - (bounds.origin.y + bounds.size.height))
}
rect.Max.X = rect.Min.X + int(bounds.size.width)
rect.Max.Y = rect.Min.Y + int(bounds.size.height)
return rect
}
func getDisplayId(displayIndex int) C.CGDirectDisplayID {
main := C.CGMainDisplayID()
if displayIndex == 0 {
return main
} else {
n := NumActiveDisplays()
ids := make([]C.CGDirectDisplayID, n)
if C.CGGetActiveDisplayList(C.uint32_t(n), (*C.CGDirectDisplayID)(unsafe.Pointer(&ids[0])), nil) != C.kCGErrorSuccess {
return 0
}
index := 0
for i := 0; i < n; i++ {
if ids[i] == main {
continue
}
index++
if index == displayIndex {
return ids[i]
}
}
}
return 0
}
func getCoreGraphicsCoordinateOfDisplay(id C.CGDirectDisplayID) C.CGRect {
main := C.CGDisplayBounds(C.CGMainDisplayID())
r := C.CGDisplayBounds(id)
return C.CGRectMake(r.origin.x, -r.origin.y-r.size.height+main.size.height,
r.size.width, r.size.height)
}
func getCoreGraphicsCoordinateFromWindowsCoordinate(p C.CGPoint, mainDisplayBounds C.CGRect) C.CGPoint {
return C.CGPointMake(p.x, mainDisplayBounds.size.height-p.y)
}
func createBitmapContext(width int, height int, data *C.uint32_t, bytesPerRow int) C.CGContextRef {
colorSpace := createColorspace()
if colorSpace == 0 {
return 0
}
defer C.CGColorSpaceRelease(colorSpace)
return C.CGBitmapContextCreate(unsafe.Pointer(data),
C.size_t(width),
C.size_t(height),
8, // bits per component
C.size_t(bytesPerRow),
colorSpace,
C.kCGImageAlphaNoneSkipFirst)
}
func createColorspace() C.CGColorSpaceRef {
return C.CGColorSpaceCreateWithName(C.kCGColorSpaceSRGB)
}
func activeDisplayList() []C.CGDirectDisplayID {
count := C.uint32_t(NumActiveDisplays())
ret := make([]C.CGDirectDisplayID, count)
if count > 0 && C.CGGetActiveDisplayList(count, (*C.CGDirectDisplayID)(unsafe.Pointer(&ret[0])), nil) == C.kCGErrorSuccess {
return ret
} else {
return make([]C.CGDirectDisplayID, 0)
}
}