修复: 粘贴图失效与SFTP串台回退等一批问题

粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
This commit is contained in:
lxy
2026-10-24 13:48:54 +08:00
parent 4675ae381d
commit 75a42c0b6a
48 changed files with 1072 additions and 220 deletions
+164
View File
@@ -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
}