| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- // 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
- 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 available for the requested format
- r, _, err := isClipboardFormatAvailable.Call(format)
- if r == 0 {
- return nil, errUnavailable
- }
- // try again until open clipboard succeeds
- 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")
- )
|