// 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 //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") )