From 26e0bf5bb532546f20a7c30955ea70c2bb8f4ace Mon Sep 17 00:00:00 2001 From: Starainrt Date: Wed, 27 Mar 2024 11:20:59 +0800 Subject: [PATCH] init --- .idea/.gitignore | 10 ++ .idea/clipboard.iml | 11 +++ .idea/modules.xml | 8 ++ clipboard.go | 112 ++++++++++++++++++++++ clipboard_test.go | 20 ++++ clipboard_windows.go | 132 ++++++++++++++++++++++++++ go.mod | 10 ++ go.sum | 6 ++ handle_windows.go | 215 +++++++++++++++++++++++++++++++++++++++++++ listener_windows.go | 5 + 10 files changed, 529 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/clipboard.iml create mode 100644 .idea/modules.xml create mode 100644 clipboard.go create mode 100644 clipboard_test.go create mode 100644 clipboard_windows.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handle_windows.go create mode 100644 listener_windows.go diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..01b5f8c --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/.idea/clipboard.iml b/.idea/clipboard.iml new file mode 100644 index 0000000..5d81a31 --- /dev/null +++ b/.idea/clipboard.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f6f9c30 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/clipboard.go b/clipboard.go new file mode 100644 index 0000000..daee37e --- /dev/null +++ b/clipboard.go @@ -0,0 +1,112 @@ +package clipboard + +import ( + "strings" + "time" +) + +type FileType string + +const ( + Text FileType = "text" + File FileType = "file" + Image FileType = "image" + HTML FileType = "html" +) + +type Clipboard struct { + winOriginTypes []string + plateform string + date time.Time + secondaryOriType string + secondaryType FileType + secondaryData []byte + + primaryOriType string + primaryType FileType + primaryData []byte + hash string +} + +func (c *Clipboard) PrimaryType() FileType { + return c.primaryType +} + +func (c *Clipboard) AvailableTypes() []FileType { + var res = make([]FileType, 0, 2) + if c.primaryType != "" { + res = append(res, c.primaryType) + } + if c.secondaryType != "" { + res = append(res, c.secondaryType) + } + return res +} + +func (c *Clipboard) IsText() bool { + return c.primaryType == Text || c.secondaryType == Text +} + +func (c *Clipboard) Text() string { + if c.primaryType == Text { + return string(c.primaryData) + } + if c.secondaryType == Text { + return string(c.secondaryData) + } + return "" +} + +func (c *Clipboard) IsHTML() bool { + return (c.primaryType == HTML || c.secondaryType == HTML) || c.IsText() +} + +func (c *Clipboard) HTML() string { + var htmlBytes []byte + if c.primaryType == HTML { + htmlBytes = c.primaryData + } else if c.secondaryType == HTML { + htmlBytes = c.secondaryData + } else { + return c.Text() + } + formats := strings.SplitN(string(htmlBytes), "\n", 7) + if len(formats) < 7 { + return string(htmlBytes) + } + return formats[6] +} + +func (c *Clipboard) FilePaths() []string { + if c.primaryType == File { + return strings.Split(string(c.primaryData), "|") + } + if c.secondaryType == File { + return strings.Split(string(c.secondaryData), "|") + } + return nil +} + +func (c *Clipboard) FirstFilePath() string { + if c.primaryType == File { + return strings.Split(string(c.primaryData), "|")[0] + } + if c.secondaryType == File { + return strings.Split(string(c.secondaryData), "|")[0] + } + return "" +} + +func (c *Clipboard) Image() []byte { + if c.primaryType == Image { + return c.primaryData + } + if c.secondaryType == Image { + return c.secondaryData + } + return nil +} + +func (c *Clipboard) IsImage() bool { + return c.primaryType == Image || c.secondaryType == Image +} diff --git a/clipboard_test.go b/clipboard_test.go new file mode 100644 index 0000000..491f096 --- /dev/null +++ b/clipboard_test.go @@ -0,0 +1,20 @@ +package clipboard + +import ( + "fmt" + "testing" +) + +func TestGet(t *testing.T) { + c, err := Get() + if err != nil { + t.Error(err) + } + fmt.Println(c.plateform) + fmt.Println(c.winOriginTypes) + fmt.Println(c.PrimaryType()) + fmt.Println(c.AvailableTypes()) + fmt.Println(c.Text()) + fmt.Println(c.HTML()) + fmt.Println(c.FilePaths()) +} diff --git a/clipboard_windows.go b/clipboard_windows.go new file mode 100644 index 0000000..71d588a --- /dev/null +++ b/clipboard_windows.go @@ -0,0 +1,132 @@ +package clipboard + +import ( + "b612.me/win32api" +) + +var winformat = map[win32api.DWORD]string{ + 1: "CF_TEXT", + 2: "CF_BITMAP", + 3: "CF_METAFILEPICT", + 4: "CF_SYLK", + 5: "CF_DIF", + 6: "CF_TIFF", + 7: "CF_OEMTEXT", + 8: "CF_DIB", + 9: "CF_PALETTE", + 10: "CF_PENDATA", + 11: "CF_RIFF", + 12: "CF_WAVE", + 13: "CF_UNICODETEXT", + 14: "CF_ENHMETAFILE", + 15: "CF_HDROP", + 16: "CF_LOCALE", + 17: "CF_DIBV5", + 130: "CF_DSPBITMAP", + 129: "CF_DSPTEXT", + 131: "CF_DSPMETAFILEPICT", + 142: "CF_DSPENHMETAFILE", + 0x03FF: "CF_GDIOBJLAST", + 0x0200: "CF_PRIVATEFIRST", +} + +var formatRank = map[string]int{ + "CF_UNICODETEXT": 1, + "CF_DIBV5": 2, + //"CF_DIB": 2, + "PNG": 2, + "HTML Format": 3, + "CF_HDROP": 4, +} + +func Get() (Clipboard, error) { + return innerGetClipboard() +} + +func innerGetClipboard() (Clipboard, error) { + var c = Clipboard{ + plateform: "windows", + } + err := win32api.OpenClipboard(0) + if err != nil { + return c, err + } + defer win32api.CloseClipboard() + formats, err := win32api.GetUpdatedClipboardFormatsAll() + if err != nil { + return c, err + } + var firstFormatName, secondFormatName string + var firstFormat, secondFormat int = 65535, 65535 + for _, format := range formats { + if d, ok := winformat[format]; ok { + if formatRank[d] > 0 { + if formatRank[d] < firstFormat { + secondFormat = firstFormat + secondFormatName = firstFormatName + firstFormat = formatRank[d] + firstFormatName = d + } else if formatRank[d] != firstFormat && formatRank[d] < secondFormat { + secondFormat = formatRank[d] + secondFormatName = d + } + } + c.winOriginTypes = append(c.winOriginTypes, d) + continue + } + d, e := win32api.GetClipboardFormatName(format) + if e != nil { + continue + } + if formatRank[d] > 0 { + if formatRank[d] < firstFormat { + secondFormat = firstFormat + secondFormatName = firstFormatName + firstFormat = formatRank[d] + firstFormatName = d + } else if formatRank[d] != firstFormat && formatRank[d] < secondFormat { + secondFormat = formatRank[d] + secondFormatName = d + } + } + c.winOriginTypes = append(c.winOriginTypes, d) + } + + c.primaryOriType = firstFormatName + switch c.primaryOriType { + case "CF_UNICODETEXT": + c.primaryType = Text + case "HTML Format": + c.primaryType = HTML + case "PNG", "CF_DIBV5", "CF_DIB": + c.primaryType = Image + case "CF_HDROP": + c.primaryType = File + } + c.primaryData, err = AutoFetcher(firstFormatName) + if err != nil { + return c, err + } + if secondFormatName != "" { + switch secondFormatName { + case "CF_UNICODETEXT": + c.secondaryType = Text + case "HTML Format": + c.secondaryType = HTML + case "PNG", "CF_DIBV5", "CF_DIB": + c.secondaryType = Image + case "CF_HDROP": + c.secondaryType = File + } + c.secondaryOriType = secondFormatName + c.secondaryData, err = AutoFetcher(secondFormatName) + if err != nil { + return c, err + } + } + return c, nil +} + +func GetTopMatch() { + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..623057d --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module b612.me/clipboard + +go 1.21.2 + +require ( + b612.me/win32api v0.0.0-20240326080749-ad19f5cd4247 + golang.org/x/image v0.15.0 +) + +require golang.org/x/sys v0.18.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8029e86 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +b612.me/win32api v0.0.0-20240326080749-ad19f5cd4247 h1:fDTZ1HzVtVpEcXqlsQB9O2AbtrbqiAruaRX1Yd7M9Z8= +b612.me/win32api v0.0.0-20240326080749-ad19f5cd4247/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= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/handle_windows.go b/handle_windows.go new file mode 100644 index 0000000..c854575 --- /dev/null +++ b/handle_windows.go @@ -0,0 +1,215 @@ +package clipboard + +import ( + "b612.me/win32api" + "bytes" + "encoding/binary" + "errors" + "golang.org/x/image/bmp" + "image" + "image/color" + "image/png" + "reflect" + "strings" + "syscall" + "unsafe" +) + +func fetchClipboardData(uFormat win32api.DWORD, fn func(p unsafe.Pointer, size uint32) ([]byte, error)) ([]byte, error) { + mem, err := win32api.GetClipboardData(uFormat) + if err != nil { + return nil, err + } + p, err := win32api.GlobalLock(mem) + if err != nil { + return nil, err + } + defer win32api.GlobalUnlock(mem) + size, err := win32api.GlobalSize(mem) + if err != nil { + return nil, err + } + if fn == nil { + return defaultFetchFn(p, uint32(size)) + } + return fn(p, uint32(size)) +} + +func defaultFetchFn(p unsafe.Pointer, size uint32) ([]byte, error) { + var buf []byte + for i := 0; i < int(size); i++ { + buf = append(buf, *(*byte)(unsafe.Pointer(uintptr(p) + uintptr(i)))) + } + return buf, nil +} + +func AutoFetcher(uFormat string) ([]byte, error) { + switch uFormat { + case "CF_TEXT", "CF_UNICODETEXT": + return fetchClipboardData(win32api.CF_UNICODETEXT, textFetcher) + case "HTML Format": + return fetchClipboardData(win32api.RegisterClipboardFormat("HTML Format"), nil) + case "CF_HDROP": + return fetchClipboardData(win32api.CF_HDROP, filedropFetcher) + case "CF_DIBV5": + return fetchClipboardData(win32api.CF_DIBV5, cfDIBv5Fetcher) + case "CF_DIB": + return fetchClipboardData(win32api.CF_DIB, cfDIBFetcher) + case "PNG": + return fetchClipboardData(win32api.RegisterClipboardFormat("PNG"), nil) + } + return nil, errors.New("not support uFormat:" + uFormat) +} + +func textFetcher(p unsafe.Pointer, size uint32) ([]byte, 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)) + } + return []byte(syscall.UTF16ToString(buf)), nil +} + +func filedropFetcher(p unsafe.Pointer, size uint32) ([]byte, error) { + var res []string + c, err := win32api.DragQueryFile(win32api.HDROP(p), 0xFFFFFFFF, nil, 0) + if err != nil { + return nil, err + } + count := int(c) + + for i := 0; i < count; i++ { + c, err = win32api.DragQueryFile(win32api.HDROP(p), win32api.DWORD(i), nil, 0) + if err != nil { + return nil, err + } + length := int(c) + buffer := make([]uint16, length+1) + + _, err = win32api.DragQueryFile(win32api.HDROP(p), win32api.DWORD(i), + &buffer[0], win32api.DWORD(len(buffer))) + res = append(res, syscall.UTF16ToString(buffer)) + } + return []byte(strings.Join(res, "|")), nil +} + +func cfDIBv5Fetcher(p unsafe.Pointer, size uint32) ([]byte, error) { + // inspect header information + info := (*bitmapV5Header)(unsafe.Pointer(p)) + // maybe deal with other formats? + if info.BitCount != 32 { + return nil, errors.New("not support image format") + } + + 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 + err := png.Encode(&buf, img) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func cfDIBFetcher(p unsafe.Pointer, size uint32) ([]byte, error) { + // 函数意外报错,待修正 + const ( + fileHeaderLen = 14 + infoHeaderLen = 40 + ) + bmpHeader := (*bitmapHeader)(p) + 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(uintptr(p) + 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 +} + +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 +} diff --git a/listener_windows.go b/listener_windows.go new file mode 100644 index 0000000..fd56200 --- /dev/null +++ b/listener_windows.go @@ -0,0 +1,5 @@ +package clipboard + +func Listen() (<-chan Clipboard, error) + res := make(chan Clipboard) +} \ No newline at end of file