diff --git a/src/main.rs b/src/main.rs index deb968b..de61c91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -80,6 +80,9 @@ async fn run_server(addr: SocketAddr, relay_only: bool) -> Result<()> { ClipboardContent::Image { width, height, .. } => { println!("Received clipboard content from client: image ({}x{}), relaying to other clients...", width, height); } + ClipboardContent::Html { html, .. } => { + println!("Received clipboard content from client: html ({} bytes), relaying to other clients...", html.len()); + } } // 在只转发模式下,通过 broadcast 发送给其他客户端(保留 client_id) if let Err(e) = broadcast_tx.send(message) { @@ -91,7 +94,7 @@ async fn run_server(addr: SocketAddr, relay_only: bool) -> Result<()> { tokio::try_join!(server_handle, receive_handle)?; } else { // 正常模式:访问剪贴板 - let broadcast_for_clipboard = broadcast_tx.clone(); + // 统一的剪贴板管理任务,避免重复广播 let clipboard_handle = tokio::spawn(async move { let mut clipboard = match ClipboardMonitor::new() { Ok(c) => c, @@ -101,64 +104,86 @@ async fn run_server(addr: SocketAddr, relay_only: bool) -> Result<()> { } }; - if let Err(e) = clipboard.monitor(move |content| { - match &content { - ClipboardContent::Text(text) => { - println!("Server clipboard changed: text ({} bytes), broadcasting to clients...", text.len()); - } - ClipboardContent::Image { width, height, .. } => { - println!("Server clipboard changed: image ({}x{}), broadcasting to clients...", width, height); + let (local_tx, mut local_rx) = mpsc::unbounded_channel(); + + // 剪贴板监控任务 + let monitor_handle = { + let local_tx = local_tx.clone(); + tokio::spawn(async move { + loop { + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + if let Err(e) = local_tx.send(()) { + eprintln!("Monitor channel closed: {}", e); + break; + } } - } - let message = ClipboardMessage { - content: content.clone(), - timestamp: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(), - client_id: None, // 服务器本地的剪贴板变化没有 client_id - }; - if let Err(e) = broadcast_for_clipboard.send(message) { - eprintln!("Failed to broadcast: {}", e); - } - Ok(()) - }).await { - eprintln!("Clipboard monitor error: {}", e); - } - }); - - let receive_handle = tokio::spawn(async move { - let mut clipboard = match ClipboardMonitor::new() { - Ok(c) => c, - Err(e) => { - eprintln!("Failed to create clipboard monitor for receiving: {}", e); - return; - } + }) }; - while let Some(message) = rx.recv().await { - match &message.content { - ClipboardContent::Text(text) => { - println!( - "Received clipboard content from client: text ({} bytes)", - text.len() - ); + loop { + tokio::select! { + // 检查本地剪贴板变化 + Some(_) = local_rx.recv() => { + if let Ok(Some(content)) = clipboard.get_clipboard_content() { + match &content { + ClipboardContent::Text(text) => { + println!("Server clipboard changed: text ({} bytes), broadcasting to clients...", text.len()); + } + ClipboardContent::Image { width, height, .. } => { + println!("Server clipboard changed: image ({}x{}), broadcasting to clients...", width, height); + } + ClipboardContent::Html { html, .. } => { + println!("Server clipboard changed: html ({} bytes), broadcasting to clients...", html.len()); + } + } + let message = ClipboardMessage { + content, + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + client_id: None, // 服务器本地的剪贴板变化没有 client_id + }; + if let Err(e) = broadcast_tx.send(message) { + eprintln!("Failed to broadcast: {}", e); + } + } } - ClipboardContent::Image { width, height, .. } => { - println!( - "Received clipboard content from client: image ({}x{})", - width, height - ); + // 接收来自客户端的消息 + Some(message) = rx.recv() => { + match &message.content { + ClipboardContent::Text(text) => { + println!( + "Received clipboard content from client: text ({} bytes)", + text.len() + ); + } + ClipboardContent::Image { width, height, .. } => { + println!( + "Received clipboard content from client: image ({}x{})", + width, height + ); + } + ClipboardContent::Html { html, .. } => { + println!( + "Received clipboard content from client: html ({} bytes)", + html.len() + ); + } + } + // 更新服务器剪贴板(会同时更新 hash) + if let Err(e) = clipboard.set_clipboard_content(&message.content) { + eprintln!("Failed to set server clipboard: {}", e); + } } - } - // Update server's clipboard when receiving from client - if let Err(e) = clipboard.set_clipboard_content(&message.content) { - eprintln!("Failed to set server clipboard: {}", e); + else => break, } } + + monitor_handle.abort(); }); - tokio::try_join!(server_handle, clipboard_handle, receive_handle)?; + tokio::try_join!(server_handle, clipboard_handle)?; } Ok(()) @@ -254,6 +279,12 @@ async fn run_client(server_addr: SocketAddr, _listen_addr: SocketAddr) -> Result width, height ); } + ClipboardContent::Html { html, .. } => { + println!( + "Local clipboard changed, sending to server: html ({} bytes)", + html.len() + ); + } } if let Err(e) = to_server_tx.send(content) { eprintln!("Failed to send to server: {}", e); @@ -280,6 +311,12 @@ async fn run_client(server_addr: SocketAddr, _listen_addr: SocketAddr) -> Result width, height ); } + ClipboardContent::Html { html, .. } => { + println!( + "Received clipboard from server: html ({} bytes)", + html.len() + ); + } } // Update clipboard and hash together if let Err(e) = clipboard.set_clipboard_content(&message.content) { diff --git a/src/modules/clipboard.rs b/src/modules/clipboard.rs index 4c86c53..4ed694a 100644 --- a/src/modules/clipboard.rs +++ b/src/modules/clipboard.rs @@ -2,12 +2,15 @@ use crate::modules::sync::ClipboardContent; use anyhow::Result; use arboard::{Clipboard, ImageData}; use sha2::{Digest, Sha256}; -use std::time::Duration; -use tokio::time::sleep; #[cfg(target_os = "linux")] use std::process::Command; +// 图片大小限制:5MB +const MAX_IMAGE_SIZE: usize = 5 * 1024 * 1024; +// 图片尺寸限制:4096x4096 +const MAX_IMAGE_DIMENSION: u32 = 4096; + #[derive(Debug, Clone, Copy)] enum ClipboardBackend { Arboard, @@ -79,6 +82,11 @@ impl ClipboardMonitor { hasher.update(&width.to_le_bytes()); hasher.update(&height.to_le_bytes()); } + ClipboardContent::Html { html, text } => { + hasher.update(b"html:"); + hasher.update(html.as_bytes()); + hasher.update(text.as_bytes()); + } } format!("{:x}", hasher.finalize()) } @@ -93,18 +101,30 @@ impl ClipboardMonitor { // Try to get image first if let Ok(img) = clipboard.get_image() { - // Convert ImageData to PNG and encode as base64 - let png_data = Self::image_data_to_png(&img)?; - let base64_data = base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - &png_data, - ); - - Ok(ClipboardContent::Image { - data: base64_data, - width: img.width as u32, - height: img.height as u32, - }) + match Self::image_data_to_png(&img) { + Ok(png_data) => { + let base64_data = base64::Engine::encode( + &base64::engine::general_purpose::STANDARD, + &png_data, + ); + + Ok(ClipboardContent::Image { + data: base64_data, + width: img.width as u32, + height: img.height as u32, + }) + } + Err(e) => { + eprintln!("Failed to process image from clipboard: {}", e); + // 尝试获取文本作为备选 + clipboard + .get_text() + .map(ClipboardContent::Text) + .map_err(|e| { + anyhow::anyhow!("Failed to get clipboard content: {}", e) + }) + } + } } else { // Fall back to text clipboard @@ -116,11 +136,16 @@ impl ClipboardMonitor { #[cfg(target_os = "linux")] ClipboardBackend::WlClipboard => { // Try to get image first - if let Ok(img_data) = Self::wl_paste_image() { - Ok(img_data) - } else { - // Fall back to text - Self::wl_paste().map(ClipboardContent::Text) + match Self::wl_paste_image() { + Ok(img_data) => Ok(img_data), + Err(e) => { + // 记录图片获取失败,但不是错误(可能剪贴板中没有图片) + if !e.to_string().contains("wl-paste image failed") { + eprintln!("Failed to get image from clipboard: {}", e); + } + // Fall back to text + Self::wl_paste().map(ClipboardContent::Text) + } } } }; @@ -136,25 +161,125 @@ impl ClipboardMonitor { Ok(None) } } - Err(_) => Ok(None), + Err(e) => { + // 记录错误但不中断程序 + eprintln!("Error reading clipboard: {}", e); + Ok(None) + } } } fn image_data_to_png(img: &ImageData) -> Result> { - use image::{ImageBuffer, RgbaImage}; + use image::{DynamicImage, ImageBuffer, RgbaImage}; use std::io::Cursor; + let width = img.width as u32; + let height = img.height as u32; + + // 检查图片尺寸 + if width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION { + println!( + "Image dimensions too large: {}x{}, resizing to {}x{}", + width, height, MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION + ); + } + // Convert ImageData bytes to RgbaImage - let img_buffer: RgbaImage = - ImageBuffer::from_raw(img.width as u32, img.height as u32, img.bytes.to_vec()) - .ok_or_else(|| anyhow::anyhow!("Failed to create image buffer"))?; + let img_buffer: RgbaImage = ImageBuffer::from_raw(width, height, img.bytes.to_vec()) + .ok_or_else(|| anyhow::anyhow!("Failed to create image buffer"))?; + + let mut dynamic_img = DynamicImage::ImageRgba8(img_buffer); + + // 计算初始缩放尺寸 + let mut target_width = width.min(MAX_IMAGE_DIMENSION); + let mut target_height = height.min(MAX_IMAGE_DIMENSION); - // Encode as PNG - let mut png_data = Vec::new(); - let mut cursor = Cursor::new(&mut png_data); - img_buffer.write_to(&mut cursor, image::ImageFormat::Png)?; + // 保持宽高比 + if width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION { + let scale = (MAX_IMAGE_DIMENSION as f64 / width.max(height) as f64).min(1.0); + target_width = (width as f64 * scale) as u32; + target_height = (height as f64 * scale) as u32; + } + + // 估算大小并预先缩放 + let estimated_size = target_width as usize * target_height as usize * 4; + if estimated_size > MAX_IMAGE_SIZE * 2 { + let scale = ((MAX_IMAGE_SIZE * 2) as f64 / estimated_size as f64).sqrt(); + target_width = (target_width as f64 * scale) as u32; + target_height = (target_height as f64 * scale) as u32; + + println!( + "Pre-scaling image from {}x{} to {}x{} for size limit", + width, height, target_width, target_height + ); + } + + // 如果需要缩放 + if target_width != width || target_height != height { + dynamic_img = dynamic_img.resize( + target_width, + target_height, + image::imageops::FilterType::Lanczos3, + ); + } + + // 尝试编码,如果太大则继续缩小 + let mut attempts = 0; + let max_attempts = 3; + + loop { + attempts += 1; + + // Encode as PNG + let mut png_data = Vec::new(); + let mut cursor = Cursor::new(&mut png_data); + dynamic_img.write_to(&mut cursor, image::ImageFormat::Png)?; + + // 检查大小 + if png_data.len() <= MAX_IMAGE_SIZE { + if attempts > 1 { + println!( + "Successfully compressed image to {} bytes after {} attempts", + png_data.len(), + attempts + ); + } + return Ok(png_data); + } - Ok(png_data) + // 如果还是太大且未超过最大尝试次数 + if attempts < max_attempts { + let current_width = dynamic_img.width(); + let current_height = dynamic_img.height(); + let scale = 0.7; // 每次缩小到 70% + let new_width = (current_width as f64 * scale) as u32; + let new_height = (current_height as f64 * scale) as u32; + + println!( + "Image still too large ({} bytes), resizing from {}x{} to {}x{} (attempt {}/{})", + png_data.len(), + current_width, + current_height, + new_width, + new_height, + attempts, + max_attempts + ); + + dynamic_img = dynamic_img.resize( + new_width.max(100), // 最小保持 100px + new_height.max(100), + image::imageops::FilterType::Triangle, // 使用更快的算法 + ); + } else { + anyhow::bail!( + "Failed to compress image to size limit after {} attempts. Final size: {} bytes (max: {} bytes)", + attempts, + png_data.len(), + MAX_IMAGE_SIZE + ); + } + } } fn png_to_image_data(png_data: &[u8], width: u32, height: u32) -> Result> { @@ -194,26 +319,68 @@ impl ClipboardMonitor { .output()?; if output.status.success() && !output.stdout.is_empty() { - // Decode the PNG to get dimensions - use image::ImageReader; - use std::io::Cursor; - - let img = ImageReader::new(Cursor::new(&output.stdout)) - .with_guessed_format()? - .decode()?; - - let width = img.width(); - let height = img.height(); - - // Encode as base64 - let base64_data = - base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &output.stdout); - - Ok(ClipboardContent::Image { - data: base64_data, - width, - height, - }) + let png_data = &output.stdout; + + // 检查大小 + if png_data.len() > MAX_IMAGE_SIZE { + println!( + "Clipboard image too large ({} bytes), reprocessing...", + png_data.len() + ); + + // 解码并重新处理 + use image::ImageReader; + use std::io::Cursor; + + let img = ImageReader::new(Cursor::new(png_data)) + .with_guessed_format()? + .decode()?; + + // 转换为 ImageData 格式并使用我们的压缩逻辑 + let rgba = img.to_rgba8(); + let width = img.width(); + let height = img.height(); + + let img_data = ImageData { + width: width as usize, + height: height as usize, + bytes: std::borrow::Cow::Owned(rgba.into_raw()), + }; + + // 使用我们的压缩函数 + let compressed_png = Self::image_data_to_png(&img_data)?; + let base64_data = base64::Engine::encode( + &base64::engine::general_purpose::STANDARD, + &compressed_png, + ); + + Ok(ClipboardContent::Image { + data: base64_data, + width, + height, + }) + } else { + // 大小合适,直接使用 + use image::ImageReader; + use std::io::Cursor; + + let img = ImageReader::new(Cursor::new(png_data)) + .with_guessed_format()? + .decode()?; + + let width = img.width(); + let height = img.height(); + + // Encode as base64 + let base64_data = + base64::Engine::encode(&base64::engine::general_purpose::STANDARD, png_data); + + Ok(ClipboardContent::Image { + data: base64_data, + width, + height, + }) + } } else { anyhow::bail!("wl-paste image failed") } @@ -251,6 +418,12 @@ impl ClipboardMonitor { .set_image(img_data) .map_err(|e| anyhow::anyhow!("Failed to set clipboard image: {}", e))?; } + ClipboardContent::Html { html: _, text } => { + // arboard 不直接支持 HTML,使用纯文本回退 + clipboard.set_text(text).map_err(|e| { + anyhow::anyhow!("Failed to set clipboard HTML as text: {}", e) + })?; + } } } #[cfg(target_os = "linux")] @@ -261,6 +434,9 @@ impl ClipboardMonitor { ClipboardContent::Image { data, .. } => { Self::wl_copy_image(data)?; } + ClipboardContent::Html { html, text: _ } => { + Self::wl_copy_html(html)?; + } }, } @@ -314,15 +490,26 @@ impl ClipboardMonitor { } } - pub async fn monitor(&mut self, mut callback: F) -> Result<()> - where - F: FnMut(ClipboardContent) -> Result<()>, - { - loop { - if let Some(content) = self.get_clipboard_content()? { - callback(content)?; - } - sleep(Duration::from_millis(500)).await; + #[cfg(target_os = "linux")] + fn wl_copy_html(html: &str) -> Result<()> { + use std::io::Write; + use std::process::Stdio; + + let mut child = Command::new("wl-copy") + .arg("--type") + .arg("text/html") + .stdin(Stdio::piped()) + .spawn()?; + + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(html.as_bytes())?; + } + + let status = child.wait()?; + if status.success() { + Ok(()) + } else { + anyhow::bail!("wl-copy html failed") } } } diff --git a/src/modules/sync.rs b/src/modules/sync.rs index 779ec0b..0eb8ff4 100644 --- a/src/modules/sync.rs +++ b/src/modules/sync.rs @@ -15,6 +15,13 @@ pub enum ClipboardContent { width: u32, height: u32, }, + Html { + // HTML content + html: String, + // Plain text fallback + #[serde(default)] + text: String, + }, } #[derive(Serialize, Deserialize, Debug, Clone)]