232 lines
5.3 KiB
Go
232 lines
5.3 KiB
Go
//go:build windows
|
|||
|
|
|
||
|
|
package clipmon
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/binary"
|
||
|
|
"image"
|
||
|
|
"image/color"
|
||
|
|
"image/png"
|
||
|
|
"strings"
|
||
|
|
"syscall"
|
||
|
|
"time"
|
||
|
|
"unsafe"
|
||
|
|
)
|
||
|
|
|
||
|
|
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)
|
||
|
|
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
|
||
|
|
}
|