diff --git a/clipboard.go b/clipboard.go index daee37e..6f34283 100644 --- a/clipboard.go +++ b/clipboard.go @@ -1,6 +1,7 @@ package clipboard import ( + "errors" "strings" "time" ) @@ -21,13 +22,33 @@ type Clipboard struct { secondaryOriType string secondaryType FileType secondaryData []byte + secondarySize int primaryOriType string primaryType FileType primaryData []byte + primarySize int hash string } +func Set(types FileType, data []byte) error { + switch types { + case Text: + return AutoSetter("CF_UNICODETEXT", data) + case File: + return AutoSetter("CF_HDROP", data) + case Image: + return AutoSetter("PNG", data) + case HTML: + return AutoSetter("HTML Format", data) + } + return errors.New("not support type:" + string(types)) +} + +func SetOrigin(types string, data []byte) error { + return AutoSetter(types, data) +} + func (c *Clipboard) PrimaryType() FileType { return c.primaryType } @@ -57,6 +78,16 @@ func (c *Clipboard) Text() string { return "" } +func (c *Clipboard) TextSize() int { + if c.primaryType == Text { + return c.primarySize + } + if c.secondaryType == Text { + return c.secondarySize + } + return 0 +} + func (c *Clipboard) IsHTML() bool { return (c.primaryType == HTML || c.secondaryType == HTML) || c.IsText() } @@ -87,6 +118,10 @@ func (c *Clipboard) FilePaths() []string { return nil } +func (c *Clipboard) IsFile() bool { + return c.primaryType == File || c.secondaryType == File +} + func (c *Clipboard) FirstFilePath() string { if c.primaryType == File { return strings.Split(string(c.primaryData), "|")[0] @@ -107,6 +142,25 @@ func (c *Clipboard) Image() []byte { return nil } +func (c *Clipboard) ImageSize() int { + if c.primaryType == Image { + return c.primarySize + } + if c.secondaryType == Image { + return c.secondarySize + } + return 0 + +} + func (c *Clipboard) IsImage() bool { return c.primaryType == Image || c.secondaryType == Image } + +func (c *Clipboard) PrimaryTypeSize() int { + return c.primarySize +} + +func (c *Clipboard) SecondaryTypeSize() int { + return c.secondarySize +} diff --git a/clipboard_test.go b/clipboard_test.go index d91874d..78779af 100644 --- a/clipboard_test.go +++ b/clipboard_test.go @@ -1,13 +1,18 @@ package clipboard import ( + "b612.me/win32api" + "encoding/binary" "fmt" + "os" + "syscall" "testing" "time" ) func TestGet(t *testing.T) { - lsn, err := Listen() + fmt.Println(win32api.RegisterClipboardFormat("Preferred DropEffect")) + lsn, err := Listen(false) if err != nil { t.Fatal(err) } @@ -15,9 +20,17 @@ func TestGet(t *testing.T) { select { case cb := <-lsn: fmt.Println(cb.plateform) + fmt.Println(cb.winOriginTypes) fmt.Println(cb.AvailableTypes()) - fmt.Println(cb.Text()) - fmt.Println(cb.HTML()) + if cb.IsText() { + fmt.Println(cb.Text()) + } + if cb.IsHTML() { + fmt.Println(cb.HTML()) + } + if cb.IsFile() { + fmt.Println(cb.FilePaths()) + } case <-time.After(60 * time.Second): fmt.Println("not get clipboard data in 60s") StopListen() @@ -26,3 +39,60 @@ func TestGet(t *testing.T) { } } } + +func TestGetMeta(t *testing.T) { + fmt.Println(win32api.RegisterClipboardFormat("Preferred DropEffect")) + lsn, err := Listen(true) + if err != nil { + t.Fatal(err) + } + for { + select { + case cb := <-lsn: + fmt.Println(cb.plateform) + fmt.Println(cb.winOriginTypes) + fmt.Println(cb.AvailableTypes()) + fmt.Println(cb.primarySize) + fmt.Println(cb.secondarySize) + case <-time.After(60 * time.Second): + fmt.Println("not get clipboard data in 60s") + StopListen() + time.Sleep(time.Second * 15) + return + } + } +} + +func TestAutoSetter(t *testing.T) { + //samp := "天狼星、测试,123.hello.love.what??" + /* + err := AutoSetter("File", []string{"C:\\Users\\Starainrt\\Desktop\\haruhi.jpg"}) + if err != nil { + t.Fatal(err) + } + */ + f, err := os.ReadFile("C:\\Users\\Starainrt\\Desktop\\60.png") + if err != nil { + t.Fatal(err) + } + err = AutoSetter("Image", f) + if err != nil { + t.Fatal(err) + } +} + +func TestSetTextOrigin(t *testing.T) { + samp := "天狼星、测试,123.hello.love.what" + u, err := syscall.UTF16FromString(samp) + if err != nil { + t.Fatal(err) + } + b := make([]byte, 2*len(u)) + for i, v := range u { + binary.LittleEndian.PutUint16(b[i*2:], v) + } + err = setClipboardData(win32api.CF_UNICODETEXT, b, nil) + if err != nil { + t.Fatal(err) + } +} diff --git a/clipboard_windows.go b/clipboard_windows.go index 10d03c5..7fd94db 100644 --- a/clipboard_windows.go +++ b/clipboard_windows.go @@ -42,12 +42,17 @@ var formatRank = map[string]int{ } func Get() (Clipboard, error) { - return innerGetClipboard() + return innerGetClipboard(true) } -func innerGetClipboard() (Clipboard, error) { +func GetMeta() (Clipboard, error) { + return innerGetClipboard(false) +} + +func innerGetClipboard(withFetch bool) (Clipboard, error) { runtime.LockOSThread() defer runtime.UnlockOSThread() + var tmpData interface{} var c = Clipboard{ plateform: "windows", } @@ -107,10 +112,20 @@ func innerGetClipboard() (Clipboard, error) { case "CF_HDROP": c.primaryType = File } - c.primaryData, err = AutoFetcher(firstFormatName) - if err != nil { - return c, fmt.Errorf("AutoFetcher error: %v", err) + if withFetch { + tmpData, err = AutoFetcher(firstFormatName) + if err != nil { + return c, fmt.Errorf("AutoFetcher error: %v", err) + } + c.primaryData = tmpData.([]byte) + c.primarySize = len(c.primaryData) + } else { + c.primarySize, err = ClipSize(firstFormatName) + if err != nil { + return c, fmt.Errorf("ClipSize error: %v", err) + } } + if secondFormatName != "" { switch secondFormatName { case "CF_UNICODETEXT": @@ -123,7 +138,19 @@ func innerGetClipboard() (Clipboard, error) { c.secondaryType = File } c.secondaryOriType = secondFormatName - c.secondaryData, err = AutoFetcher(secondFormatName) + if withFetch { + tmpData, err = AutoFetcher(secondFormatName) + if err != nil { + return c, fmt.Errorf("AutoFetcher error: %v", err) + } + c.secondaryData = tmpData.([]byte) + c.secondarySize = len(c.secondaryData) + } else { + c.secondarySize, err = ClipSize(secondFormatName) + if err != nil { + return c, fmt.Errorf("ClipSize error: %v", err) + } + } if err != nil { return c, fmt.Errorf("AutoFetcher error: %v", err) } diff --git a/go.mod b/go.mod index 5bd4269..a226e3f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module b612.me/clipboard go 1.21.2 require ( - b612.me/win32api v0.0.0-20240328010943-f10bafb4e804 + b612.me/win32api v0.0.0-20240402021613-0959dfb96afa golang.org/x/image v0.15.0 ) diff --git a/go.sum b/go.sum index fa555c4..8ce4fb1 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ -b612.me/win32api v0.0.0-20240326080749-ad19f5cd4247 h1:fDTZ1HzVtVpEcXqlsQB9O2AbtrbqiAruaRX1Yd7M9Z8= -b612.me/win32api v0.0.0-20240326080749-ad19f5cd4247/go.mod h1:sj66sFJDKElEjOR+0YhdSW6b4kq4jsXu4T5/Hnpyot0= -b612.me/win32api v0.0.0-20240328010943-f10bafb4e804 h1:eLeVqlAmljdycU1cP7svO8cY7vklan6mAuSR/BcfHMs= -b612.me/win32api v0.0.0-20240328010943-f10bafb4e804/go.mod h1:sj66sFJDKElEjOR+0YhdSW6b4kq4jsXu4T5/Hnpyot0= +b612.me/win32api v0.0.0-20240402021613-0959dfb96afa h1:BsFIbLbjQqq9Yuh+eWs7JmmXcw2RKerP1NT7X8+GKR4= +b612.me/win32api v0.0.0-20240402021613-0959dfb96afa/go.mod h1:sj66sFJDKElEjOR+0YhdSW6b4kq4jsXu4T5/Hnpyot0= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= diff --git a/listener_windows.go b/listener_windows.go index cc2f8bd..a88a805 100644 --- a/listener_windows.go +++ b/listener_windows.go @@ -102,7 +102,7 @@ func StopListen() error { return nil } -func Listen() (<-chan Clipboard, error) { +func Listen(onlyMeta bool) (<-chan Clipboard, error) { if atomic.LoadUint32(&isListening) != 0 { return nil, errors.New("Already listening") } @@ -127,13 +127,24 @@ func Listen() (<-chan Clipboard, error) { storeSeq = seq //fmt.Println("Clipboard updated", seq, storeSeq) if atomic.LoadUint32(&isListening) == 1 { - cb, err := Get() - if err != nil { - continue - } - if atomic.LoadUint32(&isListening) == 1 { - res <- cb - continue + if !onlyMeta { + cb, err := Get() + if err != nil { + continue + } + if atomic.LoadUint32(&isListening) == 1 { + res <- cb + continue + } + } else { + cb, err := GetMeta() + if err != nil { + continue + } + if atomic.LoadUint32(&isListening) == 1 { + res <- cb + continue + } } } } diff --git a/readhandle_windows.go b/readhandle_windows.go index 79ff3b4..6519c47 100644 --- a/readhandle_windows.go +++ b/readhandle_windows.go @@ -16,7 +16,7 @@ import ( "unsafe" ) -func fetchClipboardData(uFormat win32api.DWORD, fn func(p unsafe.Pointer, size uint32) ([]byte, error)) ([]byte, error) { +func fetchClipboardData(uFormat win32api.DWORD, fn func(p unsafe.Pointer, size uint32) (interface{}, error)) (interface{}, error) { mem, err := win32api.GetClipboardData(uFormat) if err != nil { return nil, fmt.Errorf("GetClipboardData failed: %v", err) @@ -36,7 +36,7 @@ func fetchClipboardData(uFormat win32api.DWORD, fn func(p unsafe.Pointer, size u return fn(p, uint32(size)) } -func defaultFetchFn(p unsafe.Pointer, size uint32) ([]byte, error) { +func defaultFetchFn(p unsafe.Pointer, size uint32) (interface{}, error) { var buf []byte for i := 0; i < int(size); i++ { buf = append(buf, *(*byte)(unsafe.Pointer(uintptr(p) + uintptr(i)))) @@ -44,7 +44,7 @@ func defaultFetchFn(p unsafe.Pointer, size uint32) ([]byte, error) { return buf, nil } -func AutoFetcher(uFormat string) ([]byte, error) { +func AutoFetcher(uFormat string) (interface{}, error) { switch uFormat { case "CF_TEXT", "CF_UNICODETEXT": return fetchClipboardData(win32api.CF_UNICODETEXT, textFetcher) @@ -62,7 +62,17 @@ func AutoFetcher(uFormat string) ([]byte, error) { return nil, errors.New("not support uFormat:" + uFormat) } -func textFetcher(p unsafe.Pointer, size uint32) ([]byte, error) { +func ClipSize(uFormat string) (int, error) { + s, err := fetchClipboardData(getFormat(uFormat), func(p unsafe.Pointer, size uint32) (interface{}, error) { + return int(size), nil + }) + if err != nil { + return 0, err + } + return s.(int), nil +} + +func textFetcher(p unsafe.Pointer, size uint32) (interface{}, error) { var buf []uint16 for ptr := unsafe.Pointer(p); *(*uint16)(ptr) != 0; ptr = unsafe.Pointer(uintptr(ptr) + unsafe.Sizeof(*((*uint16)(unsafe.Pointer(p))))) { buf = append(buf, *(*uint16)(ptr)) @@ -70,7 +80,7 @@ func textFetcher(p unsafe.Pointer, size uint32) ([]byte, error) { return []byte(syscall.UTF16ToString(buf)), nil } -func filedropFetcher(p unsafe.Pointer, size uint32) ([]byte, error) { +func filedropFetcher(p unsafe.Pointer, size uint32) (interface{}, error) { var res []string c, err := win32api.DragQueryFile(win32api.HDROP(p), 0xFFFFFFFF, nil, 0) if err != nil { @@ -93,7 +103,7 @@ func filedropFetcher(p unsafe.Pointer, size uint32) ([]byte, error) { return []byte(strings.Join(res, "|")), nil } -func cfDIBv5Fetcher(p unsafe.Pointer, size uint32) ([]byte, error) { +func cfDIBv5Fetcher(p unsafe.Pointer, size uint32) (interface{}, error) { // inspect header information info := (*bitmapV5Header)(unsafe.Pointer(p)) // maybe deal with other formats? @@ -130,7 +140,7 @@ func cfDIBv5Fetcher(p unsafe.Pointer, size uint32) ([]byte, error) { return buf.Bytes(), nil } -func cfDIBFetcher(p unsafe.Pointer, size uint32) ([]byte, error) { +func cfDIBFetcher(p unsafe.Pointer, size uint32) (interface{}, error) { // 函数意外报错,待修正 const ( fileHeaderLen = 14 diff --git a/writehandle_windows.go b/writehandle_windows.go index 0b1389c..26a5a25 100644 --- a/writehandle_windows.go +++ b/writehandle_windows.go @@ -1 +1,304 @@ package clipboard + +import ( + "b612.me/win32api" + "bytes" + "fmt" + "image/png" + "runtime" + "syscall" + "unsafe" +) + +func allocNewMem(size uintptr) (win32api.HGLOBAL, unsafe.Pointer, error) { + mem, err := win32api.GlobalAlloc(win32api.GMEM_MOVEABLE, size) + if err != nil { + return 0, nil, fmt.Errorf("GlobalAlloc failed: %v", err) + } + p, err := win32api.GlobalLock(mem) + if err != nil { + return 0, nil, fmt.Errorf("GlobalLock failed: %v", err) + } + return 0, p, nil +} + +var winformatrev = map[string]win32api.DWORD{ + "CF_TEXT": 1, + "CF_BITMAP": 2, + "CF_METAFILEPICT": 3, + "CF_SYLK": 4, + "CF_DIF": 5, + "CF_TIFF": 6, + "CF_OEMTEXT": 7, + "CF_DIB": 8, + "CF_PALETTE": 9, + "CF_PENDATA": 10, + "CF_RIFF": 11, + "CF_WAVE": 12, + "CF_UNICODETEXT": 13, + "CF_ENHMETAFILE": 14, + "CF_HDROP": 15, + "CF_LOCALE": 16, + "CF_DIBV5": 17, + "CF_DSPBITMAP": 130, + "CF_DSPTEXT": 129, + "CF_DSPMETAFILEPICT": 131, + "CF_DSPENHMETAFILE": 142, + "CF_GDIOBJLAST": 0x03FF, + "CF_PRIVATEFIRST": 0x0200, +} + +func getFormat(uFormat string) win32api.DWORD { + if v, ok := winformatrev[uFormat]; ok { + return v + } + return win32api.RegisterClipboardFormat(uFormat) +} + +func AutoSetter(uFormat string, data interface{}) error { + switch uFormat { + case "CF_TEXT", "CF_UNICODETEXT", "TEXT": + return setClipboardData(win32api.CF_UNICODETEXT, data, textSetFn) + case "File": + return setClipboardData(win32api.CF_HDROP, data, fileSetFn) + case "Image", "PNG": + return setClipboardData(0, data, imageSetFn) + default: + } + return setClipboardData(getFormat(uFormat), data, nil) +} + +func setClipboardData(uFormat win32api.DWORD, data interface{}, fn func(uFormat win32api.DWORD, data interface{}) error) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + err := win32api.OpenClipboard(0) + if err != nil { + return fmt.Errorf("OpenClipboard failed: %v", err) + } + defer win32api.CloseClipboard() + err = win32api.EmptyClipboard() + if err != nil { + return fmt.Errorf("EmptyClipboard failed: %v", err) + } + if fn == nil { + return defaultSetFn(uFormat, data) + } + return fn(uFormat, data) +} + +func defaultSetFn(uFormat win32api.DWORD, idata interface{}) error { + data, ok := idata.([]byte) + if !ok { + return fmt.Errorf("data is not a byte slice") + } + mem, p, err := allocNewMem(uintptr(uint32(len(data)))) + if err != nil { + return err + } + defer win32api.GlobalUnlock(mem) + + err = win32api.RtlMoveMemory(p, unsafe.Pointer(&data[0]), uintptr(len(data))) + if err != nil { + return fmt.Errorf("RtlMoveMemory failed: %v", err) + } + _, err = win32api.SetClipboardData(uFormat, win32api.HGLOBAL(p)) + if err != nil { + win32api.GlobalFree(win32api.HGLOBAL(p)) + return fmt.Errorf("SetClipboardData failed: %v", err) + } + return nil +} + +func textSetFn(uFormat win32api.DWORD, idata interface{}) error { + data, ok := idata.(string) + if !ok { + return fmt.Errorf("data is not a byte slice") + } + str, err := syscall.UTF16FromString(data) + if err != nil { + return fmt.Errorf("UTF16FromString failed: %v", err) + } + size := uintptr(len(str) * int(unsafe.Sizeof(str[0]))) + mem, p, err := allocNewMem(size) + if err != nil { + return err + } + defer win32api.GlobalUnlock(mem) + + err = win32api.RtlMoveMemory(p, unsafe.Pointer(&str[0]), size) + if err != nil { + return fmt.Errorf("RtlMoveMemory failed: %v", err) + } + _, err = win32api.SetClipboardData(uFormat, win32api.HGLOBAL(p)) + if err != nil { + win32api.GlobalFree(win32api.HGLOBAL(p)) + return fmt.Errorf("SetClipboardData failed: %v", err) + } + return nil +} + +type dropfiles struct { + pFiles uint32 // 4 bytes + pt point // 2 * 4 = 8 bytes + fNC uint32 // 2 byte + fWide uint32 // 2 byte +} + +type point struct { + X int32 // 4 bytes + Y int32 // 4 bytes +} + +func fileSetFn(uFormat win32api.DWORD, idata interface{}) error { + data, ok := idata.([]string) + if !ok { + return fmt.Errorf("data is not a filepath string slice") + } + var utf16s = make([][]uint16, 0, len(data)) + var size uintptr = 20 + 2 + for _, d := range data { + str, err := syscall.UTF16FromString(d) + if err != nil { + return fmt.Errorf("UTF16FromString failed: %v", err) + } + utf16s = append(utf16s, str) + size += uintptr((len(str)) * int(unsafe.Sizeof(str[0]))) + } + { + mem, p, err := allocNewMem(size) + if err != nil { + return err + } + defer win32api.GlobalUnlock(mem) + var fdrop = dropfiles{ + pFiles: 20, + pt: point{0, 0}, + fNC: 0, + fWide: 1, + } + offset := unsafe.Sizeof(fdrop) + err = win32api.RtlMoveMemory(unsafe.Pointer(p), unsafe.Pointer(&fdrop), unsafe.Sizeof(fdrop)) + if err != nil { + return fmt.Errorf("RtlMoveMemory failed: %v", err) + } + for _, v := range utf16s { + size = uintptr(len(v) * int(unsafe.Sizeof(v[0]))) + err = win32api.RtlMoveMemory(unsafe.Pointer(uintptr(p)+offset), unsafe.Pointer(&v[0]), size) + if err != nil { + return fmt.Errorf("RtlMoveMemory failed: %v", err) + } + offset += size + } + *(*uint16)(unsafe.Pointer(uintptr(p) + offset)) = 0 + _, err = win32api.SetClipboardData(win32api.CF_HDROP, win32api.HGLOBAL(p)) + if err != nil { + win32api.GlobalFree(win32api.HGLOBAL(p)) + return fmt.Errorf("SetClipboardData failed: %v", err) + } + } + { + mem, p, err := allocNewMem(4) + if err != nil { + return err + } + defer win32api.GlobalUnlock(mem) + for idx, v := range []byte{5, 0, 0, 0} { + *(*byte)(unsafe.Pointer(uintptr(p) + uintptr(idx))) = v + } + format := win32api.RegisterClipboardFormat("Preferred DropEffect") + _, err = win32api.SetClipboardData(format, win32api.HGLOBAL(p)) + if err != nil { + win32api.GlobalFree(win32api.HGLOBAL(p)) + return fmt.Errorf("SetClipboardData failed: %v", err) + } + } + + return nil +} + +func imageSetFn(uFormat win32api.DWORD, idata interface{}) error { + data, ok := idata.([]byte) + if !ok { + return fmt.Errorf("data is not a byte slice") + } + if len(data) == 0 { + return nil + } + { + img, err := png.Decode(bytes.NewReader(data)) + 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 + + datas := 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() + datas[idx+2] = uint8(r) + datas[idx+1] = uint8(g) + datas[idx+0] = uint8(b) + datas[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. + info.CSType = 0x73524742 + 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(datas[:], infob[:]) + size := uintptr(len(datas) * int(unsafe.Sizeof(datas[0]))) + mem, p, err := allocNewMem(size) + if err != nil { + return err + } + defer win32api.GlobalUnlock(mem) + err = win32api.RtlMoveMemory(p, unsafe.Pointer(&datas[0]), size) + if err != nil { + return fmt.Errorf("RtlMoveMemory failed: %v", err) + } + _, err = win32api.SetClipboardData(win32api.CF_DIBV5, win32api.HGLOBAL(p)) + if err != nil { + win32api.GlobalFree(win32api.HGLOBAL(p)) + return fmt.Errorf("SetClipboardData failed: %v", err) + } + } + { + mem, p, err := allocNewMem(uintptr(len(data) * int(unsafe.Sizeof(data[0])))) + if err != nil { + return err + } + defer win32api.GlobalUnlock(mem) + err = win32api.RtlMoveMemory(p, unsafe.Pointer(&data[0]), uintptr(len(data)*int(unsafe.Sizeof(data[0])))) + if err != nil { + return fmt.Errorf("RtlMoveMemory failed: %v", err) + } + format := win32api.RegisterClipboardFormat("PNG") + _, err = win32api.SetClipboardData(format, win32api.HGLOBAL(p)) + if err != nil { + win32api.GlobalFree(win32api.HGLOBAL(p)) + return fmt.Errorf("SetClipboardData failed: %v", err) + } + } + return nil +}