552 lines
16 KiB
Go
552 lines
16 KiB
Go
// Copyright 2021 The golang.design Initiative Authors.
|
|
// All rights reserved. Use of this source code is governed
|
|
// by a MIT license that can be found in the LICENSE file.
|
|
//
|
|
// Written by Changkun Ou <changkun.de>
|
|
|
|
//go:build windows
|
|
// +build windows
|
|
|
|
package clipboard
|
|
|
|
// Interacting with Clipboard on Windows:
|
|
// https://docs.microsoft.com/zh-cn/windows/win32/dataxchg/using-the-clipboard
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"reflect"
|
|
"runtime"
|
|
"syscall"
|
|
"time"
|
|
"unicode/utf16"
|
|
"unsafe"
|
|
|
|
"golang.org/x/image/bmp"
|
|
)
|
|
|
|
func initialize() error { return nil }
|
|
|
|
// readText reads the clipboard and returns the text data if presents.
|
|
// The caller is responsible for opening/closing the clipboard before
|
|
// calling this function.
|
|
func readText() (buf []byte, err error) {
|
|
hMem, _, err := getClipboardData.Call(cFmtUnicodeText)
|
|
if hMem == 0 {
|
|
return nil, err
|
|
}
|
|
p, _, err := gLock.Call(hMem)
|
|
if p == 0 {
|
|
return nil, err
|
|
}
|
|
defer gUnlock.Call(hMem)
|
|
|
|
// Find NUL terminator
|
|
n := 0
|
|
for ptr := unsafe.Pointer(p); *(*uint16)(ptr) != 0; n++ {
|
|
ptr = unsafe.Pointer(uintptr(ptr) +
|
|
unsafe.Sizeof(*((*uint16)(unsafe.Pointer(p)))))
|
|
}
|
|
|
|
var s []uint16
|
|
h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
|
|
h.Data = p
|
|
h.Len = n
|
|
h.Cap = n
|
|
return []byte(string(utf16.Decode(s))), nil
|
|
}
|
|
|
|
// writeText writes given data to the clipboard. It is the caller's
|
|
// responsibility for opening/closing the clipboard before calling
|
|
// this function.
|
|
func writeText(buf []byte) error {
|
|
r, _, err := emptyClipboard.Call()
|
|
if r == 0 {
|
|
return fmt.Errorf("failed to clear clipboard: %w", err)
|
|
}
|
|
|
|
// empty text, we are done here.
|
|
if len(buf) == 0 {
|
|
return nil
|
|
}
|
|
|
|
s, err := syscall.UTF16FromString(string(buf))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to convert given string: %w", err)
|
|
}
|
|
|
|
hMem, _, err := gAlloc.Call(gmemMoveable, uintptr(len(s)*int(unsafe.Sizeof(s[0]))))
|
|
if hMem == 0 {
|
|
return fmt.Errorf("failed to alloc global memory: %w", err)
|
|
}
|
|
|
|
p, _, err := gLock.Call(hMem)
|
|
if p == 0 {
|
|
return fmt.Errorf("failed to lock global memory: %w", err)
|
|
}
|
|
defer gUnlock.Call(hMem)
|
|
|
|
// no return value
|
|
memMove.Call(p, uintptr(unsafe.Pointer(&s[0])),
|
|
uintptr(len(s)*int(unsafe.Sizeof(s[0]))))
|
|
|
|
v, _, err := setClipboardData.Call(cFmtUnicodeText, hMem)
|
|
if v == 0 {
|
|
gFree.Call(hMem)
|
|
return fmt.Errorf("failed to set text to clipboard: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// readImage reads the clipboard and returns PNG encoded image data
|
|
// if presents. The caller is responsible for opening/closing the
|
|
// clipboard before calling this function.
|
|
func readImage() ([]byte, error) {
|
|
hMem, _, err := getClipboardData.Call(cFmtDIBV5)
|
|
if hMem == 0 {
|
|
// second chance to try FmtDIB
|
|
return readImageDib()
|
|
}
|
|
p, _, err := gLock.Call(hMem)
|
|
if p == 0 {
|
|
return nil, err
|
|
}
|
|
defer gUnlock.Call(hMem)
|
|
|
|
// inspect header information
|
|
info := (*bitmapV5Header)(unsafe.Pointer(p))
|
|
|
|
// maybe deal with other formats?
|
|
if info.BitCount != 32 {
|
|
return nil, errUnsupported
|
|
}
|
|
|
|
var data []byte
|
|
sh := (*reflect.SliceHeader)(unsafe.Pointer(&data))
|
|
sh.Data = uintptr(p)
|
|
sh.Cap = int(info.Size + 4*uint32(info.Width)*uint32(info.Height))
|
|
sh.Len = int(info.Size + 4*uint32(info.Width)*uint32(info.Height))
|
|
img := image.NewRGBA(image.Rect(0, 0, int(info.Width), int(info.Height)))
|
|
offset := int(info.Size)
|
|
stride := int(info.Width)
|
|
for y := 0; y < int(info.Height); y++ {
|
|
for x := 0; x < int(info.Width); x++ {
|
|
idx := offset + 4*(y*stride+x)
|
|
xhat := (x + int(info.Width)) % int(info.Width)
|
|
yhat := int(info.Height) - 1 - y
|
|
r := data[idx+2]
|
|
g := data[idx+1]
|
|
b := data[idx+0]
|
|
a := data[idx+3]
|
|
img.SetRGBA(xhat, yhat, color.RGBA{r, g, b, a})
|
|
}
|
|
}
|
|
// always use PNG encoding.
|
|
var buf bytes.Buffer
|
|
png.Encode(&buf, img)
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func readImageDib() ([]byte, error) {
|
|
const (
|
|
fileHeaderLen = 14
|
|
infoHeaderLen = 40
|
|
cFmtDIB = 8
|
|
)
|
|
|
|
hClipDat, _, err := getClipboardData.Call(cFmtDIB)
|
|
if err != nil {
|
|
return nil, errors.New("not dib format data: " + err.Error())
|
|
}
|
|
pMemBlk, _, err := gLock.Call(hClipDat)
|
|
if pMemBlk == 0 {
|
|
return nil, errors.New("failed to call global lock: " + err.Error())
|
|
}
|
|
defer gUnlock.Call(hClipDat)
|
|
|
|
bmpHeader := (*bitmapHeader)(unsafe.Pointer(pMemBlk))
|
|
dataSize := bmpHeader.SizeImage + fileHeaderLen + infoHeaderLen
|
|
|
|
if bmpHeader.SizeImage == 0 && bmpHeader.Compression == 0 {
|
|
iSizeImage := bmpHeader.Height * ((bmpHeader.Width*uint32(bmpHeader.BitCount)/8 + 3) &^ 3)
|
|
dataSize += iSizeImage
|
|
}
|
|
buf := new(bytes.Buffer)
|
|
binary.Write(buf, binary.LittleEndian, uint16('B')|(uint16('M')<<8))
|
|
binary.Write(buf, binary.LittleEndian, uint32(dataSize))
|
|
binary.Write(buf, binary.LittleEndian, uint32(0))
|
|
const sizeof_colorbar = 0
|
|
binary.Write(buf, binary.LittleEndian, uint32(fileHeaderLen+infoHeaderLen+sizeof_colorbar))
|
|
j := 0
|
|
for i := fileHeaderLen; i < int(dataSize); i++ {
|
|
binary.Write(buf, binary.BigEndian, *(*byte)(unsafe.Pointer(pMemBlk + uintptr(j))))
|
|
j++
|
|
}
|
|
return bmpToPng(buf)
|
|
}
|
|
|
|
func bmpToPng(bmpBuf *bytes.Buffer) (buf []byte, err error) {
|
|
var f bytes.Buffer
|
|
original_image, err := bmp.Decode(bmpBuf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = png.Encode(&f, original_image)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return f.Bytes(), nil
|
|
}
|
|
|
|
func writeImage(buf []byte) error {
|
|
r, _, err := emptyClipboard.Call()
|
|
if r == 0 {
|
|
return fmt.Errorf("failed to clear clipboard: %w", err)
|
|
}
|
|
|
|
// empty text, we are done here.
|
|
if len(buf) == 0 {
|
|
return nil
|
|
}
|
|
|
|
img, err := png.Decode(bytes.NewReader(buf))
|
|
if err != nil {
|
|
return fmt.Errorf("input bytes is not PNG encoded: %w", err)
|
|
}
|
|
|
|
offset := unsafe.Sizeof(bitmapV5Header{})
|
|
width := img.Bounds().Dx()
|
|
height := img.Bounds().Dy()
|
|
imageSize := 4 * width * height
|
|
|
|
data := make([]byte, int(offset)+imageSize)
|
|
for y := 0; y < height; y++ {
|
|
for x := 0; x < width; x++ {
|
|
idx := int(offset) + 4*(y*width+x)
|
|
r, g, b, a := img.At(x, height-1-y).RGBA()
|
|
data[idx+2] = uint8(r)
|
|
data[idx+1] = uint8(g)
|
|
data[idx+0] = uint8(b)
|
|
data[idx+3] = uint8(a)
|
|
}
|
|
}
|
|
|
|
info := bitmapV5Header{}
|
|
info.Size = uint32(offset)
|
|
info.Width = int32(width)
|
|
info.Height = int32(height)
|
|
info.Planes = 1
|
|
info.Compression = 0 // BI_RGB
|
|
info.SizeImage = uint32(4 * info.Width * info.Height)
|
|
info.RedMask = 0xff0000 // default mask
|
|
info.GreenMask = 0xff00
|
|
info.BlueMask = 0xff
|
|
info.AlphaMask = 0xff000000
|
|
info.BitCount = 32 // we only deal with 32 bpp at the moment.
|
|
// Use calibrated RGB values as Go's image/png assumes linear color space.
|
|
// Other options:
|
|
// - LCS_CALIBRATED_RGB = 0x00000000
|
|
// - LCS_sRGB = 0x73524742
|
|
// - LCS_WINDOWS_COLOR_SPACE = 0x57696E20
|
|
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/eb4bbd50-b3ce-4917-895c-be31f214797f
|
|
info.CSType = 0x73524742
|
|
// Use GL_IMAGES for GamutMappingIntent
|
|
// Other options:
|
|
// - LCS_GM_ABS_COLORIMETRIC = 0x00000008
|
|
// - LCS_GM_BUSINESS = 0x00000001
|
|
// - LCS_GM_GRAPHICS = 0x00000002
|
|
// - LCS_GM_IMAGES = 0x00000004
|
|
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/9fec0834-607d-427d-abd5-ab240fb0db38
|
|
info.Intent = 4 // LCS_GM_IMAGES
|
|
|
|
infob := make([]byte, int(unsafe.Sizeof(info)))
|
|
for i, v := range *(*[unsafe.Sizeof(info)]byte)(unsafe.Pointer(&info)) {
|
|
infob[i] = v
|
|
}
|
|
copy(data[:], infob[:])
|
|
|
|
hMem, _, err := gAlloc.Call(gmemMoveable,
|
|
uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
|
|
if hMem == 0 {
|
|
return fmt.Errorf("failed to alloc global memory: %w", err)
|
|
}
|
|
|
|
p, _, err := gLock.Call(hMem)
|
|
if p == 0 {
|
|
return fmt.Errorf("failed to lock global memory: %w", err)
|
|
}
|
|
defer gUnlock.Call(hMem)
|
|
|
|
memMove.Call(p, uintptr(unsafe.Pointer(&data[0])),
|
|
uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
|
|
|
|
v, _, err := setClipboardData.Call(cFmtDIBV5, hMem)
|
|
if v == 0 {
|
|
gFree.Call(hMem)
|
|
return fmt.Errorf("failed to set text to clipboard: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func read(t Format) (buf []byte, err error) {
|
|
// On Windows, OpenClipboard and CloseClipboard must be executed on
|
|
// the same thread. Thus, lock the OS thread for further execution.
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
var format uintptr
|
|
switch t {
|
|
case FmtImage:
|
|
format = cFmtDIBV5
|
|
case FmtText:
|
|
fallthrough
|
|
default:
|
|
format = cFmtUnicodeText
|
|
}
|
|
|
|
// check if clipboard is avaliable for the requested format
|
|
r, _, err := isClipboardFormatAvailable.Call(format)
|
|
if r == 0 {
|
|
return nil, errUnavailable
|
|
}
|
|
|
|
// try again until open clipboard successed
|
|
for {
|
|
r, _, _ = openClipboard.Call()
|
|
if r == 0 {
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
defer closeClipboard.Call()
|
|
|
|
switch format {
|
|
case cFmtDIBV5:
|
|
return readImage()
|
|
case cFmtUnicodeText:
|
|
fallthrough
|
|
default:
|
|
return readText()
|
|
}
|
|
}
|
|
|
|
// write writes the given data to clipboard and
|
|
// returns true if success or false if failed.
|
|
func write(t Format, buf []byte) (<-chan struct{}, error) {
|
|
errch := make(chan error)
|
|
changed := make(chan struct{}, 1)
|
|
go func() {
|
|
// make sure GetClipboardSequenceNumber happens with
|
|
// OpenClipboard on the same thread.
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
for {
|
|
r, _, _ := openClipboard.Call(0)
|
|
if r == 0 {
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
// var param uintptr
|
|
switch t {
|
|
case FmtImage:
|
|
err := writeImage(buf)
|
|
if err != nil {
|
|
errch <- err
|
|
closeClipboard.Call()
|
|
return
|
|
}
|
|
case FmtText:
|
|
fallthrough
|
|
default:
|
|
// param = cFmtUnicodeText
|
|
err := writeText(buf)
|
|
if err != nil {
|
|
errch <- err
|
|
closeClipboard.Call()
|
|
return
|
|
}
|
|
}
|
|
// Close the clipboard otherwise other applications cannot
|
|
// paste the data.
|
|
closeClipboard.Call()
|
|
|
|
cnt, _, _ := getClipboardSequenceNumber.Call()
|
|
errch <- nil
|
|
for {
|
|
time.Sleep(time.Second)
|
|
cur, _, _ := getClipboardSequenceNumber.Call()
|
|
if cur != cnt {
|
|
changed <- struct{}{}
|
|
close(changed)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
err := <-errch
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return changed, nil
|
|
}
|
|
|
|
func watch(ctx context.Context, t Format) <-chan []byte {
|
|
recv := make(chan []byte, 1)
|
|
ready := make(chan struct{})
|
|
go func() {
|
|
// not sure if we are too slow or the user too fast :)
|
|
ti := time.NewTicker(time.Second)
|
|
cnt, _, _ := getClipboardSequenceNumber.Call()
|
|
ready <- struct{}{}
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
close(recv)
|
|
return
|
|
case <-ti.C:
|
|
cur, _, _ := getClipboardSequenceNumber.Call()
|
|
if cnt != cur {
|
|
b := Read(t)
|
|
if b == nil {
|
|
continue
|
|
}
|
|
recv <- b
|
|
cnt = cur
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
<-ready
|
|
return recv
|
|
}
|
|
|
|
const (
|
|
cFmtBitmap = 2 // Win+PrintScreen
|
|
cFmtUnicodeText = 13
|
|
cFmtDIBV5 = 17
|
|
// Screenshot taken from special shortcut is in different format (why??), see:
|
|
// https://jpsoft.com/forums/threads/detecting-clipboard-format.5225/
|
|
cFmtDataObject = 49161 // Shift+Win+s, returned from enumClipboardFormats
|
|
gmemMoveable = 0x0002
|
|
)
|
|
|
|
// BITMAPV5Header structure, see:
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
|
|
type bitmapV5Header struct {
|
|
Size uint32
|
|
Width int32
|
|
Height int32
|
|
Planes uint16
|
|
BitCount uint16
|
|
Compression uint32
|
|
SizeImage uint32
|
|
XPelsPerMeter int32
|
|
YPelsPerMeter int32
|
|
ClrUsed uint32
|
|
ClrImportant uint32
|
|
RedMask uint32
|
|
GreenMask uint32
|
|
BlueMask uint32
|
|
AlphaMask uint32
|
|
CSType uint32
|
|
Endpoints struct {
|
|
CiexyzRed, CiexyzGreen, CiexyzBlue struct {
|
|
CiexyzX, CiexyzY, CiexyzZ int32 // FXPT2DOT30
|
|
}
|
|
}
|
|
GammaRed uint32
|
|
GammaGreen uint32
|
|
GammaBlue uint32
|
|
Intent uint32
|
|
ProfileData uint32
|
|
ProfileSize uint32
|
|
Reserved uint32
|
|
}
|
|
|
|
type bitmapHeader struct {
|
|
Size uint32
|
|
Width uint32
|
|
Height uint32
|
|
PLanes uint16
|
|
BitCount uint16
|
|
Compression uint32
|
|
SizeImage uint32
|
|
XPelsPerMeter uint32
|
|
YPelsPerMeter uint32
|
|
ClrUsed uint32
|
|
ClrImportant uint32
|
|
}
|
|
|
|
// Calling a Windows DLL, see:
|
|
// https://github.com/golang/go/wiki/WindowsDLLs
|
|
var (
|
|
user32 = syscall.MustLoadDLL("user32")
|
|
// Opens the clipboard for examination and prevents other
|
|
// applications from modifying the clipboard content.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-openclipboard
|
|
openClipboard = user32.MustFindProc("OpenClipboard")
|
|
// Closes the clipboard.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-closeclipboard
|
|
closeClipboard = user32.MustFindProc("CloseClipboard")
|
|
// Empties the clipboard and frees handles to data in the clipboard.
|
|
// The function then assigns ownership of the clipboard to the
|
|
// window that currently has the clipboard open.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-emptyclipboard
|
|
emptyClipboard = user32.MustFindProc("EmptyClipboard")
|
|
// Retrieves data from the clipboard in a specified format.
|
|
// The clipboard must have been opened previously.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboarddata
|
|
getClipboardData = user32.MustFindProc("GetClipboardData")
|
|
// Places data on the clipboard in a specified clipboard format.
|
|
// The window must be the current clipboard owner, and the
|
|
// application must have called the OpenClipboard function. (When
|
|
// responding to the WM_RENDERFORMAT message, the clipboard owner
|
|
// must not call OpenClipboard before calling SetClipboardData.)
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata
|
|
setClipboardData = user32.MustFindProc("SetClipboardData")
|
|
// Determines whether the clipboard contains data in the specified format.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-isclipboardformatavailable
|
|
isClipboardFormatAvailable = user32.MustFindProc("IsClipboardFormatAvailable")
|
|
// Clipboard data formats are stored in an ordered list. To perform
|
|
// an enumeration of clipboard data formats, you make a series of
|
|
// calls to the EnumClipboardFormats function. For each call, the
|
|
// format parameter specifies an available clipboard format, and the
|
|
// function returns the next available clipboard format.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-isclipboardformatavailable
|
|
enumClipboardFormats = user32.MustFindProc("EnumClipboardFormats")
|
|
// Retrieves the clipboard sequence number for the current window station.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboardsequencenumber
|
|
getClipboardSequenceNumber = user32.MustFindProc("GetClipboardSequenceNumber")
|
|
// Registers a new clipboard format. This format can then be used as
|
|
// a valid clipboard format.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformata
|
|
registerClipboardFormatA = user32.MustFindProc("RegisterClipboardFormatA")
|
|
|
|
kernel32 = syscall.NewLazyDLL("kernel32")
|
|
|
|
// Locks a global memory object and returns a pointer to the first
|
|
// byte of the object's memory block.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globallock
|
|
gLock = kernel32.NewProc("GlobalLock")
|
|
// Decrements the lock count associated with a memory object that was
|
|
// allocated with GMEM_MOVEABLE. This function has no effect on memory
|
|
// objects allocated with GMEM_FIXED.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalunlock
|
|
gUnlock = kernel32.NewProc("GlobalUnlock")
|
|
// Allocates the specified number of bytes from the heap.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalalloc
|
|
gAlloc = kernel32.NewProc("GlobalAlloc")
|
|
// Frees the specified global memory object and invalidates its handle.
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalfree
|
|
gFree = kernel32.NewProc("GlobalFree")
|
|
memMove = kernel32.NewProc("RtlMoveMemory")
|
|
)
|