252 lines
7.7 KiB
Go
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)
|
|
}
|
|
}
|