clipboard_windows.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. // Copyright 2021 The golang.design Initiative Authors.
  2. // All rights reserved. Use of this source code is governed
  3. // by a MIT license that can be found in the LICENSE file.
  4. //
  5. // Written by Changkun Ou <changkun.de>
  6. //go:build windows
  7. package clipboard
  8. // Interacting with Clipboard on Windows:
  9. // https://docs.microsoft.com/zh-cn/windows/win32/dataxchg/using-the-clipboard
  10. import (
  11. "bytes"
  12. "context"
  13. "encoding/binary"
  14. "errors"
  15. "fmt"
  16. "image"
  17. "image/color"
  18. "image/png"
  19. "reflect"
  20. "runtime"
  21. "syscall"
  22. "time"
  23. "unicode/utf16"
  24. "unsafe"
  25. "golang.org/x/image/bmp"
  26. )
  27. func initialize() error { return nil }
  28. // readText reads the clipboard and returns the text data if presents.
  29. // The caller is responsible for opening/closing the clipboard before
  30. // calling this function.
  31. func readText() (buf []byte, err error) {
  32. hMem, _, err := getClipboardData.Call(cFmtUnicodeText)
  33. if hMem == 0 {
  34. return nil, err
  35. }
  36. p, _, err := gLock.Call(hMem)
  37. if p == 0 {
  38. return nil, err
  39. }
  40. defer gUnlock.Call(hMem)
  41. // Find NUL terminator
  42. n := 0
  43. for ptr := unsafe.Pointer(p); *(*uint16)(ptr) != 0; n++ {
  44. ptr = unsafe.Pointer(uintptr(ptr) +
  45. unsafe.Sizeof(*((*uint16)(unsafe.Pointer(p)))))
  46. }
  47. var s []uint16
  48. h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
  49. h.Data = p
  50. h.Len = n
  51. h.Cap = n
  52. return []byte(string(utf16.Decode(s))), nil
  53. }
  54. // writeText writes given data to the clipboard. It is the caller's
  55. // responsibility for opening/closing the clipboard before calling
  56. // this function.
  57. func writeText(buf []byte) error {
  58. r, _, err := emptyClipboard.Call()
  59. if r == 0 {
  60. return fmt.Errorf("failed to clear clipboard: %w", err)
  61. }
  62. // empty text, we are done here.
  63. if len(buf) == 0 {
  64. return nil
  65. }
  66. s, err := syscall.UTF16FromString(string(buf))
  67. if err != nil {
  68. return fmt.Errorf("failed to convert given string: %w", err)
  69. }
  70. hMem, _, err := gAlloc.Call(gmemMoveable, uintptr(len(s)*int(unsafe.Sizeof(s[0]))))
  71. if hMem == 0 {
  72. return fmt.Errorf("failed to alloc global memory: %w", err)
  73. }
  74. p, _, err := gLock.Call(hMem)
  75. if p == 0 {
  76. return fmt.Errorf("failed to lock global memory: %w", err)
  77. }
  78. defer gUnlock.Call(hMem)
  79. // no return value
  80. memMove.Call(p, uintptr(unsafe.Pointer(&s[0])),
  81. uintptr(len(s)*int(unsafe.Sizeof(s[0]))))
  82. v, _, err := setClipboardData.Call(cFmtUnicodeText, hMem)
  83. if v == 0 {
  84. gFree.Call(hMem)
  85. return fmt.Errorf("failed to set text to clipboard: %w", err)
  86. }
  87. return nil
  88. }
  89. // readImage reads the clipboard and returns PNG encoded image data
  90. // if presents. The caller is responsible for opening/closing the
  91. // clipboard before calling this function.
  92. func readImage() ([]byte, error) {
  93. hMem, _, err := getClipboardData.Call(cFmtDIBV5)
  94. if hMem == 0 {
  95. // second chance to try FmtDIB
  96. return readImageDib()
  97. }
  98. p, _, err := gLock.Call(hMem)
  99. if p == 0 {
  100. return nil, err
  101. }
  102. defer gUnlock.Call(hMem)
  103. // inspect header information
  104. info := (*bitmapV5Header)(unsafe.Pointer(p))
  105. // maybe deal with other formats?
  106. if info.BitCount != 32 {
  107. return nil, errUnsupported
  108. }
  109. var data []byte
  110. sh := (*reflect.SliceHeader)(unsafe.Pointer(&data))
  111. sh.Data = uintptr(p)
  112. sh.Cap = int(info.Size + 4*uint32(info.Width)*uint32(info.Height))
  113. sh.Len = int(info.Size + 4*uint32(info.Width)*uint32(info.Height))
  114. img := image.NewRGBA(image.Rect(0, 0, int(info.Width), int(info.Height)))
  115. offset := int(info.Size)
  116. stride := int(info.Width)
  117. for y := 0; y < int(info.Height); y++ {
  118. for x := 0; x < int(info.Width); x++ {
  119. idx := offset + 4*(y*stride+x)
  120. xhat := (x + int(info.Width)) % int(info.Width)
  121. yhat := int(info.Height) - 1 - y
  122. r := data[idx+2]
  123. g := data[idx+1]
  124. b := data[idx+0]
  125. a := data[idx+3]
  126. img.SetRGBA(xhat, yhat, color.RGBA{r, g, b, a})
  127. }
  128. }
  129. // always use PNG encoding.
  130. var buf bytes.Buffer
  131. png.Encode(&buf, img)
  132. return buf.Bytes(), nil
  133. }
  134. func readImageDib() ([]byte, error) {
  135. const (
  136. fileHeaderLen = 14
  137. infoHeaderLen = 40
  138. cFmtDIB = 8
  139. )
  140. hClipDat, _, err := getClipboardData.Call(cFmtDIB)
  141. if err != nil {
  142. return nil, errors.New("not dib format data: " + err.Error())
  143. }
  144. pMemBlk, _, err := gLock.Call(hClipDat)
  145. if pMemBlk == 0 {
  146. return nil, errors.New("failed to call global lock: " + err.Error())
  147. }
  148. defer gUnlock.Call(hClipDat)
  149. bmpHeader := (*bitmapHeader)(unsafe.Pointer(pMemBlk))
  150. dataSize := bmpHeader.SizeImage + fileHeaderLen + infoHeaderLen
  151. if bmpHeader.SizeImage == 0 && bmpHeader.Compression == 0 {
  152. iSizeImage := bmpHeader.Height * ((bmpHeader.Width*uint32(bmpHeader.BitCount)/8 + 3) &^ 3)
  153. dataSize += iSizeImage
  154. }
  155. buf := new(bytes.Buffer)
  156. binary.Write(buf, binary.LittleEndian, uint16('B')|(uint16('M')<<8))
  157. binary.Write(buf, binary.LittleEndian, uint32(dataSize))
  158. binary.Write(buf, binary.LittleEndian, uint32(0))
  159. const sizeof_colorbar = 0
  160. binary.Write(buf, binary.LittleEndian, uint32(fileHeaderLen+infoHeaderLen+sizeof_colorbar))
  161. j := 0
  162. for i := fileHeaderLen; i < int(dataSize); i++ {
  163. binary.Write(buf, binary.BigEndian, *(*byte)(unsafe.Pointer(pMemBlk + uintptr(j))))
  164. j++
  165. }
  166. return bmpToPng(buf)
  167. }
  168. func bmpToPng(bmpBuf *bytes.Buffer) (buf []byte, err error) {
  169. var f bytes.Buffer
  170. original_image, err := bmp.Decode(bmpBuf)
  171. if err != nil {
  172. return nil, err
  173. }
  174. err = png.Encode(&f, original_image)
  175. if err != nil {
  176. return nil, err
  177. }
  178. return f.Bytes(), nil
  179. }
  180. func writeImage(buf []byte) error {
  181. r, _, err := emptyClipboard.Call()
  182. if r == 0 {
  183. return fmt.Errorf("failed to clear clipboard: %w", err)
  184. }
  185. // empty text, we are done here.
  186. if len(buf) == 0 {
  187. return nil
  188. }
  189. img, err := png.Decode(bytes.NewReader(buf))
  190. if err != nil {
  191. return fmt.Errorf("input bytes is not PNG encoded: %w", err)
  192. }
  193. offset := unsafe.Sizeof(bitmapV5Header{})
  194. width := img.Bounds().Dx()
  195. height := img.Bounds().Dy()
  196. imageSize := 4 * width * height
  197. data := make([]byte, int(offset)+imageSize)
  198. for y := 0; y < height; y++ {
  199. for x := 0; x < width; x++ {
  200. idx := int(offset) + 4*(y*width+x)
  201. r, g, b, a := img.At(x, height-1-y).RGBA()
  202. data[idx+2] = uint8(r)
  203. data[idx+1] = uint8(g)
  204. data[idx+0] = uint8(b)
  205. data[idx+3] = uint8(a)
  206. }
  207. }
  208. info := bitmapV5Header{}
  209. info.Size = uint32(offset)
  210. info.Width = int32(width)
  211. info.Height = int32(height)
  212. info.Planes = 1
  213. info.Compression = 0 // BI_RGB
  214. info.SizeImage = uint32(4 * info.Width * info.Height)
  215. info.RedMask = 0xff0000 // default mask
  216. info.GreenMask = 0xff00
  217. info.BlueMask = 0xff
  218. info.AlphaMask = 0xff000000
  219. info.BitCount = 32 // we only deal with 32 bpp at the moment.
  220. // Use calibrated RGB values as Go's image/png assumes linear color space.
  221. // Other options:
  222. // - LCS_CALIBRATED_RGB = 0x00000000
  223. // - LCS_sRGB = 0x73524742
  224. // - LCS_WINDOWS_COLOR_SPACE = 0x57696E20
  225. // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/eb4bbd50-b3ce-4917-895c-be31f214797f
  226. info.CSType = 0x73524742
  227. // Use GL_IMAGES for GamutMappingIntent
  228. // Other options:
  229. // - LCS_GM_ABS_COLORIMETRIC = 0x00000008
  230. // - LCS_GM_BUSINESS = 0x00000001
  231. // - LCS_GM_GRAPHICS = 0x00000002
  232. // - LCS_GM_IMAGES = 0x00000004
  233. // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/9fec0834-607d-427d-abd5-ab240fb0db38
  234. info.Intent = 4 // LCS_GM_IMAGES
  235. infob := make([]byte, int(unsafe.Sizeof(info)))
  236. for i, v := range *(*[unsafe.Sizeof(info)]byte)(unsafe.Pointer(&info)) {
  237. infob[i] = v
  238. }
  239. copy(data[:], infob[:])
  240. hMem, _, err := gAlloc.Call(gmemMoveable,
  241. uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
  242. if hMem == 0 {
  243. return fmt.Errorf("failed to alloc global memory: %w", err)
  244. }
  245. p, _, err := gLock.Call(hMem)
  246. if p == 0 {
  247. return fmt.Errorf("failed to lock global memory: %w", err)
  248. }
  249. defer gUnlock.Call(hMem)
  250. memMove.Call(p, uintptr(unsafe.Pointer(&data[0])),
  251. uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
  252. v, _, err := setClipboardData.Call(cFmtDIBV5, hMem)
  253. if v == 0 {
  254. gFree.Call(hMem)
  255. return fmt.Errorf("failed to set text to clipboard: %w", err)
  256. }
  257. return nil
  258. }
  259. func read(t Format) (buf []byte, err error) {
  260. // On Windows, OpenClipboard and CloseClipboard must be executed on
  261. // the same thread. Thus, lock the OS thread for further execution.
  262. runtime.LockOSThread()
  263. defer runtime.UnlockOSThread()
  264. var format uintptr
  265. switch t {
  266. case FmtImage:
  267. format = cFmtDIBV5
  268. case FmtText:
  269. fallthrough
  270. default:
  271. format = cFmtUnicodeText
  272. }
  273. // check if clipboard is available for the requested format
  274. r, _, err := isClipboardFormatAvailable.Call(format)
  275. if r == 0 {
  276. return nil, errUnavailable
  277. }
  278. // try again until open clipboard succeeds
  279. for {
  280. r, _, _ = openClipboard.Call()
  281. if r == 0 {
  282. continue
  283. }
  284. break
  285. }
  286. defer closeClipboard.Call()
  287. switch format {
  288. case cFmtDIBV5:
  289. return readImage()
  290. case cFmtUnicodeText:
  291. fallthrough
  292. default:
  293. return readText()
  294. }
  295. }
  296. // write writes the given data to clipboard and
  297. // returns true if success or false if failed.
  298. func write(t Format, buf []byte) (<-chan struct{}, error) {
  299. errch := make(chan error)
  300. changed := make(chan struct{}, 1)
  301. go func() {
  302. // make sure GetClipboardSequenceNumber happens with
  303. // OpenClipboard on the same thread.
  304. runtime.LockOSThread()
  305. defer runtime.UnlockOSThread()
  306. for {
  307. r, _, _ := openClipboard.Call(0)
  308. if r == 0 {
  309. continue
  310. }
  311. break
  312. }
  313. // var param uintptr
  314. switch t {
  315. case FmtImage:
  316. err := writeImage(buf)
  317. if err != nil {
  318. errch <- err
  319. closeClipboard.Call()
  320. return
  321. }
  322. case FmtText:
  323. fallthrough
  324. default:
  325. // param = cFmtUnicodeText
  326. err := writeText(buf)
  327. if err != nil {
  328. errch <- err
  329. closeClipboard.Call()
  330. return
  331. }
  332. }
  333. // Close the clipboard otherwise other applications cannot
  334. // paste the data.
  335. closeClipboard.Call()
  336. cnt, _, _ := getClipboardSequenceNumber.Call()
  337. errch <- nil
  338. for {
  339. time.Sleep(time.Second)
  340. cur, _, _ := getClipboardSequenceNumber.Call()
  341. if cur != cnt {
  342. changed <- struct{}{}
  343. close(changed)
  344. return
  345. }
  346. }
  347. }()
  348. err := <-errch
  349. if err != nil {
  350. return nil, err
  351. }
  352. return changed, nil
  353. }
  354. func watch(ctx context.Context, t Format) <-chan []byte {
  355. recv := make(chan []byte, 1)
  356. ready := make(chan struct{})
  357. go func() {
  358. // not sure if we are too slow or the user too fast :)
  359. ti := time.NewTicker(time.Second)
  360. cnt, _, _ := getClipboardSequenceNumber.Call()
  361. ready <- struct{}{}
  362. for {
  363. select {
  364. case <-ctx.Done():
  365. close(recv)
  366. return
  367. case <-ti.C:
  368. cur, _, _ := getClipboardSequenceNumber.Call()
  369. if cnt != cur {
  370. b := Read(t)
  371. if b == nil {
  372. continue
  373. }
  374. recv <- b
  375. cnt = cur
  376. }
  377. }
  378. }
  379. }()
  380. <-ready
  381. return recv
  382. }
  383. const (
  384. cFmtBitmap = 2 // Win+PrintScreen
  385. cFmtUnicodeText = 13
  386. cFmtDIBV5 = 17
  387. // Screenshot taken from special shortcut is in different format (why??), see:
  388. // https://jpsoft.com/forums/threads/detecting-clipboard-format.5225/
  389. cFmtDataObject = 49161 // Shift+Win+s, returned from enumClipboardFormats
  390. gmemMoveable = 0x0002
  391. )
  392. // BITMAPV5Header structure, see:
  393. // https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
  394. type bitmapV5Header struct {
  395. Size uint32
  396. Width int32
  397. Height int32
  398. Planes uint16
  399. BitCount uint16
  400. Compression uint32
  401. SizeImage uint32
  402. XPelsPerMeter int32
  403. YPelsPerMeter int32
  404. ClrUsed uint32
  405. ClrImportant uint32
  406. RedMask uint32
  407. GreenMask uint32
  408. BlueMask uint32
  409. AlphaMask uint32
  410. CSType uint32
  411. Endpoints struct {
  412. CiexyzRed, CiexyzGreen, CiexyzBlue struct {
  413. CiexyzX, CiexyzY, CiexyzZ int32 // FXPT2DOT30
  414. }
  415. }
  416. GammaRed uint32
  417. GammaGreen uint32
  418. GammaBlue uint32
  419. Intent uint32
  420. ProfileData uint32
  421. ProfileSize uint32
  422. Reserved uint32
  423. }
  424. type bitmapHeader struct {
  425. Size uint32
  426. Width uint32
  427. Height uint32
  428. PLanes uint16
  429. BitCount uint16
  430. Compression uint32
  431. SizeImage uint32
  432. XPelsPerMeter uint32
  433. YPelsPerMeter uint32
  434. ClrUsed uint32
  435. ClrImportant uint32
  436. }
  437. // Calling a Windows DLL, see:
  438. // https://github.com/golang/go/wiki/WindowsDLLs
  439. var (
  440. user32 = syscall.MustLoadDLL("user32")
  441. // Opens the clipboard for examination and prevents other
  442. // applications from modifying the clipboard content.
  443. // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-openclipboard
  444. openClipboard = user32.MustFindProc("OpenClipboard")
  445. // Closes the clipboard.
  446. // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-closeclipboard
  447. closeClipboard = user32.MustFindProc("CloseClipboard")
  448. // Empties the clipboard and frees handles to data in the clipboard.
  449. // The function then assigns ownership of the clipboard to the
  450. // window that currently has the clipboard open.
  451. // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-emptyclipboard
  452. emptyClipboard = user32.MustFindProc("EmptyClipboard")
  453. // Retrieves data from the clipboard in a specified format.
  454. // The clipboard must have been opened previously.
  455. // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboarddata
  456. getClipboardData = user32.MustFindProc("GetClipboardData")
  457. // Places data on the clipboard in a specified clipboard format.
  458. // The window must be the current clipboard owner, and the
  459. // application must have called the OpenClipboard function. (When
  460. // responding to the WM_RENDERFORMAT message, the clipboard owner
  461. // must not call OpenClipboard before calling SetClipboardData.)
  462. // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata
  463. setClipboardData = user32.MustFindProc("SetClipboardData")
  464. // Determines whether the clipboard contains data in the specified format.
  465. // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-isclipboardformatavailable
  466. isClipboardFormatAvailable = user32.MustFindProc("IsClipboardFormatAvailable")
  467. // Clipboard data formats are stored in an ordered list. To perform
  468. // an enumeration of clipboard data formats, you make a series of
  469. // calls to the EnumClipboardFormats function. For each call, the
  470. // format parameter specifies an available clipboard format, and the
  471. // function returns the next available clipboard format.
  472. // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-isclipboardformatavailable
  473. enumClipboardFormats = user32.MustFindProc("EnumClipboardFormats")
  474. // Retrieves the clipboard sequence number for the current window station.
  475. // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboardsequencenumber
  476. getClipboardSequenceNumber = user32.MustFindProc("GetClipboardSequenceNumber")
  477. // Registers a new clipboard format. This format can then be used as
  478. // a valid clipboard format.
  479. // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformata
  480. registerClipboardFormatA = user32.MustFindProc("RegisterClipboardFormatA")
  481. kernel32 = syscall.NewLazyDLL("kernel32")
  482. // Locks a global memory object and returns a pointer to the first
  483. // byte of the object's memory block.
  484. // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globallock
  485. gLock = kernel32.NewProc("GlobalLock")
  486. // Decrements the lock count associated with a memory object that was
  487. // allocated with GMEM_MOVEABLE. This function has no effect on memory
  488. // objects allocated with GMEM_FIXED.
  489. // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalunlock
  490. gUnlock = kernel32.NewProc("GlobalUnlock")
  491. // Allocates the specified number of bytes from the heap.
  492. // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalalloc
  493. gAlloc = kernel32.NewProc("GlobalAlloc")
  494. // Frees the specified global memory object and invalidates its handle.
  495. // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalfree
  496. gFree = kernel32.NewProc("GlobalFree")
  497. memMove = kernel32.NewProc("RtlMoveMemory")
  498. )