//go:build windows package screenshot import ( "errors" "github.com/lxn/win" "image" "syscall" "unsafe" ) var ( libUser32, _ = syscall.LoadLibrary("user32.dll") funcGetDesktopWindow, _ = syscall.GetProcAddress(syscall.Handle(libUser32), "GetDesktopWindow") funcEnumDisplayMonitors, _ = syscall.GetProcAddress(syscall.Handle(libUser32), "EnumDisplayMonitors") funcGetMonitorInfo, _ = syscall.GetProcAddress(syscall.Handle(libUser32), "GetMonitorInfoW") funcEnumDisplaySettings, _ = syscall.GetProcAddress(syscall.Handle(libUser32), "EnumDisplaySettingsW") ) func Capture(x, y, width, height int) (*image.RGBA, error) { rect := image.Rect(0, 0, width, height) img, err := createImage(rect) if err != nil { return nil, err } hwnd := getDesktopWindow() hdc := win.GetDC(hwnd) if hdc == 0 { return nil, errors.New("GetDC failed") } defer win.ReleaseDC(hwnd, hdc) memory_device := win.CreateCompatibleDC(hdc) if memory_device == 0 { return nil, errors.New("CreateCompatibleDC failed") } defer win.DeleteDC(memory_device) bitmap := win.CreateCompatibleBitmap(hdc, int32(width), int32(height)) if bitmap == 0 { return nil, errors.New("CreateCompatibleBitmap failed") } defer win.DeleteObject(win.HGDIOBJ(bitmap)) var header win.BITMAPINFOHEADER header.BiSize = uint32(unsafe.Sizeof(header)) header.BiPlanes = 1 header.BiBitCount = 32 header.BiWidth = int32(width) header.BiHeight = int32(-height) header.BiCompression = win.BI_RGB header.BiSizeImage = 0 // GetDIBits balks at using Go memory on some systems. The MSDN example uses // GlobalAlloc, so we'll do that too. See: // https://docs.microsoft.com/en-gb/windows/desktop/gdi/capturing-an-image bitmapDataSize := uintptr(((int64(width)*int64(header.BiBitCount) + 31) / 32) * 4 * int64(height)) hmem := win.GlobalAlloc(win.GMEM_MOVEABLE, bitmapDataSize) defer win.GlobalFree(hmem) memptr := win.GlobalLock(hmem) defer win.GlobalUnlock(hmem) old := win.SelectObject(memory_device, win.HGDIOBJ(bitmap)) if old == 0 { return nil, errors.New("SelectObject failed") } defer win.SelectObject(memory_device, old) if !win.BitBlt(memory_device, 0, 0, int32(width), int32(height), hdc, int32(x), int32(y), win.SRCCOPY) { return nil, errors.New("BitBlt failed") } if win.GetDIBits(hdc, bitmap, 0, uint32(height), (*uint8)(memptr), (*win.BITMAPINFO)(unsafe.Pointer(&header)), win.DIB_RGB_COLORS) == 0 { return nil, errors.New("GetDIBits failed") } i := 0 src := uintptr(memptr) for y := 0; y < height; y++ { for x := 0; x < width; x++ { v0 := *(*uint8)(unsafe.Pointer(src)) v1 := *(*uint8)(unsafe.Pointer(src + 1)) v2 := *(*uint8)(unsafe.Pointer(src + 2)) // BGRA => RGBA, and set A to 255 img.Pix[i], img.Pix[i+1], img.Pix[i+2], img.Pix[i+3] = v2, v1, v0, 255 i += 4 src += 4 } } return img, nil } func getDesktopWindow() win.HWND { ret, _, _ := syscall.Syscall(funcGetDesktopWindow, 0, 0, 0, 0) return win.HWND(ret) } func enumDisplayMonitors(hdc win.HDC, lprcClip *win.RECT, lpfnEnum uintptr, dwData uintptr) bool { ret, _, _ := syscall.Syscall6(funcEnumDisplayMonitors, 4, uintptr(hdc), uintptr(unsafe.Pointer(lprcClip)), lpfnEnum, dwData, 0, 0) return int(ret) != 0 } func countupMonitorCallback(hMonitor win.HMONITOR, hdcMonitor win.HDC, lprcMonitor *win.RECT, dwData uintptr) uintptr { var count *int count = (*int)(unsafe.Pointer(dwData)) *count = *count + 1 return uintptr(1) } type getMonitorBoundsContext struct { Index int Rect win.RECT Count int } func getMonitorBoundsCallback(hMonitor win.HMONITOR, hdcMonitor win.HDC, lprcMonitor *win.RECT, dwData uintptr) uintptr { var ctx *getMonitorBoundsContext ctx = (*getMonitorBoundsContext)(unsafe.Pointer(dwData)) if ctx.Count != ctx.Index { ctx.Count = ctx.Count + 1 return uintptr(1) } if realSize := getMonitorRealSize(hMonitor); realSize != nil { ctx.Rect = *realSize } else { ctx.Rect = *lprcMonitor } return uintptr(0) } type _MONITORINFOEX struct { win.MONITORINFO DeviceName [win.CCHDEVICENAME]uint16 } const _ENUM_CURRENT_SETTINGS = 0xFFFFFFFF type _DEVMODE struct { _ [68]byte DmSize uint16 _ [6]byte DmPosition win.POINT _ [86]byte DmPelsWidth uint32 DmPelsHeight uint32 _ [40]byte } // getMonitorRealSize makes a call to GetMonitorInfo // to obtain the device name for the monitor handle // provided to the method. // // With the device name, EnumDisplaySettings is called to // obtain the current configuration for the monitor, this // information includes the real resolution of the monitor // rather than the scaled version based on DPI. // // If either handle calls fail, it will return a nil // allowing the calling method to use the bounds information // returned by EnumDisplayMonitors which may be affected // by DPI. func getMonitorRealSize(hMonitor win.HMONITOR) *win.RECT { info := _MONITORINFOEX{} info.CbSize = uint32(unsafe.Sizeof(info)) ret, _, _ := syscall.Syscall(funcGetMonitorInfo, 2, uintptr(hMonitor), uintptr(unsafe.Pointer(&info)), 0) if ret == 0 { return nil } devMode := _DEVMODE{} devMode.DmSize = uint16(unsafe.Sizeof(devMode)) if ret, _, _ := syscall.Syscall(funcEnumDisplaySettings, 3, uintptr(unsafe.Pointer(&info.DeviceName[0])), _ENUM_CURRENT_SETTINGS, uintptr(unsafe.Pointer(&devMode))); ret == 0 { return nil } return &win.RECT{ Left: devMode.DmPosition.X, Right: devMode.DmPosition.X + int32(devMode.DmPelsWidth), Top: devMode.DmPosition.Y, Bottom: devMode.DmPosition.Y + int32(devMode.DmPelsHeight), } }