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