粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
165 lines
4.9 KiB
Go
165 lines
4.9 KiB
Go
//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
|
||
}
|