go-tg-screenshot-bot
251 строка · 7.7 Кб
1//go:build cgo && darwin
2
3package screenshot
4
5/*
6#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > MAC_OS_VERSION_14_4
7#cgo CFLAGS: -x objective-c
8#cgo LDFLAGS: -framework CoreGraphics -framework CoreFoundation -framework ScreenCaptureKit
9#include <ScreenCaptureKit/ScreenCaptureKit.h>
10#else
11#cgo LDFLAGS: -framework CoreGraphics -framework CoreFoundation
12#endif
13#include <CoreGraphics/CoreGraphics.h>
14
15static CGImageRef capture(CGDirectDisplayID id, CGRect diIntersectDisplayLocal, CGColorSpaceRef colorSpace) {
16#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > MAC_OS_VERSION_14_4
17dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
18__block CGImageRef result = nil;
19[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent* content, NSError* error) {
20@autoreleasepool {
21if (error) {
22dispatch_semaphore_signal(semaphore);
23return;
24}
25SCDisplay* target = nil;
26for (SCDisplay *display in content.displays) {
27if (display.displayID == id) {
28target = display;
29break;
30}
31}
32if (!target) {
33dispatch_semaphore_signal(semaphore);
34return;
35}
36SCContentFilter* filter = [[SCContentFilter alloc] initWithDisplay:target excludingWindows:@[]];
37SCStreamConfiguration* config = [[SCStreamConfiguration alloc] init];
38config.sourceRect = diIntersectDisplayLocal;
39config.width = diIntersectDisplayLocal.size.width;
40config.height = diIntersectDisplayLocal.size.height;
41[SCScreenshotManager captureImageWithFilter:filter
42configuration:config
43completionHandler:^(CGImageRef img, NSError* error) {
44if (!error) {
45result = CGImageCreateCopyWithColorSpace(img, colorSpace);
46}
47dispatch_semaphore_signal(semaphore);
48}];
49}
50}];
51dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
52dispatch_release(semaphore);
53return result;
54#else
55CGImageRef img = CGDisplayCreateImageForRect(id, diIntersectDisplayLocal);
56if (!img) {
57return nil;
58}
59CGImageRef copy = CGImageCreateCopyWithColorSpace(img, colorSpace);
60CGImageRelease(img);
61if (!copy) {
62return nil;
63}
64return copy;
65#endif
66}
67*/
68import "C"
69
70import (
71"errors"
72"image"
73"unsafe"
74)
75
76func Capture(x, y, width, height int) (*image.RGBA, error) {
77if width <= 0 || height <= 0 {
78return nil, errors.New("width or height should be > 0")
79}
80
81rect := image.Rect(0, 0, width, height)
82img, err := createImage(rect)
83if err != nil {
84return nil, err
85}
86
87// cg: CoreGraphics coordinate (origin: lower-left corner of primary display, x-axis: rightward, y-axis: upward)
88// win: Windows coordinate (origin: upper-left corner of primary display, x-axis: rightward, y-axis: downward)
89// di: Display local coordinate (origin: upper-left corner of the display, x-axis: rightward, y-axis: downward)
90
91cgMainDisplayBounds := getCoreGraphicsCoordinateOfDisplay(C.CGMainDisplayID())
92
93winBottomLeft := C.CGPointMake(C.CGFloat(x), C.CGFloat(y+height))
94cgBottomLeft := getCoreGraphicsCoordinateFromWindowsCoordinate(winBottomLeft, cgMainDisplayBounds)
95cgCaptureBounds := C.CGRectMake(cgBottomLeft.x, cgBottomLeft.y, C.CGFloat(width), C.CGFloat(height))
96
97ids := activeDisplayList()
98
99ctx := createBitmapContext(width, height, (*C.uint32_t)(unsafe.Pointer(&img.Pix[0])), img.Stride)
100if ctx == 0 {
101return nil, errors.New("cannot create bitmap context")
102}
103
104colorSpace := createColorspace()
105if colorSpace == 0 {
106return nil, errors.New("cannot create colorspace")
107}
108defer C.CGColorSpaceRelease(colorSpace)
109
110for _, id := range ids {
111cgBounds := getCoreGraphicsCoordinateOfDisplay(id)
112cgIntersect := C.CGRectIntersection(cgBounds, cgCaptureBounds)
113if C.CGRectIsNull(cgIntersect) {
114continue
115}
116if cgIntersect.size.width <= 0 || cgIntersect.size.height <= 0 {
117continue
118}
119
120// CGDisplayCreateImageForRect potentially fail in case width/height is odd number.
121if int(cgIntersect.size.width)%2 != 0 {
122cgIntersect.size.width = C.CGFloat(int(cgIntersect.size.width) + 1)
123}
124if int(cgIntersect.size.height)%2 != 0 {
125cgIntersect.size.height = C.CGFloat(int(cgIntersect.size.height) + 1)
126}
127
128diIntersectDisplayLocal := C.CGRectMake(cgIntersect.origin.x-cgBounds.origin.x,
129cgBounds.origin.y+cgBounds.size.height-(cgIntersect.origin.y+cgIntersect.size.height),
130cgIntersect.size.width, cgIntersect.size.height)
131
132image := C.capture(id, diIntersectDisplayLocal, colorSpace)
133if unsafe.Pointer(image) == nil {
134return nil, errors.New("cannot capture display")
135}
136defer C.CGImageRelease(image)
137
138cgDrawRect := C.CGRectMake(cgIntersect.origin.x-cgCaptureBounds.origin.x, cgIntersect.origin.y-cgCaptureBounds.origin.y,
139cgIntersect.size.width, cgIntersect.size.height)
140C.CGContextDrawImage(ctx, cgDrawRect, image)
141}
142
143i := 0
144for iy := 0; iy < height; iy++ {
145j := i
146for ix := 0; ix < width; ix++ {
147// ARGB => RGBA, and set A to 255
148img.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
149j += 4
150}
151i += img.Stride
152}
153
154return img, nil
155}
156
157func NumActiveDisplays() int {
158var count C.uint32_t = 0
159if C.CGGetActiveDisplayList(0, nil, &count) == C.kCGErrorSuccess {
160return int(count)
161} else {
162return 0
163}
164}
165
166func GetDisplayBounds(displayIndex int) image.Rectangle {
167id := getDisplayId(displayIndex)
168main := C.CGMainDisplayID()
169
170var rect image.Rectangle
171
172bounds := getCoreGraphicsCoordinateOfDisplay(id)
173rect.Min.X = int(bounds.origin.x)
174if main == id {
175rect.Min.Y = 0
176} else {
177mainBounds := getCoreGraphicsCoordinateOfDisplay(main)
178mainHeight := mainBounds.size.height
179rect.Min.Y = int(mainHeight - (bounds.origin.y + bounds.size.height))
180}
181rect.Max.X = rect.Min.X + int(bounds.size.width)
182rect.Max.Y = rect.Min.Y + int(bounds.size.height)
183
184return rect
185}
186
187func getDisplayId(displayIndex int) C.CGDirectDisplayID {
188main := C.CGMainDisplayID()
189if displayIndex == 0 {
190return main
191} else {
192n := NumActiveDisplays()
193ids := make([]C.CGDirectDisplayID, n)
194if C.CGGetActiveDisplayList(C.uint32_t(n), (*C.CGDirectDisplayID)(unsafe.Pointer(&ids[0])), nil) != C.kCGErrorSuccess {
195return 0
196}
197index := 0
198for i := 0; i < n; i++ {
199if ids[i] == main {
200continue
201}
202index++
203if index == displayIndex {
204return ids[i]
205}
206}
207}
208
209return 0
210}
211
212func getCoreGraphicsCoordinateOfDisplay(id C.CGDirectDisplayID) C.CGRect {
213main := C.CGDisplayBounds(C.CGMainDisplayID())
214r := C.CGDisplayBounds(id)
215return C.CGRectMake(r.origin.x, -r.origin.y-r.size.height+main.size.height,
216r.size.width, r.size.height)
217}
218
219func getCoreGraphicsCoordinateFromWindowsCoordinate(p C.CGPoint, mainDisplayBounds C.CGRect) C.CGPoint {
220return C.CGPointMake(p.x, mainDisplayBounds.size.height-p.y)
221}
222
223func createBitmapContext(width int, height int, data *C.uint32_t, bytesPerRow int) C.CGContextRef {
224colorSpace := createColorspace()
225if colorSpace == 0 {
226return 0
227}
228defer C.CGColorSpaceRelease(colorSpace)
229
230return C.CGBitmapContextCreate(unsafe.Pointer(data),
231C.size_t(width),
232C.size_t(height),
2338, // bits per component
234C.size_t(bytesPerRow),
235colorSpace,
236C.kCGImageAlphaNoneSkipFirst)
237}
238
239func createColorspace() C.CGColorSpaceRef {
240return C.CGColorSpaceCreateWithName(C.kCGColorSpaceSRGB)
241}
242
243func activeDisplayList() []C.CGDirectDisplayID {
244count := C.uint32_t(NumActiveDisplays())
245ret := make([]C.CGDirectDisplayID, count)
246if count > 0 && C.CGGetActiveDisplayList(count, (*C.CGDirectDisplayID)(unsafe.Pointer(&ret[0])), nil) == C.kCGErrorSuccess {
247return ret
248} else {
249return make([]C.CGDirectDisplayID, 0)
250}
251}
252