diff --git a/internal/ui/model/clipboard.go b/internal/ui/model/clipboard.go index dfe42be5d7..eb8f854f04 100644 --- a/internal/ui/model/clipboard.go +++ b/internal/ui/model/clipboard.go @@ -12,4 +12,5 @@ const ( var ( errClipboardPlatformUnsupported = errors.New("clipboard operations are not supported on this platform") errClipboardUnknownFormat = errors.New("unknown clipboard format") + errClipboardImageUnavailable = errors.New("clipboard does not contain a supported image format") ) diff --git a/internal/ui/model/clipboard_image_darwin.go b/internal/ui/model/clipboard_image_darwin.go new file mode 100644 index 0000000000..b795e9a3d6 --- /dev/null +++ b/internal/ui/model/clipboard_image_darwin.go @@ -0,0 +1,112 @@ +//go:build darwin && !ios + +package model + +import ( + "bytes" + "image/png" + "runtime" + "unsafe" + + "github.com/ebitengine/purego" + "github.com/ebitengine/purego/objc" + "golang.org/x/image/tiff" +) + +var ( + nsPasteboardClass objc.Class + nsDataClass objc.Class + + selGeneralPasteboard objc.SEL + selDataForType objc.SEL + selBytes objc.SEL + selLength objc.SEL + + nsPasteboardTypeTIFF objc.ID + + clipboardImageDarwinInitialized bool + clipboardImageDarwinInitError error +) + +func initClipboardImageDarwin() { + if clipboardImageDarwinInitialized { + return + } + clipboardImageDarwinInitialized = true + + appkit, err := purego.Dlopen("/System/Library/Frameworks/AppKit.framework/AppKit", purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + clipboardImageDarwinInitError = err + return + } + + nsPasteboardClass = objc.GetClass("NSPasteboard") + nsDataClass = objc.GetClass("NSData") + + selGeneralPasteboard = objc.RegisterName("generalPasteboard") + selDataForType = objc.RegisterName("dataForType:") + selBytes = objc.RegisterName("bytes") + selLength = objc.RegisterName("length") + + typeTIFFPtr, err := purego.Dlsym(appkit, "NSPasteboardTypeTIFF") + if err != nil { + clipboardImageDarwinInitError = err + return + } + nsPasteboardTypeTIFF = objc.ID(*(*uintptr)(unsafe.Pointer(typeTIFFPtr))) +} + +// readClipboardImageFallback attempts to read image data from the macOS +// clipboard in formats other than PNG (e.g., TIFF from WeChat screenshots). +// It returns the image encoded as PNG. +func readClipboardImageFallback() ([]byte, error) { + initClipboardImageDarwin() + if clipboardImageDarwinInitError != nil { + return nil, clipboardImageDarwinInitError + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + pasteboard := objc.ID(nsPasteboardClass).Send(selGeneralPasteboard) + if pasteboard == 0 { + return nil, errClipboardImageUnavailable + } + + data := pasteboard.Send(selDataForType, nsPasteboardTypeTIFF) + if data == 0 { + return nil, errClipboardImageUnavailable + } + + length := objc.Send[uint64](data, selLength) + if length == 0 { + return nil, errClipboardImageUnavailable + } + + bytesPtr := data.Send(selBytes) + if bytesPtr == 0 { + return nil, errClipboardImageUnavailable + } + + buf := make([]byte, length) + copyBytes(buf, uintptr(bytesPtr), int(length)) + + img, err := tiff.Decode(bytes.NewReader(buf)) + if err != nil { + return nil, err + } + + var out bytes.Buffer + if err := png.Encode(&out, img); err != nil { + return nil, err + } + return out.Bytes(), nil +} + +// copyBytes copies n bytes from src to dst. It is defined locally to avoid +// depending on the implementation details of go-nativeclipboard. +func copyBytes(dst []byte, src uintptr, n int) { + for i := range n { + dst[i] = *(*byte)(unsafe.Pointer(src + uintptr(i))) + } +} diff --git a/internal/ui/model/clipboard_image_other.go b/internal/ui/model/clipboard_image_other.go new file mode 100644 index 0000000000..d3c2a2ef12 --- /dev/null +++ b/internal/ui/model/clipboard_image_other.go @@ -0,0 +1,10 @@ +//go:build !(darwin && !ios) + +package model + +// readClipboardImageFallback is a no-op on non-macOS platforms. The +// go-nativeclipboard library already handles the common image formats on +// Linux and Windows. +func readClipboardImageFallback() ([]byte, error) { + return nil, errClipboardImageUnavailable +} diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index ab6e1779d6..206091215c 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -3730,6 +3730,12 @@ func (m *UI) handleFilePathPaste(path string) tea.Cmd { // interpreting clipboard text as a file path. func (m *UI) pasteImageFromClipboard() tea.Msg { imageData, err := readClipboard(clipboardFormatImage) + if err != nil { + // The nativeclipboard library may not support every clipboard image + // format on all platforms (e.g., WeChat screenshots on macOS are + // copied as TIFF). Try a platform-specific fallback before giving up. + imageData, err = readClipboardImageFallback() + } if int64(len(imageData)) > common.MaxAttachmentSize { return util.InfoMsg{ Type: util.InfoTypeError,