截图模块优化调整

This commit is contained in:
zhongluofeng
2026-08-03 17:36:07 +08:00
parent f51a7f894e
commit c913379e2b
19 changed files with 1744 additions and 300 deletions
+140 -40
View File
@@ -78,12 +78,6 @@ fn bgra_to_bmp(bgra: &[u8], width: i32, height: i32) -> Result<Vec<u8>, String>
let data_size = w * h * 4;
let file_size = 14 + 40 + data_size;
// BitBlt 取出的 alpha 通道未定义(常为 0),强制不透明
let mut pixels = bgra.to_vec();
for a in pixels.iter_mut().skip(3).step_by(4) {
*a = 255;
}
let mut bmp = Vec::with_capacity(file_size);
// BITMAPFILEHEADER
bmp.extend_from_slice(b"BM");
@@ -103,7 +97,12 @@ fn bgra_to_bmp(bgra: &[u8], width: i32, height: i32) -> Result<Vec<u8>, String>
bmp.extend_from_slice(&0u32.to_le_bytes()); // y ppm
bmp.extend_from_slice(&0u32.to_le_bytes()); // colors used
bmp.extend_from_slice(&0u32.to_le_bytes()); // important colors
bmp.extend_from_slice(&pixels);
// 像素数据:memcpy 一次性拷贝,再原地修正 alpha(避免 to_vec 中间分配)
let pixel_start = bmp.len();
bmp.extend_from_slice(bgra);
for i in (pixel_start + 3..bmp.len()).step_by(4) {
bmp[i] = 255;
}
Ok(bmp)
}
@@ -117,16 +116,16 @@ fn bgra_to_png(bgra: &[u8], width: i32, height: i32) -> Result<Vec<u8>, String>
if width <= 0 || height <= 0 {
return Err("无效尺寸".into());
}
let w = width as usize;
let h = height as usize;
let mut rgba = vec![0u8; w * h * 4];
for i in 0..(w * h) {
rgba[i * 4] = bgra[i * 4 + 2]; // R
rgba[i * 4 + 1] = bgra[i * 4 + 1]; // G
rgba[i * 4 + 2] = bgra[i * 4]; // B
rgba[i * 4 + 3] = 255; // 强制不透明
let pixel_count = (width as usize) * (height as usize);
let mut rgba = vec![0u8; pixel_count * 4];
// chunks_exact 迭代器写法:4 字节对齐连续读取,编译器更容易自动向量化
for (src, dst) in bgra.chunks_exact(4).zip(rgba.chunks_exact_mut(4)) {
dst[0] = src[2]; // R
dst[1] = src[1]; // G
dst[2] = src[0]; // B
dst[3] = 255; // A 强制不透明
}
let mut buf = Vec::with_capacity(w * h * 4 / 3);
let mut buf = Vec::with_capacity(pixel_count * 4 / 3);
PngEncoder::new_with_quality(
&mut buf,
CompressionType::Fast,
@@ -493,7 +492,9 @@ fn bgra_to_dib(bgra: &[u8], width: i32, height: i32) -> Vec<u8> {
let header_size = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
let w = width as usize;
let h = height as usize;
let mut dib = Vec::with_capacity(header_size as usize + w * h * 4);
let row_size = w * 4;
let pixel_size = row_size * h;
let mut dib = Vec::with_capacity(header_size as usize + pixel_size);
let header = BITMAPINFOHEADER {
biSize: header_size,
biWidth: width,
@@ -501,7 +502,7 @@ fn bgra_to_dib(bgra: &[u8], width: i32, height: i32) -> Vec<u8> {
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB,
biSizeImage: (w * h * 4) as u32,
biSizeImage: (pixel_size) as u32,
biXPelsPerMeter: 0,
biYPelsPerMeter: 0,
biClrUsed: 0,
@@ -514,15 +515,17 @@ fn bgra_to_dib(bgra: &[u8], width: i32, height: i32) -> Vec<u8> {
)
};
dib.extend_from_slice(raw);
// top-down 行序倒序拷贝为 bottom-upalpha 强制 255
for y in (0..h).rev() {
let row = &bgra[y * w * 4..(y + 1) * w * 4];
for x in 0..w {
dib.push(row[x * 4]); // B
dib.push(row[x * 4 + 1]); // G
dib.push(row[x * 4 + 2]); // R
dib.push(255); // A
}
// 预分配像素区,按行 memcpy 拷贝(top-down bottom-up 行序翻转),再原地修正 alpha
let pixel_start = dib.len();
dib.resize(dib.len() + pixel_size, 0);
for y in 0..h {
let src = y * row_size;
let dst = (h - 1 - y) * row_size;
dib[pixel_start + dst..pixel_start + dst + row_size]
.copy_from_slice(&bgra[src..src + row_size]);
}
for i in (pixel_start + 3..pixel_start + pixel_size).step_by(4) {
dib[i] = 255;
}
dib
}
@@ -592,11 +595,17 @@ pub fn copy_png_to_clipboard(png_base64: &str) -> Result<(), String> {
write_dib_to_clipboard(&dib)
}
/// RGBA → CF_DIBBITMAPINFOHEADER + BGRA bottom-upalpha 强制 255
fn rgba_to_dib(rgba: &image::RgbaImage, width: i32, height: i32) -> Vec<u8> {
/// raw RGBA 像素 → CF_DIB直接从连续像素切片构造,无需 image crate 解码
///
/// 用于有标注导出路径:前端 canvas.getImageData() 直传 raw RGBA
/// 省去 toDataURL(PNG 编码+base64) → Rust base64 解码 → PNG 解码 的三次往返。
fn rgba_raw_to_dib(rgba: &[u8], width: i32, height: i32) -> Vec<u8> {
let header_size = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
let row_size = (width as usize) * 4;
let mut dib = Vec::with_capacity(header_size as usize + row_size * height as usize);
let w = width as usize;
let h = height as usize;
let row_size = w * 4;
let pixel_size = row_size * h;
let mut dib = Vec::with_capacity(header_size as usize + pixel_size);
let header = BITMAPINFOHEADER {
biSize: header_size,
biWidth: width,
@@ -604,7 +613,7 @@ fn rgba_to_dib(rgba: &image::RgbaImage, width: i32, height: i32) -> Vec<u8> {
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB,
biSizeImage: (row_size * height as usize) as u32,
biSizeImage: (pixel_size) as u32,
biXPelsPerMeter: 0,
biYPelsPerMeter: 0,
biClrUsed: 0,
@@ -617,14 +626,105 @@ fn rgba_to_dib(rgba: &image::RgbaImage, width: i32, height: i32) -> Vec<u8> {
)
};
dib.extend_from_slice(raw);
// bottom-up 行序,BGRA 像素,alpha 强制 255
for y in (0..height as usize).rev() {
for x in 0..width as usize {
let p = rgba.get_pixel(x as u32, y as u32).0;
dib.push(p[2]); // B
dib.push(p[1]); // G
dib.push(p[0]); // R
dib.push(255); // A
let pixel_start = dib.len();
dib.resize(dib.len() + pixel_size, 0);
// RGBA top-down → BGRA bottom-upalpha 强制 255
for y in 0..h {
let src_off = (h - 1 - y) * row_size;
let dst_off = y * row_size;
for x in 0..w {
let si = src_off + x * 4;
let di = pixel_start + dst_off + x * 4;
dib[di] = rgba[si + 2]; // B
dib[di + 1] = rgba[si + 1]; // G
dib[di + 2] = rgba[si]; // R
dib[di + 3] = 255; // A
}
}
dib
}
/// raw RGBA → PNG(直接从连续像素切片编码,跳过 image crate 的 RgbaImage 包装)
fn rgba_raw_to_png(rgba: &[u8], width: i32, height: i32) -> Result<Vec<u8>, String> {
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::ImageEncoder;
if width <= 0 || height <= 0 {
return Err("无效尺寸".into());
}
let mut buf = Vec::with_capacity(rgba.len() / 3);
PngEncoder::new_with_quality(
&mut buf,
CompressionType::Fast,
FilterType::NoFilter,
)
.write_image(rgba, width as u32, height as u32, image::ExtendedColorType::Rgba8)
.map_err(|e| format!("PNG 编码失败: {}", e))?;
Ok(buf)
}
/// 有标注导出:raw RGBA → 剪贴板 DIB + PNG base64(一次调用完成复制+编码)
///
/// 替代原 composeBase64 + screenshot_copy_image 两步:
/// 省去前端 toDataURL(PNG 编码+base64) + Rust base64 解码 + PNG 解码,
/// 直接 raw RGBA 传输 → DIB 写剪贴板 + PNG 编码返回。
pub fn compose_copy_rgba(rgba: &[u8], width: i32, height: i32) -> Result<CaptureData, String> {
let expected = (width as usize) * (height as usize) * 4;
if rgba.len() < expected {
return Err(format!("像素数据不足: {} < {}", rgba.len(), expected));
}
let dib = rgba_raw_to_dib(rgba, width, height);
write_dib_to_clipboard(&dib)?;
let png = rgba_raw_to_png(rgba, width, height)?;
Ok(CaptureData {
png_base64: base64_encode(&png),
width,
height,
})
}
/// RGBA → CF_DIBBITMAPINFOHEADER + BGRA bottom-upalpha 强制 255
fn rgba_to_dib(rgba: &image::RgbaImage, width: i32, height: i32) -> Vec<u8> {
let header_size = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
let w = width as usize;
let h = height as usize;
let row_size = w * 4;
let pixel_size = row_size * h;
let mut dib = Vec::with_capacity(header_size as usize + pixel_size);
let header = BITMAPINFOHEADER {
biSize: header_size,
biWidth: width,
biHeight: height, // 正值 = bottom-up
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB,
biSizeImage: (pixel_size) as u32,
biXPelsPerMeter: 0,
biYPelsPerMeter: 0,
biClrUsed: 0,
biClrImportant: 0,
};
let raw = unsafe {
std::slice::from_raw_parts(
&header as *const _ as *const u8,
std::mem::size_of::<BITMAPINFOHEADER>(),
)
};
dib.extend_from_slice(raw);
// 预分配像素区,用 as_raw() 连续切片替代逐像素 get_pixel
let pixel_start = dib.len();
dib.resize(dib.len() + pixel_size, 0);
let src = rgba.as_raw(); // &[u8] RGBA top-down
for y in 0..h {
// bottom-up: DIB 第一行 = 图像最后一行
let src_off = (h - 1 - y) * row_size;
let dst_off = y * row_size;
for x in 0..w {
let si = src_off + x * 4;
let di = pixel_start + dst_off + x * 4;
dib[di] = src[si + 2]; // B
dib[di + 1] = src[si + 1]; // G
dib[di + 2] = src[si]; // R
dib[di + 3] = 255; // A
}
}
dib
+33
View File
@@ -279,6 +279,39 @@ pub async fn screenshot_copy_image(png_base64: String) -> Result<(), String> {
}
}
/// 有标注导出:接收 raw RGBA(前端 canvas.getImageData 直传),一次完成 剪贴板+PNG base64。
///
/// 省去前端 toDataURL(PNG 编码+base64) → Rust base64 解码 → PNG 解码 三次往返。
/// body 格式:前 8 字节 = width(i32 LE) + height(i32 LE),之后为 raw RGBA 像素。
#[tauri::command]
pub async fn screenshot_compose_copy(
request: tauri::ipc::Request<'_>,
) -> Result<super::CaptureData, String> {
#[cfg(windows)]
{
let body = match request.body() {
tauri::ipc::InvokeBody::Raw(data) => data.clone(),
_ => return Err("需要 raw bodyArrayBuffer".into()),
};
if body.len() < 8 {
return Err("数据不足:缺少尺寸头".into());
}
let width = i32::from_le_bytes([body[0], body[1], body[2], body[3]]);
let height = i32::from_le_bytes([body[4], body[5], body[6], body[7]]);
let rgba = body[8..].to_vec();
tauri::async_runtime::spawn_blocking(move || {
super::capture::compose_copy_rgba(&rgba, width, height)
})
.await
.map_err(|e| format!("合成复制任务失败: {}", e))?
}
#[cfg(not(windows))]
{
let _ = request;
Err("截图仅支持 Windows".into())
}
}
/// 将 PNG base64 写入文件
#[tauri::command]
pub async fn screenshot_save_png(png_base64: String, path: String) -> Result<(), String> {