Files
Thing/src-tauri/src/clipboard/reader.rs
T

324 lines
11 KiB
Rust
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.
//! Win32 剪贴板读取与写回
//!
//! 读取优先级:文本 > 图片 > 文件。写回支持三种格式。
//! 句柄类型说明(windows-sys 0.52):
//! - HWND / HANDLE = isize
//! - HGLOBAL / HLOCAL = *mut c_void
//! - HDROP = isize(与 HANDLE 同)
use windows_sys::Win32::System::DataExchange::{
CloseClipboard, EmptyClipboard, GetClipboardData, IsClipboardFormatAvailable, OpenClipboard,
SetClipboardData,
};
use windows_sys::Win32::System::Memory::{GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock, GMEM_MOVEABLE, GMEM_ZEROINIT};
use windows_sys::Win32::System::Ole::{CF_DIB, CF_HDROP, CF_UNICODETEXT};
use windows_sys::Win32::UI::Shell::DragQueryFileW;
/// 剪贴板当前内容
pub enum ClipData {
Text(String),
Image { dib: Vec<u8>, width: u32, height: u32 },
Files(Vec<String>),
}
/// 读取剪贴板(需在 monitor 线程或命令线程调用)。OpenClipboard 失败返回 None。
pub fn read_clipboard() -> Option<ClipData> {
unsafe {
if OpenClipboard(0) == 0 {
return None;
}
let result = if IsClipboardFormatAvailable(CF_UNICODETEXT as u32) != 0 {
read_text()
} else if IsClipboardFormatAvailable(CF_DIB as u32) != 0 {
read_image()
} else if IsClipboardFormatAvailable(CF_HDROP as u32) != 0 {
read_files()
} else {
None
};
CloseClipboard();
result
}
}
unsafe fn read_text() -> Option<ClipData> {
let h = GetClipboardData(CF_UNICODETEXT as u32); // HANDLE = isize
if h == 0 {
return None;
}
let hglob = h as *mut core::ffi::c_void; // 实际是 HGLOBAL
let ptr = GlobalLock(hglob) as *const u16;
if ptr.is_null() {
return None;
}
let mut len = 0usize;
while *ptr.add(len) != 0 {
len += 1;
}
let slice = core::slice::from_raw_parts(ptr, len);
let s = String::from_utf16_lossy(slice);
GlobalUnlock(hglob);
if s.is_empty() {
None
} else {
Some(ClipData::Text(s))
}
}
unsafe fn read_image() -> Option<ClipData> {
let h = GetClipboardData(CF_DIB as u32);
if h == 0 {
return None;
}
let hglob = h as *mut core::ffi::c_void;
let ptr = GlobalLock(hglob) as *const u8;
if ptr.is_null() {
return None;
}
let size = GlobalSize(hglob);
let slice = core::slice::from_raw_parts(ptr, size);
let dib = slice.to_vec();
GlobalUnlock(hglob);
let (w, hgt) = dib_info(&dib).unwrap_or((0, 0));
Some(ClipData::Image {
dib,
width: w,
height: hgt,
})
}
unsafe fn read_files() -> Option<ClipData> {
let h = GetClipboardData(CF_HDROP as u32); // HANDLE = isize
if h == 0 {
return None;
}
let hdrop = h; // HDROP = isize
let count = DragQueryFileW(hdrop, 0xFFFFFFFF, core::ptr::null_mut(), 0);
let mut files = Vec::with_capacity(count as usize);
for i in 0..count {
let len = DragQueryFileW(hdrop, i, core::ptr::null_mut(), 0);
if len == 0 {
continue;
}
let mut buf = vec![0u16; (len as usize) + 1];
let got = DragQueryFileW(hdrop, i, buf.as_mut_ptr(), buf.len() as u32);
let s = String::from_utf16_lossy(&buf[..got as usize]);
files.push(s);
}
// 剪贴板拥有句柄,不调用 DragFinish
if files.is_empty() {
None
} else {
Some(ClipData::Files(files))
}
}
// ===== 写回剪贴板(copy_back =====
/// 写回文本。失败返回 false。写回会清空其他格式。
pub fn write_text(s: &str) -> bool {
unsafe {
if OpenClipboard(0) == 0 {
return false;
}
let r = write_text_inner(s);
CloseClipboard();
r
}
}
unsafe fn write_text_inner(s: &str) -> bool {
if EmptyClipboard() == 0 {
return false;
}
let mut utf16: Vec<u16> = s.encode_utf16().collect();
utf16.push(0); // null terminator
let byte_len = utf16.len() * 2;
let hglob = GlobalAlloc(GMEM_MOVEABLE, byte_len);
if hglob.is_null() {
return false;
}
let ptr = GlobalLock(hglob) as *mut u16;
if ptr.is_null() {
return false;
}
core::ptr::copy_nonoverlapping(utf16.as_ptr(), ptr, utf16.len());
GlobalUnlock(hglob);
SetClipboardData(CF_UNICODETEXT as u32, hglob as isize) != 0
}
/// 写回图片(CF_DIB 原始字节)。
pub fn write_dib(dib: &[u8]) -> bool {
unsafe {
if OpenClipboard(0) == 0 {
return false;
}
let r = (|| {
if EmptyClipboard() == 0 {
return false;
}
let hglob = GlobalAlloc(GMEM_MOVEABLE, dib.len());
if hglob.is_null() {
return false;
}
let ptr = GlobalLock(hglob) as *mut u8;
if ptr.is_null() {
return false;
}
core::ptr::copy_nonoverlapping(dib.as_ptr(), ptr, dib.len());
GlobalUnlock(hglob);
SetClipboardData(CF_DIB as u32, hglob as isize) != 0
})();
CloseClipboard();
r
}
}
/// 写回文件列表(CF_HDROP)。
pub fn write_files(paths: &[String]) -> bool {
unsafe {
if OpenClipboard(0) == 0 {
return false;
}
let r = (|| {
if EmptyClipboard() == 0 {
return false;
}
// DROPFILES 头部 20 字节:pFiles(u32) + pt.x(i32) + pt.y(i32) + fNC(i32) + fWide(i32)
const DF_SIZE: usize = 20;
let mut wide: Vec<u16> = Vec::new();
for p in paths {
wide.extend(p.encode_utf16());
wide.push(0); // 每条字符串 null 结尾
}
wide.push(0); // 末尾额外 null 表示列表结束
let total = DF_SIZE + wide.len() * 2;
let hglob = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, total);
if hglob.is_null() {
return false;
}
let ptr = GlobalLock(hglob) as *mut u8;
if ptr.is_null() {
return false;
}
let offset = DF_SIZE as u32;
let off_bytes = offset.to_ne_bytes();
core::ptr::copy_nonoverlapping(off_bytes.as_ptr(), ptr, 4); // pFiles
let fwide: u32 = 1;
let fwide_bytes = fwide.to_ne_bytes();
core::ptr::copy_nonoverlapping(fwide_bytes.as_ptr(), ptr.add(16), 4); // fWide = 1
let str_ptr = ptr.add(DF_SIZE) as *mut u16;
core::ptr::copy_nonoverlapping(wide.as_ptr(), str_ptr, wide.len());
GlobalUnlock(hglob);
SetClipboardData(CF_HDROP as u32, hglob as isize) != 0
})();
CloseClipboard();
r
}
}
// ===== DIB → PNG(用于预览) =====
/// 从 DIB 字节解析宽高
pub fn dib_info(dib: &[u8]) -> Option<(u32, u32)> {
if dib.len() < 40 {
return None;
}
let width = i32::from_le_bytes([dib[4], dib[5], dib[6], dib[7]]);
let height = i32::from_le_bytes([dib[8], dib[9], dib[10], dib[11]]);
if width <= 0 {
return None;
}
Some((width as u32, height.abs() as u32))
}
/// 将 CF_DIB 字节转换为 PNG 字节(支持 24/32bpp BI_RGB / BI_BITFIELDS)。
pub fn dib_to_png(dib: &[u8]) -> Option<Vec<u8>> {
use image::codecs::png::PngEncoder;
use image::ImageEncoder;
// 兜底:数据本身就是 PNG / JPEG(极少数来源直接存放压缩数据)
if dib.len() >= 4 && &dib[0..4] == b"\x89PNG" {
return Some(dib.to_vec());
}
if dib.len() >= 3 && dib[0] == 0xFF && dib[1] == 0xD8 && dib[2] == 0xFF {
return Some(dib.to_vec());
}
if dib.len() < 40 {
return None;
}
let header_size = u32::from_le_bytes([dib[0], dib[1], dib[2], dib[3]]);
let width = i32::from_le_bytes([dib[4], dib[5], dib[6], dib[7]]);
let height_raw = i32::from_le_bytes([dib[8], dib[9], dib[10], dib[11]]);
let bpp = u16::from_le_bytes([dib[14], dib[15]]);
let compression = u32::from_le_bytes([dib[16], dib[17], dib[18], dib[19]]);
if width <= 0 {
return None;
}
// 接受 BI_RGB(0) 和 BI_BITFIELDS(3)。
// Windows 截图工具(Win+Shift+S / 截图工具)常用 BI_BITFIELDS 标记 32bpp BGRA
// 像素数据本身未压缩,与 BI_RGB 解码方式一致。
// 拒绝 BI_RLE4/8(1/2) 和 BI_JPEG/PNG(4/5) 等真正压缩格式。
if compression != 0 && compression != 3 {
return None;
}
if bpp != 24 && bpp != 32 {
return None;
}
let height = height_raw.abs();
let top_down = height_raw < 0;
let pixel_offset = header_size as usize; // 24/32bpp 无调色板
let row_size = ((bpp as u32 * width as u32 + 31) / 32) * 4;
let bytes_per_pixel = (bpp / 8) as usize;
let needed = pixel_offset + (row_size as usize) * (height as usize);
if dib.len() < needed {
return None;
}
let mut rgba = vec![0u8; (width as usize) * (height as usize) * 4];
for y in 0..height {
let src_row = if top_down { y } else { height - 1 - y };
let src_off = pixel_offset + (src_row as usize) * (row_size as usize);
for x in 0..width {
let sp = src_off + (x as usize) * bytes_per_pixel;
let dp = (y as usize) * (width as usize) * 4 + (x as usize) * 4;
rgba[dp] = dib[sp + 2]; // R
rgba[dp + 1] = dib[sp + 1]; // G
rgba[dp + 2] = dib[sp]; // B
// 32bpp 保留 alpha 通道(截图工具常用);24bpp 不透明
rgba[dp + 3] = if bpp == 32 { dib[sp + 3] } else { 255 };
}
}
let mut buf = Vec::new();
let enc = PngEncoder::new(&mut buf);
enc.write_image(&rgba, width as u32, height as u32, image::ExtendedColorType::Rgba8)
.ok()?;
Some(buf)
}
/// 生成缩略图 PNG(最长边不超过 max_dim 像素)。
/// 小图直接复用 `dib_to_png` 结果;大图降采样后重新编码,供弹窗悬停预览使用。
pub fn dib_to_thumbnail(dib: &[u8], max_dim: u32) -> Option<Vec<u8>> {
use image::codecs::png::PngEncoder;
use image::GenericImageView;
use image::ImageEncoder;
let png = dib_to_png(dib)?;
let img = image::load_from_memory(&png).ok()?;
let (w, h) = img.dimensions();
if w.max(h) <= max_dim {
return Some(png); // 小图直接使用,避免重复编码
}
// 让 image 库在 max_dim×max_dim 边界内自动等比缩放,避免手算 nw/nh 与库内部
// resize_dimensions 的取整不一致(如手算 256x10 而库实际输出 250x10),
// 否则 write_image 的缓冲区长度断言会失败。
let resized = img.resize(max_dim, max_dim, image::imageops::FilterType::Triangle);
let (rw, rh) = resized.dimensions();
let rgba = resized.to_rgba8();
let mut buf = Vec::new();
let enc = PngEncoder::new(&mut buf);
enc.write_image(rgba.as_raw(), rw, rh, image::ExtendedColorType::Rgba8)
.ok()?;
Some(buf)
}