Files
u-desk/app_clipboard_image.go
T
lxy 75a42c0b6a 修复: 粘贴图失效与SFTP串台回退等一批问题
粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
2026-10-24 13:48:54 +08:00

165 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build windows
package main
import (
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"image/png"
"runtime"
"syscall"
"time"
"unsafe"
"golang.org/x/image/bmp"
"golang.org/x/sys/windows"
)
// binaryLE DIB/BMP 头字段均为小端
var binaryLE = binary.LittleEndian
var (
procRegisterClipboardFormatW = windows.NewLazySystemDLL("user32.dll").NewProc("RegisterClipboardFormatW")
procGlobalLockImg = windows.NewLazySystemDLL("kernel32.dll").NewProc("GlobalLock")
procGlobalUnlockImg = windows.NewLazySystemDLL("kernel32.dll").NewProc("GlobalUnlock")
procGlobalSizeImg = windows.NewLazySystemDLL("kernel32.dll").NewProc("GlobalSize")
)
// cfDIB 剪贴板设备无关位图格式,Win32 稳定 ABI 值
const cfDIB = 8
// pngFormatAtom "PNG" 注册格式的剪贴板原子号(系统级稳定,进程内缓存一次)
var pngFormatAtom uintptr
// ReadClipboardImage 读取系统剪贴板中的图片,返回 PNG 的 base64(无图返回空串+nil)。
// 优先取 "PNG" 注册格式(截图工具/浏览器复制时放的就是完整 PNG 文件字节),
// 否则回落 CF_DIB:补 14 字节 BMP 文件头后交 x/image/bmp 解码再重编为 PNG。
// 供前端 Ctrl+V keydown 直读(WebView2 无编辑焦点时 DOM paste 事件不可靠派发)。
func (a *App) ReadClipboardImage() (string, error) {
// OpenClipboard 与 CloseClipboard 必须同线程,否则剪贴板死锁
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if pngFormatAtom == 0 {
name, err := syscall.UTF16PtrFromString("PNG")
if err != nil {
return "", err
}
if atom, _, _ := procRegisterClipboardFormatW.Call(uintptr(unsafe.Pointer(name))); atom != 0 {
pngFormatAtom = atom
}
}
// 剪贴板是全局互斥资源,短重试避开其他进程的瞬时占用
opened := false
for i := 0; i < 20; i++ {
if r, _, _ := procOpenClipboard.Call(0); r != 0 {
opened = true
break
}
time.Sleep(10 * time.Millisecond)
}
if !opened {
return "", syscall.EBUSY
}
defer procCloseClipboard.Call()
if pngFormatAtom != 0 {
if r, _, _ := procIsClipboardFormatAvailable.Call(pngFormatAtom); r != 0 {
data, ok, err := clipboardFormatData(pngFormatAtom)
if err != nil {
return "", err
}
if ok && len(data) > 0 {
return base64.StdEncoding.EncodeToString(data), nil
}
}
}
if r, _, _ := procIsClipboardFormatAvailable.Call(cfDIB); r == 0 {
return "", nil
}
data, ok, err := clipboardFormatData(cfDIB)
if err != nil {
return "", err
}
if !ok || len(data) == 0 {
return "", nil
}
pngBytes, err := dibToPNG(data)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(pngBytes), nil
}
// clipboardFormatData 取指定格式的剪贴板字节(调用方须已 OpenClipboard)
func clipboardFormatData(format uintptr) ([]byte, bool, error) {
h, _, _ := procGetClipboardData.Call(format)
if h == 0 {
return nil, false, nil
}
p, _, _ := procGlobalLockImg.Call(h)
if p == 0 {
return nil, false, syscall.EBUSY
}
defer procGlobalUnlockImg.Call(h)
size, _, _ := procGlobalSizeImg.Call(h)
if size == 0 {
return nil, false, nil
}
data := make([]byte, size)
copy(data, unsafe.Slice((*byte)(unsafe.Pointer(p)), size))
return data, true, nil
}
// dibToPNG 把 CF_DIB(BITMAPINFO 起、无 14 字节文件头)转为 PNG 字节。
// 覆盖 24/32bpp 的 BI_RGB(0) 与 BI_BITFIELDS(3) 常见情形:DIB 原样透传,
// 仅补 BMP 文件头后交给 x/image/bmp 解码(行字节对齐、bottom-up、负高度=top-down 均由解码器处理)
func dibToPNG(dib []byte) ([]byte, error) {
if len(dib) < 40 {
return nil, fmt.Errorf("DIB 数据过短: %d 字节", len(dib))
}
headerSize := binaryLE.Uint32(dib[0:4])
bpp := binaryLE.Uint16(dib[14:16])
compression := binaryLE.Uint32(dib[16:20])
if headerSize < 40 || int(headerSize) > len(dib) {
return nil, fmt.Errorf("不支持的 DIB 头大小: %d", headerSize)
}
if bpp != 24 && bpp != 32 {
return nil, fmt.Errorf("不支持的位深: %d", bpp)
}
if compression != 0 && compression != 3 { // BI_RGB=0, BI_BITFIELDS=3
return nil, fmt.Errorf("不支持的压缩格式: %d", compression)
}
// 像素数据偏移 = 14 字节文件头 + 信息头;BI_BITFIELDS 且 40 字节头时
// 3 个掩码 DWORD 紧随其后(V4/V5 头 ≥108 字节的掩码内嵌在头里,无需外加)
pixelOffset := 14 + int(headerSize)
if compression == 3 && headerSize == 40 {
pixelOffset += 12
}
bmpBuf := make([]byte, 14+len(dib))
bmpBuf[0], bmpBuf[1] = 'B', 'M'
binaryLE.PutUint32(bmpBuf[2:6], uint32(len(bmpBuf)))
// 6..9 保留位保持 0
binaryLE.PutUint32(bmpBuf[10:14], uint32(pixelOffset))
copy(bmpBuf[14:], dib)
img, err := bmp.Decode(bytes.NewReader(bmpBuf))
if err != nil {
return nil, fmt.Errorf("DIB 解码失败: %w", err)
}
var out bytes.Buffer
if err := png.Encode(&out, img); err != nil {
return nil, fmt.Errorf("PNG 编码失败: %w", err)
}
return out.Bytes(), nil
}