diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e747de4..b2f3804 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,15 +2,22 @@ name: Test on: push: - branches: [master] - paths-ignore: - - "README_ZH.md" - - "README.md" + branches: + - master + paths: + - '.github/workflows/test.yml' + - 'src/**/*.rs' + - 'tests/**/*.rs' + - 'Cargo.toml' pull_request: - branches: [master] - paths-ignore: - - "README_ZH.md" - - "README.md" + types: [opened, synchronize, reopened, ready_for_review] + branches: + - '**' + paths: + - '.github/workflows/test.yml' + - 'src/**/*.rs' + - 'tests/**/*.rs' + - 'Cargo.toml' jobs: rustfmt: diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee6cbd..c487bde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v0.1.3 (2024-03-14) [released] + +- Fix: Fix the bug on `Windows` can't read DIBV5 format image from clipboard [issues#8](https://github.com/ChurchTao/clipboard-rs/issues/8) +- Fix: Fix the bug on `Windows` can't move `WatcherContext` to another thread [issues#4](https://github.com/ChurchTao/clipboard-rs/issues/4) +- Change: Demo `watch_change.rs` The callback function for monitoring changes in the clipboard is changed to implement a trait. [pr#6](https://github.com/ChurchTao/clipboard-rs/pull/6) + ## v0.1.2 (2024-03-08) [released] - Change `rust-version = "1.75.0"` to `rust-version = "1.63.0"` [pr#3](https://github.com/ChurchTao/clipboard-rs/pull/3) diff --git a/Cargo.toml b/Cargo.toml index e471a45..d083df3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clipboard-rs" -version = "0.1.2" +version = "0.1.3" authors = ["ChurchTao "] description = "Cross-platform clipboard API (text | image | rich text | html | files | monitoring changes) | 跨平台剪贴板 API(文本|图片|富文本|html|文件|监听变化) Windows,MacOS,Linux" repository = "https://github.com/ChurchTao/clipboard-rs" @@ -12,11 +12,10 @@ edition = "2021" rust-version = "1.63.0" [dependencies] -image = "0.24" +image = "0.25" [target.'cfg(target_os = "windows")'.dependencies] -clipboard-win = "5.0.0" -windows-win = "3.0.0" +clipboard-win = { version = "5.2.0", features = ["monitor"] } [target.'cfg(target_os = "macos")'.dependencies] cocoa = "0.25.0" diff --git a/README.md b/README.md index 72005f0..e94b64c 100644 --- a/README.md +++ b/README.md @@ -25,34 +25,44 @@ Add the following content to your `Cargo.toml`: ```toml [dependencies] -clipboard-rs = "0.1.2" +clipboard-rs = "0.1.3" ``` ## [CHANGELOG](CHANGELOG.md) ## Examples +### All Usage Examples + +[Examples](examples) + ### Simple Read and Write ```rust -use clipboard_rs::{Clipboard, ClipboardContext}; +use clipboard_rs::{Clipboard, ClipboardContext, ContentFormat}; fn main() { - let ctx = ClipboardContext::new().unwrap(); - let types = ctx.available_formats().unwrap(); - println!("{:?}", types); + let ctx = ClipboardContext::new().unwrap(); + let types = ctx.available_formats().unwrap(); + println!("{:?}", types); - let rtf = ctx.get_rich_text().unwrap(); + let has_rtf = ctx.has(ContentFormat::Rtf); + println!("has_rtf={}", has_rtf); - println!("rtf={}", rtf); + let rtf = ctx.get_rich_text().unwrap_or("".to_string()); - let html = ctx.get_html().unwrap(); + println!("rtf={}", rtf); - println!("html={}", html); + let has_html = ctx.has(ContentFormat::Html); + println!("has_html={}", has_html); - let content = ctx.get_text().unwrap(); + let html = ctx.get_html().unwrap_or("".to_string()); - println!("txt={}", content); + println!("html={}", html); + + let content = ctx.get_text().unwrap_or("".to_string()); + + println!("txt={}", content); } ``` @@ -62,18 +72,30 @@ fn main() { ```rust use clipboard_rs::{common::RustImage, Clipboard, ClipboardContext}; -fn main() { - let ctx = ClipboardContext::new().unwrap(); - let types = ctx.available_formats().unwrap(); - println!("{:?}", types); - - let img = ctx.get_image().unwrap(); - - img.save_to_path("/tmp/test.png").unwrap(); - - let resize_img = img.thumbnail(300, 300).unwrap(); +const TMP_PATH: &str = "/tmp/"; - resize_img.save_to_path("/tmp/test_thumbnail.png").unwrap(); +fn main() { + let ctx = ClipboardContext::new().unwrap(); + let types = ctx.available_formats().unwrap(); + println!("{:?}", types); + + let img = ctx.get_image(); + + match img { + Ok(img) => { + img.save_to_path(format!("{}test.png", TMP_PATH).as_str()) + .unwrap(); + + let resize_img = img.thumbnail(300, 300).unwrap(); + + resize_img + .save_to_path(format!("{}test_thumbnail.png", TMP_PATH).as_str()) + .unwrap(); + } + Err(err) => { + println!("err={}", err); + } + } } ``` @@ -100,30 +122,49 @@ fn main() { ### Listening to Clipboard Changes ```rust -use clipboard_rs::{Clipboard, ClipboardContext, ClipboardWatcher, ClipboardWatcherContext}; +use clipboard_rs::{ + Clipboard, ClipboardContext, ClipboardHandler, ClipboardWatcher, ClipboardWatcherContext, +}; use std::{thread, time::Duration}; +struct Manager { + ctx: ClipboardContext, +} + +impl Manager { + pub fn new() -> Self { + let ctx = ClipboardContext::new().unwrap(); + Manager { ctx } + } +} + +impl ClipboardHandler for Manager { + fn on_clipboard_change(&mut self) { + println!( + "on_clipboard_change, txt = {}", + self.ctx.get_text().unwrap() + ); + } +} + fn main() { - let ctx = ClipboardContext::new().unwrap(); - let mut watcher = ClipboardWatcherContext::new().unwrap(); + let manager = Manager::new(); - watcher.add_handler(Box::new(move || { - let content = ctx.get_text().unwrap(); - println!("read:{}", content); - })); + let mut watcher = ClipboardWatcherContext::new().unwrap(); - let watcher_shutdown = watcher.get_shutdown_channel(); + let watcher_shutdown = watcher.add_handler(manager).get_shutdown_channel(); - thread::spawn(move || { - thread::sleep(Duration::from_secs(5)); - println!("stop watch!"); - watcher_shutdown.stop(); - }); + thread::spawn(move || { + thread::sleep(Duration::from_secs(5)); + println!("stop watch!"); + watcher_shutdown.stop(); + }); - println!("start watch!"); - watcher.start_watch(); + println!("start watch!"); + watcher.start_watch(); } + ``` ## Contributing diff --git a/README_ZH.md b/README_ZH.md index c13c4e6..be33426 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -30,27 +30,37 @@ clipboard-rs = "0.1.2" ## 示例 +### 所有使用示例 + +[Examples](examples) + ### 简单读写 ```rust -use clipboard_rs::{Clipboard, ClipboardContext}; +use clipboard_rs::{Clipboard, ClipboardContext, ContentFormat}; fn main() { - let ctx = ClipboardContext::new().unwrap(); - let types = ctx.available_formats().unwrap(); - println!("{:?}", types); + let ctx = ClipboardContext::new().unwrap(); + let types = ctx.available_formats().unwrap(); + println!("{:?}", types); - let rtf = ctx.get_rich_text().unwrap(); + let has_rtf = ctx.has(ContentFormat::Rtf); + println!("has_rtf={}", has_rtf); - println!("rtf={}", rtf); + let rtf = ctx.get_rich_text().unwrap_or("".to_string()); - let html = ctx.get_html().unwrap(); + println!("rtf={}", rtf); - println!("html={}", html); + let has_html = ctx.has(ContentFormat::Html); + println!("has_html={}", has_html); - let content = ctx.get_text().unwrap(); + let html = ctx.get_html().unwrap_or("".to_string()); - println!("txt={}", content); + println!("html={}", html); + + let content = ctx.get_text().unwrap_or("".to_string()); + + println!("txt={}", content); } ``` @@ -60,18 +70,30 @@ fn main() { ```rust use clipboard_rs::{common::RustImage, Clipboard, ClipboardContext}; -fn main() { - let ctx = ClipboardContext::new().unwrap(); - let types = ctx.available_formats().unwrap(); - println!("{:?}", types); - - let img = ctx.get_image().unwrap(); - - img.save_to_path("/tmp/test.png").unwrap(); - - let resize_img = img.thumbnail(300, 300).unwrap(); +const TMP_PATH: &str = "/tmp/"; - resize_img.save_to_path("/tmp/test_thumbnail.png").unwrap(); +fn main() { + let ctx = ClipboardContext::new().unwrap(); + let types = ctx.available_formats().unwrap(); + println!("{:?}", types); + + let img = ctx.get_image(); + + match img { + Ok(img) => { + img.save_to_path(format!("{}test.png", TMP_PATH).as_str()) + .unwrap(); + + let resize_img = img.thumbnail(300, 300).unwrap(); + + resize_img + .save_to_path(format!("{}test_thumbnail.png", TMP_PATH).as_str()) + .unwrap(); + } + Err(err) => { + println!("err={}", err); + } + } } @@ -99,30 +121,49 @@ fn main() { ### 监听剪贴板变化 ```rust -use clipboard_rs::{Clipboard, ClipboardContext, ClipboardWatcher, ClipboardWatcherContext}; +use clipboard_rs::{ + Clipboard, ClipboardContext, ClipboardHandler, ClipboardWatcher, ClipboardWatcherContext, +}; use std::{thread, time::Duration}; +struct Manager { + ctx: ClipboardContext, +} + +impl Manager { + pub fn new() -> Self { + let ctx = ClipboardContext::new().unwrap(); + Manager { ctx } + } +} + +impl ClipboardHandler for Manager { + fn on_clipboard_change(&mut self) { + println!( + "on_clipboard_change, txt = {}", + self.ctx.get_text().unwrap() + ); + } +} + fn main() { - let ctx = ClipboardContext::new().unwrap(); - let mut watcher = ClipboardWatcherContext::new().unwrap(); + let manager = Manager::new(); - watcher.add_handler(Box::new(move || { - let content = ctx.get_text().unwrap(); - println!("read:{}", content); - })); + let mut watcher = ClipboardWatcherContext::new().unwrap(); - let watcher_shutdown = watcher.get_shutdown_channel(); + let watcher_shutdown = watcher.add_handler(manager).get_shutdown_channel(); - thread::spawn(move || { - thread::sleep(Duration::from_secs(5)); - println!("stop watch!"); - watcher_shutdown.stop(); - }); + thread::spawn(move || { + thread::sleep(Duration::from_secs(5)); + println!("stop watch!"); + watcher_shutdown.stop(); + }); - println!("start watch!"); - watcher.start_watch(); + println!("start watch!"); + watcher.start_watch(); } + ``` ## 贡献 diff --git a/examples/files.rs b/examples/files.rs index a463165..47609ad 100644 --- a/examples/files.rs +++ b/examples/files.rs @@ -17,6 +17,6 @@ fn main() { let has = ctx.has(ContentFormat::Files); println!("has_files={}", has); - let files = ctx.get_files().unwrap(); + let files = ctx.get_files().unwrap_or(vec![]); println!("{:?}", files); } diff --git a/examples/helloworld.rs b/examples/helloworld.rs index 12e5f3e..7c495a6 100644 --- a/examples/helloworld.rs +++ b/examples/helloworld.rs @@ -8,18 +8,18 @@ fn main() { let has_rtf = ctx.has(ContentFormat::Rtf); println!("has_rtf={}", has_rtf); - let rtf = ctx.get_rich_text().unwrap(); + let rtf = ctx.get_rich_text().unwrap_or("".to_string()); println!("rtf={}", rtf); let has_html = ctx.has(ContentFormat::Html); println!("has_html={}", has_html); - let html = ctx.get_html().unwrap(); + let html = ctx.get_html().unwrap_or("".to_string()); println!("html={}", html); - let content = ctx.get_text().unwrap(); + let content = ctx.get_text().unwrap_or("".to_string()); println!("txt={}", content); } diff --git a/examples/image.rs b/examples/image.rs index a96980c..0ddd838 100644 --- a/examples/image.rs +++ b/examples/image.rs @@ -1,15 +1,40 @@ use clipboard_rs::{common::RustImage, Clipboard, ClipboardContext}; +#[cfg(target_os = "macos")] +const TMP_PATH: &str = "/tmp/"; +#[cfg(target_os = "windows")] +const TMP_PATH: &str = "C:\\Windows\\Temp\\"; +#[cfg(all( + unix, + not(any( + target_os = "macos", + target_os = "ios", + target_os = "android", + target_os = "emscripten" + )) +))] +const TMP_PATH: &str = "/tmp/"; + fn main() { let ctx = ClipboardContext::new().unwrap(); let types = ctx.available_formats().unwrap(); println!("{:?}", types); - let img = ctx.get_image().unwrap(); + let img = ctx.get_image(); - img.save_to_path("/tmp/test.png").unwrap(); + match img { + Ok(img) => { + img.save_to_path(format!("{}test.png", TMP_PATH).as_str()) + .unwrap(); - let resize_img = img.thumbnail(300, 300).unwrap(); + let resize_img = img.thumbnail(300, 300).unwrap(); - resize_img.save_to_path("/tmp/test_thumbnail.png").unwrap(); + resize_img + .save_to_path(format!("{}test_thumbnail.png", TMP_PATH).as_str()) + .unwrap(); + } + Err(err) => { + println!("err={}", err); + } + } } diff --git a/examples/watch_change.rs b/examples/watch_change.rs index a97d8eb..84e6c56 100644 --- a/examples/watch_change.rs +++ b/examples/watch_change.rs @@ -1,16 +1,34 @@ -use clipboard_rs::{Clipboard, ClipboardContext, ClipboardWatcher, ClipboardWatcherContext}; +use clipboard_rs::{ + Clipboard, ClipboardContext, ClipboardHandler, ClipboardWatcher, ClipboardWatcherContext, +}; use std::{thread, time::Duration}; +struct Manager { + ctx: ClipboardContext, +} + +impl Manager { + pub fn new() -> Self { + let ctx = ClipboardContext::new().unwrap(); + Manager { ctx } + } +} + +impl ClipboardHandler for Manager { + fn on_clipboard_change(&mut self) { + println!( + "on_clipboard_change, txt = {}", + self.ctx.get_text().unwrap() + ); + } +} + fn main() { - let ctx = ClipboardContext::new().unwrap(); - let mut watcher = ClipboardWatcherContext::new().unwrap(); + let manager = Manager::new(); - watcher.add_handler(Box::new(move || { - let content = ctx.get_text().unwrap(); - println!("read:{}", content); - })); + let mut watcher = ClipboardWatcherContext::new().unwrap(); - let watcher_shutdown = watcher.get_shutdown_channel(); + let watcher_shutdown = watcher.add_handler(manager).get_shutdown_channel(); thread::spawn(move || { thread::sleep(Duration::from_secs(5)); diff --git a/src/common.rs b/src/common.rs index 39d8ab8..c89171b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,9 +1,8 @@ use image::imageops::FilterType; -use image::{self, DynamicImage, GenericImageView}; +use image::{DynamicImage, GenericImageView, ImageFormat}; use std::error::Error; use std::io::Cursor; pub type Result = std::result::Result>; -pub type CallBack = Box; pub trait ContentData { fn get_format(&self) -> ContentFormat; @@ -13,6 +12,10 @@ pub trait ContentData { fn as_str(&self) -> Result<&str>; } +pub trait ClipboardHandler { + fn on_clipboard_change(&mut self); +} + pub enum ClipboardContent { Text(String), Rtf(String), @@ -39,7 +42,8 @@ impl ContentData for ClipboardContent { ClipboardContent::Text(data) => data.as_bytes(), ClipboardContent::Rtf(data) => data.as_bytes(), ClipboardContent::Html(data) => data.as_bytes(), - ClipboardContent::Image(data) => data.as_bytes(), + // dynamic image is not supported to as bytes + ClipboardContent::Image(_) => &[], ClipboardContent::Files(data) => { // use first file path as data if let Some(path) = data.first() { @@ -87,31 +91,9 @@ pub struct RustImageData { data: Option, } -macro_rules! handle_image_operation { - ($self:expr, $operation:expr) => { - match &$self.data { - Some(image) => { - let mut buf = Cursor::new(Vec::new()); - image.write_to(&mut buf, $operation)?; - Ok(RustImageBuffer(buf.into_inner())) - } - None => Err("image is empty".into()), - } - }; -} - /// 此处的 RustImageBuffer 已经是带有图片格式的字节流,例如 png,jpeg; pub struct RustImageBuffer(Vec); -impl RustImageData { - pub fn as_bytes(&self) -> &[u8] { - match &self.data { - Some(image) => image.as_bytes(), - None => &[], - } - } -} - pub trait RustImage: Sized { /// create an empty image fn empty() -> Self; @@ -124,6 +106,8 @@ pub trait RustImage: Sized { /// Create a new image from a byte slice fn from_bytes(bytes: &[u8]) -> Result; + fn from_dynamic_image(image: DynamicImage) -> Self; + /// width and height fn get_size(&self) -> (u32, u32); @@ -141,10 +125,7 @@ pub trait RustImage: Sized { /// zh: 调整图片大小,不保留长宽比 fn resize(&self, width: u32, height: u32, filter: FilterType) -> Result; - /// en: Convert image to jpeg format, quality is the quality, give a value of 0-100, 100 is the highest quality, - /// the returned image is a new image, and the data itself will not be modified - /// zh: 把图片转为 jpeg 格式,quality(0-100) 为质量,输出字节数组,可直接通过 io 写入文件 - fn to_jpeg(&self, quality: u8) -> Result; + fn to_jpeg(&self) -> Result; /// en: Convert to png format, the returned image is a new image, and the data itself will not be modified /// zh: 转为 png 格式,返回的为新的图片,本身数据不会修改 @@ -168,8 +149,8 @@ impl RustImage for RustImageData { self.data.is_none() } - fn from_bytes(bytes: &[u8]) -> Result { - let image = image::load_from_memory(bytes)?; + fn from_path(path: &str) -> Result { + let image = image::open(path)?; let (width, height) = image.dimensions(); Ok(RustImageData { width, @@ -178,8 +159,8 @@ impl RustImage for RustImageData { }) } - fn from_path(path: &str) -> Result { - let image = image::open(path)?; + fn from_bytes(bytes: &[u8]) -> Result { + let image = image::load_from_memory(bytes)?; let (width, height) = image.dimensions(); Ok(RustImageData { width, @@ -188,14 +169,23 @@ impl RustImage for RustImageData { }) } + fn from_dynamic_image(image: DynamicImage) -> Self { + let (width, height) = image.dimensions(); + RustImageData { + width, + height, + data: Some(image), + } + } + fn get_size(&self) -> (u32, u32) { (self.width, self.height) } - fn resize(&self, width: u32, height: u32, filter: FilterType) -> Result { + fn thumbnail(&self, width: u32, height: u32) -> Result { match &self.data { Some(image) => { - let resized = image.resize_exact(width, height, filter); + let resized = image.thumbnail(width, height); Ok(RustImageData { width: resized.width(), height: resized.height(), @@ -206,20 +196,10 @@ impl RustImage for RustImageData { } } - fn save_to_path(&self, path: &str) -> Result<()> { - match &self.data { - Some(image) => { - image.save(path)?; - Ok(()) - } - None => Err("image is empty".into()), - } - } - - fn thumbnail(&self, width: u32, height: u32) -> Result { + fn resize(&self, width: u32, height: u32, filter: FilterType) -> Result { match &self.data { Some(image) => { - let resized = image.thumbnail(width, height); + let resized = image.resize_exact(width, height, filter); Ok(RustImageData { width: resized.width(), height: resized.height(), @@ -230,16 +210,47 @@ impl RustImage for RustImageData { } } - fn to_jpeg(&self, quality: u8) -> Result { - handle_image_operation!(self, image::ImageOutputFormat::Jpeg(quality)) + fn to_jpeg(&self) -> Result { + match &self.data { + Some(image) => { + let mut buf = Cursor::new(Vec::new()); + image.write_to(&mut buf, ImageFormat::Jpeg)?; + Ok(RustImageBuffer(buf.into_inner())) + } + None => Err("image is empty".into()), + } } fn to_png(&self) -> Result { - handle_image_operation!(self, image::ImageOutputFormat::Png) + match &self.data { + Some(image) => { + let mut buf = Cursor::new(Vec::new()); + image.write_to(&mut buf, ImageFormat::Png)?; + Ok(RustImageBuffer(buf.into_inner())) + } + None => Err("image is empty".into()), + } } fn to_bitmap(&self) -> Result { - handle_image_operation!(self, image::ImageOutputFormat::Bmp) + match &self.data { + Some(image) => { + let mut buf = Cursor::new(Vec::new()); + image.write_to(&mut buf, ImageFormat::Bmp)?; + Ok(RustImageBuffer(buf.into_inner())) + } + None => Err("image is empty".into()), + } + } + + fn save_to_path(&self, path: &str) -> Result<()> { + match &self.data { + Some(image) => { + image.save(path)?; + Ok(()) + } + None => Err("image is empty".into()), + } } } diff --git a/src/lib.rs b/src/lib.rs index 741578f..4573437 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ pub mod common; mod platform; -pub use common::{CallBack, ClipboardContent, ContentFormat, Result, RustImageData}; +pub use common::{ClipboardContent, ClipboardHandler, ContentFormat, Result, RustImageData}; pub use image::imageops::FilterType; pub use platform::{ClipboardContext, ClipboardWatcherContext, WatcherShutdown}; pub trait Clipboard: Send { @@ -51,16 +51,23 @@ pub trait Clipboard: Send { fn set(&self, contents: Vec) -> Result<()>; } -pub trait ClipboardWatcher: Send { - fn add_handler(&mut self, f: CallBack) -> &mut Self; +pub trait ClipboardWatcher: Send { + /// zh: 添加一个剪切板变化处理器,可以添加多个处理器,处理器需要实现 ClipboardHandler 这个trait + /// en: Add a clipboard change handler, you can add multiple handlers, the handler needs to implement the trait ClipboardHandler + fn add_handler(&mut self, handler: T) -> &mut Self; + /// zh: 开始监视剪切板变化,这是一个阻塞方法,直到监视结束,或者调用了stop方法,所以建议在单独的线程中调用 + /// en: Start monitoring clipboard changes, this is a blocking method, until the monitoring ends, or the stop method is called, so it is recommended to call it in a separate thread fn start_watch(&mut self); + /// zh: 获得停止监视的通道,可以通过这个通道停止监视 + /// en: Get the channel to stop monitoring, you can stop monitoring through this channel fn get_shutdown_channel(&self) -> WatcherShutdown; } impl WatcherShutdown { - ///Signals shutdown + /// zh: 停止监视 + /// en: stop watching pub fn stop(self) { drop(self); } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 5fbd87e..7222bf2 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,5 +1,5 @@ -use crate::common::{CallBack, ContentData, Result, RustImage, RustImageData}; -use crate::{Clipboard, ClipboardContent, ClipboardWatcher, ContentFormat}; +use crate::common::{ContentData, Result, RustImage, RustImageData}; +use crate::{Clipboard, ClipboardContent, ClipboardHandler, ClipboardWatcher, ContentFormat}; use cocoa::appkit::{ NSFilenamesPboardType, NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypePNG, NSPasteboardTypeRTF, NSPasteboardTypeString, @@ -17,18 +17,18 @@ pub struct ClipboardContext { clipboard: id, } -pub struct ClipboardWatcherContext { +pub struct ClipboardWatcherContext { clipboard: id, - handlers: Vec, + handlers: Vec, stop_signal: Sender<()>, stop_receiver: Receiver<()>, running: bool, } -unsafe impl Send for ClipboardWatcherContext {} +unsafe impl Send for ClipboardWatcherContext {} -impl ClipboardWatcherContext { - pub fn new() -> Result { +impl ClipboardWatcherContext { + pub fn new() -> Result { let ns_pasteboard = unsafe { NSPasteboard::generalPasteboard(nil) }; let (tx, rx) = mpsc::channel(); Ok(ClipboardWatcherContext { @@ -41,9 +41,9 @@ impl ClipboardWatcherContext { } } -impl ClipboardWatcher for ClipboardWatcherContext { - fn add_handler(&mut self, f: CallBack) -> &mut Self { - self.handlers.push(f); +impl ClipboardWatcher for ClipboardWatcherContext { + fn add_handler(&mut self, handler: T) -> &mut Self { + self.handlers.push(handler); self } @@ -52,6 +52,10 @@ impl ClipboardWatcher for ClipboardWatcherContext { println!("already start watch!"); return; } + if self.handlers.is_empty() { + println!("no handler, no need to start watch!"); + return; + } self.running = true; let mut last_change_count: i64 = unsafe { self.clipboard.changeCount() }; loop { @@ -67,9 +71,9 @@ impl ClipboardWatcher for ClipboardWatcherContext { if last_change_count == 0 { last_change_count = change_count; } else if change_count != last_change_count { - self.handlers.iter().for_each(|handler| { - handler(); - }); + self.handlers + .iter_mut() + .for_each(|handler| handler.on_clipboard_change()); last_change_count = change_count; } } @@ -427,6 +431,20 @@ impl ClipboardContent { format: ContentFormat::Files, } } + ClipboardContent::Image(image) => { + let png = image.to_png()?; + WriteToClipboardData { + data: unsafe { + NSData::dataWithBytes_length_( + nil, + png.get_bytes().as_ptr() as *const c_void, + png.get_bytes().len() as u64, + ) + }, + is_multi: false, + format: ContentFormat::Image, + } + } _ => WriteToClipboardData { data: unsafe { NSData::dataWithBytes_length_( diff --git a/src/platform/win.rs b/src/platform/win.rs index d9889a6..7f4b6f3 100644 --- a/src/platform/win.rs +++ b/src/platform/win.rs @@ -1,18 +1,21 @@ -use std::collections::HashMap; - -use crate::common::{CallBack, ContentData, Result, RustImage, RustImageData}; -use crate::{Clipboard, ClipboardContent, ClipboardWatcher, ContentFormat}; +use crate::common::{ContentData, Result, RustImage, RustImageData}; +use crate::{Clipboard, ClipboardContent, ClipboardHandler, ClipboardWatcher, ContentFormat}; +use clipboard_win::formats::CF_DIBV5; use clipboard_win::raw::set_without_clear; use clipboard_win::types::c_uint; use clipboard_win::{ - formats, get, get_clipboard, raw, set_clipboard, Clipboard as ClipboardWin, Setter, SysResult, + formats, get, get_clipboard, raw, set_clipboard, Clipboard as ClipboardWin, Monitor, Setter, + SysResult, }; use image::EncodableLayout; -use windows_win::sys::{ - AddClipboardFormatListener, PostMessageW, RemoveClipboardFormatListener, HWND, - WM_CLIPBOARDUPDATE, -}; -use windows_win::{Messages, Window}; +use std::collections::HashMap; +use std::sync::mpsc::{Receiver, Sender}; +use std::thread; +use std::time::Duration; + +pub struct WatcherShutdown { + stop_signal: Sender<()>, +} static UNKNOWN_FORMAT: &str = "unknown format"; static CF_RTF: &str = "Rich Text Format"; @@ -21,62 +24,30 @@ static CF_PNG: &str = "PNG"; pub struct ClipboardContext { format_map: HashMap<&'static str, c_uint>, + html_format: formats::Html, } -pub struct ClipboardWatcherContext { - handlers: Vec, - window: Window, +pub struct ClipboardWatcherContext { + handlers: Vec, + stop_signal: Sender<()>, + stop_receiver: Receiver<()>, + running: bool, } unsafe impl Send for ClipboardContext {} unsafe impl Sync for ClipboardContext {} -unsafe impl Send for ClipboardWatcherContext {} - -pub struct WatcherShutdown { - window: HWND, -} - -impl Drop for WatcherShutdown { - fn drop(&mut self) { - unsafe { PostMessageW(self.window, WM_CLIPBOARDUPDATE, 0, -1) }; - } -} - -unsafe impl Send for WatcherShutdown {} - -pub struct ClipboardListener(HWND); - -impl ClipboardListener { - pub fn new(window: HWND) -> Result { - unsafe { - if AddClipboardFormatListener(window) != 1 { - Err("AddClipboardFormatListener failed".into()) - } else { - Ok(ClipboardListener(window)) - } - } - } -} - -impl Drop for ClipboardListener { - fn drop(&mut self) { - unsafe { - RemoveClipboardFormatListener(self.0); - } - } -} +unsafe impl Send for ClipboardWatcherContext {} +unsafe impl Sync for ClipboardWatcherContext {} impl ClipboardContext { pub fn new() -> Result { - let window = core::ptr::null_mut(); - let _ = ClipboardWin::new_attempts_for(window, 10).expect("Open clipboard"); - let format_map = { - let cf_html_uint = clipboard_win::register_format(CF_HTML); + let (format_map, html_format) = { + let cf_html_format = formats::Html::new(); let cf_rtf_uint = clipboard_win::register_format(CF_RTF); let cf_png_uint = clipboard_win::register_format(CF_PNG); let mut m: HashMap<&str, c_uint> = HashMap::new(); - if let Some(cf_html) = cf_html_uint { - m.insert(CF_HTML, cf_html.get()); + if let Some(cf_html) = cf_html_format { + m.insert(CF_HTML, cf_html.code()); } if let Some(cf_rtf) = cf_rtf_uint { m.insert(CF_RTF, cf_rtf.get()); @@ -84,9 +55,12 @@ impl ClipboardContext { if let Some(cf_png) = cf_png_uint { m.insert(CF_PNG, cf_png.get()); } - m + (m, cf_html_format) }; - Ok(ClipboardContext { format_map }) + Ok(ClipboardContext { + format_map, + html_format: html_format.ok_or("register html format error")?, + }) } fn get_format(&self, format: &ContentFormat) -> c_uint { @@ -94,33 +68,29 @@ impl ClipboardContext { ContentFormat::Text => formats::CF_UNICODETEXT, ContentFormat::Rtf => *self.format_map.get(CF_RTF).unwrap(), ContentFormat::Html => *self.format_map.get(CF_HTML).unwrap(), - ContentFormat::Image => *self.format_map.get(CF_PNG).unwrap(), + ContentFormat::Image => formats::CF_DIB, ContentFormat::Files => formats::CF_HDROP, ContentFormat::Other(format) => clipboard_win::register_format(format).unwrap().get(), } } } -impl ClipboardWatcherContext { - pub fn new() -> Result { - let window = match Window::from_builder( - windows_win::raw::window::Builder::new() - .class_name("STATIC") - .parent_message(), - ) { - Ok(window) => window, - Err(_) => return Err("create window error".into()), - }; +impl ClipboardWatcherContext { + pub fn new() -> Result { + let (tx, rx) = std::sync::mpsc::channel(); Ok(Self { handlers: Vec::new(), - window, + stop_signal: tx, + stop_receiver: rx, + running: false, }) } } impl Clipboard for ClipboardContext { fn available_formats(&self) -> Result> { - let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); + let _clip = ClipboardWin::new_attempts(10) + .map_err(|code| format!("Open clipboard error, code = {}", code)); let format_count = clipboard_win::count_formats(); if format_count.is_none() { return Ok(Vec::new()); @@ -154,6 +124,7 @@ impl Clipboard for ClipboardContext { // Currently only judge whether there is a png format let cf_png_uint = self.format_map.get(CF_PNG).unwrap(); clipboard_win::is_format_avail(*cf_png_uint) + || clipboard_win::is_format_avail(formats::CF_DIB) } ContentFormat::Files => clipboard_win::is_format_avail(formats::CF_HDROP), ContentFormat::Other(format) => { @@ -167,10 +138,11 @@ impl Clipboard for ClipboardContext { } fn clear(&self) -> Result<()> { - let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); + let _clip = ClipboardWin::new_attempts(10) + .map_err(|code| format!("Open clipboard error, code = {}", code)); let res = clipboard_win::empty(); - if res.is_err() { - return Err("clear clipboard error".into()); + if let Err(e) = res { + return Err(format!("Empty clipboard error, code = {}", e).into()); } Ok(()) } @@ -184,7 +156,7 @@ impl Clipboard for ClipboardContext { let buffer = get_clipboard(formats::RawData(format_uint)); match buffer { Ok(data) => Ok(data), - Err(_) => Err("get buffer error".into()), + Err(e) => Err(format!("Get buffer error, code = {}", e).into()), } } @@ -192,37 +164,34 @@ impl Clipboard for ClipboardContext { let string: SysResult = get_clipboard(formats::Unicode); match string { Ok(s) => Ok(s), - Err(_) => Ok("".to_string()), + Err(e) => Err(format!("Get text error, code = {}", e).into()), } } fn get_rich_text(&self) -> Result { - let rtf_raw_data = self.get_buffer(CF_RTF); - match rtf_raw_data { - Ok(data) => { - let rtf = String::from_utf8(data); - match rtf { - Ok(s) => Ok(s), - Err(_) => Ok("".to_string()), - } - } - Err(_) => Ok("".to_string()), - } + let rtf_raw_data = self.get_buffer(CF_RTF)?; + Ok(String::from_utf8(rtf_raw_data)?) } fn get_html(&self) -> Result { - let html_raw_data = self.get_buffer(CF_HTML); - match html_raw_data { - Ok(data) => cf_html_to_plain_html(data), - Err(_) => Ok("".to_string()), + let res: SysResult = get_clipboard(self.html_format); + match res { + Ok(html) => Ok(html), + Err(e) => Err(format!("Get html error, code = {}", e).into()), } } fn get_image(&self) -> Result { - let image_raw_data = self.get_buffer(CF_PNG); - match image_raw_data { - Ok(data) => RustImageData::from_bytes(&data), - Err(_) => Ok(RustImageData::empty()), + let has_bmp: bool = clipboard_win::is_format_avail(formats::CF_DIB); + if has_bmp { + let res = get_clipboard(formats::RawData(formats::CF_DIBV5)); + match res { + Ok(data) => RustImageData::from_bytes(&data), + Err(e) => Err(format!("Get image error, code = {}", e).into()), + } + } else { + let image_raw_data = self.get_buffer(CF_PNG)?; + RustImageData::from_bytes(&image_raw_data) } } @@ -230,12 +199,13 @@ impl Clipboard for ClipboardContext { let files: SysResult> = get_clipboard(formats::FileList); match files { Ok(f) => Ok(f), - Err(_) => Ok(Vec::new()), + Err(e) => Err(format!("Get files error, code = {}", e).into()), } } fn get(&self, formats: &[ContentFormat]) -> Result> { - let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); + let _clip = ClipboardWin::new_attempts(10) + .map_err(|code| format!("Open clipboard error, code = {}", code)); let mut res = Vec::new(); for format in formats { match format { @@ -260,23 +230,19 @@ impl Clipboard for ClipboardContext { } } ContentFormat::Html => { - let format_uint = self.get_format(format); - let buffer = get(formats::RawData(format_uint)); - match buffer { - Ok(buffer) => { - let html = cf_html_to_plain_html(buffer)?; + let html_res: SysResult = get(self.html_format); + match html_res { + Ok(html) => { res.push(ClipboardContent::Html(html)); } Err(_) => continue, } } ContentFormat::Image => { - let format_uint = self.get_format(format); - let buffer = get(formats::RawData(format_uint)); - match buffer { - Ok(buffer) => { - let image = RustImage::from_bytes(&buffer)?; - res.push(ClipboardContent::Image(image)); + let img = self.get_image(); + match img { + Ok(img) => { + res.push(ClipboardContent::Image(img)); } Err(_) => continue, } @@ -320,49 +286,35 @@ impl Clipboard for ClipboardContext { fn set_text(&self, text: String) -> Result<()> { let res = set_clipboard(formats::Unicode, text); - if res.is_err() { - return Err("set text error".into()); - } - Ok(()) + res.map_err(|e| format!("set text error, code = {}", e).into()) } fn set_rich_text(&self, text: String) -> Result<()> { let res = self.set_buffer(CF_RTF, text.as_bytes().to_vec()); - if res.is_err() { - return Err("set rich text error".into()); - } - Ok(()) + res.map_err(|e| format!("set rich text error, code = {}", e).into()) } fn set_html(&self, html: String) -> Result<()> { - let cf_html = plain_html_to_cf_html(&html); - let res = self.set_buffer(CF_HTML, cf_html.as_bytes().to_vec()); - if res.is_err() { - return Err("set html error".into()); - } - Ok(()) + let res = set_clipboard(self.html_format, &html); + res.map_err(|e| format!("set html error, code = {}", e).into()) } fn set_image(&self, image: RustImageData) -> Result<()> { - let png = image.to_png()?; - let res = self.set_buffer(CF_PNG, png.get_bytes().to_vec()); - if res.is_err() { - return Err("set image error".into()); - } - Ok(()) + let bmp = image.to_bitmap()?; + let res = set_clipboard(formats::RawData(CF_DIBV5), bmp.get_bytes()); + res.map_err(|e| format!("set image error, code = {}", e).into()) } fn set_files(&self, files: Vec) -> Result<()> { - let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); + let _clip = ClipboardWin::new_attempts(10) + .map_err(|code| format!("Open clipboard error, code = {}", code)); let res = formats::FileList.write_clipboard(&files); - if res.is_err() { - return Err("set files error".into()); - } - Ok(()) + res.map_err(|e| format!("set files error, code = {}", e).into()) } fn set(&self, contents: Vec) -> Result<()> { - let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); + let _clip = ClipboardWin::new_attempts(10) + .map_err(|code| format!("Open clipboard error, code = {}", code)); for content in contents { match content { ClipboardContent::Text(txt) => { @@ -373,10 +325,20 @@ impl Clipboard for ClipboardContext { continue; } } - ClipboardContent::Rtf(_) - | ClipboardContent::Html(_) - | ClipboardContent::Image(_) - | ClipboardContent::Other(_, _) => { + ClipboardContent::Html(html) => { + let res = set_clipboard(self.html_format, &html); + if res.is_err() { + continue; + } + } + ClipboardContent::Image(img) => { + // set image will clear clipboard + let res = self.set_image(img); + if res.is_err() { + continue; + } + } + ClipboardContent::Rtf(_) | ClipboardContent::Other(_, _) => { let format_uint = self.get_format(&content.get_format()); let res = set_without_clear(format_uint, content.as_bytes()); if res.is_err() { @@ -395,149 +357,61 @@ impl Clipboard for ClipboardContext { } } -impl ClipboardWatcher for ClipboardWatcherContext { - fn add_handler(&mut self, f: CallBack) -> &mut Self { +impl ClipboardWatcher for ClipboardWatcherContext { + fn add_handler(&mut self, f: T) -> &mut Self { self.handlers.push(f); self } fn start_watch(&mut self) { - let _guard = ClipboardListener::new(self.window.inner()).unwrap(); - for msg in Messages::new() - .window(Some(self.window.inner())) - .low(Some(WM_CLIPBOARDUPDATE)) - .high(Some(WM_CLIPBOARDUPDATE)) - { + if self.running { + println!("already start watch!"); + return; + } + if self.handlers.is_empty() { + println!("no handler, no need to start watch!"); + return; + } + self.running = true; + let mut monitor = Monitor::new().expect("create monitor error"); + let shutdown = monitor.shutdown_channel(); + loop { + if self.stop_receiver.try_recv().is_ok() { + break; + } + let msg = monitor.try_recv(); match msg { - Ok(msg) => match msg.id() { - WM_CLIPBOARDUPDATE => { - let msg = msg.inner(); - - //Shutdown requested - if msg.lParam == -1 { - break; - } - self.handlers.iter().for_each(|handler| { - handler(); - }); - } - _ => panic!("Unexpected message"), - }, + Ok(true) => { + self.handlers.iter_mut().for_each(|f| { + f.on_clipboard_change(); + }); + } + Ok(false) => { + // no change + thread::park_timeout(Duration::from_millis(200)); + continue; + } Err(e) => { - println!("msg: error: {:?}", e); + eprintln!("watch error, code = {}", e); + break; } } } + drop(shutdown); + self.running = false; } fn get_shutdown_channel(&self) -> WatcherShutdown { WatcherShutdown { - window: self.window.inner(), + stop_signal: self.stop_signal.clone(), } } } -// https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format -// The description header includes the clipboard version number and offsets, indicating where the context and the fragment start and end. The description is a list of ASCII text keywords followed by a string and separated by a colon (:). -// Version: vv version number of the clipboard. Starting version is . As of Windows 10 20H2 this is now .Version:0.9Version:1.0 -// StartHTML: Offset (in bytes) from the beginning of the clipboard to the start of the context, or if no context.-1 -// EndHTML: Offset (in bytes) from the beginning of the clipboard to the end of the context, or if no context.-1 -// StartFragment: Offset (in bytes) from the beginning of the clipboard to the start of the fragment. -// EndFragment: Offset (in bytes) from the beginning of the clipboard to the end of the fragment. -// StartSelection: Optional. Offset (in bytes) from the beginning of the clipboard to the start of the selection. -// EndSelection: Optional. Offset (in bytes) from the beginning of the clipboard to the end of the selection. -// The and keywords are optional and must both be omitted if you do not want the application to generate this information.StartSelectionEndSelection -// Future revisions of the clipboard format may extend the header, for example, since the HTML starts at the offset then multiple and pairs could be added later to support noncontiguous selection of fragments.CF_HTMLStartHTMLStartFragmentEndFragment -// example: -// html=Version:1.0 -// StartHTML:000000096 -// EndHTML:000000375 -// StartFragment:000000096 -// EndFragment:000000375 -//
sellChannel
-fn cf_html_to_plain_html(cf_html: Vec) -> Result { - let cf_html_str = String::from_utf8(cf_html)?; - let cf_html_bytes = cf_html_str.as_bytes(); - let mut start_fragment_offset_in_bytes = 0; - let mut end_fragment_offset_in_bytes = 0; - for line in cf_html_str.lines() { - match line.split_once(':') { - Some((k, v)) => match k { - "StartFragment" => { - start_fragment_offset_in_bytes = v.trim_start_matches('0').parse::()?; - } - "EndFragment" => { - end_fragment_offset_in_bytes = v.trim_start_matches('0').parse::()?; - } - _ => {} - }, - None => { - if start_fragment_offset_in_bytes != 0 && end_fragment_offset_in_bytes != 0 { - return Ok(String::from_utf8( - cf_html_bytes[start_fragment_offset_in_bytes..end_fragment_offset_in_bytes] - .to_vec(), - )?); - } - } - } +impl Drop for WatcherShutdown { + fn drop(&mut self) { + let _ = self.stop_signal.send(()); } - // if no StartFragment and EndFragment, return the whole html - Ok(cf_html_str) -} - -// cp from https://github.com/Devolutions/IronRDP/blob/37aa6426dba3272f38a2bb46a513144a326854ee/crates/ironrdp-cliprdr-format/src/html.rs#L91 -fn plain_html_to_cf_html(fragment: &str) -> String { - const POS_PLACEHOLDER: &str = "0000000000"; - - let mut buffer = String::new(); - - let mut write_header = |key: &str, value: &str| { - let size = key.len() + value.len() + ":\r\n".len(); - buffer.reserve(size); - - buffer.push_str(key); - buffer.push(':'); - let value_pos = buffer.len(); - buffer.push_str(value); - buffer.push_str("\r\n"); - - value_pos - }; - - write_header("Version", "0.9"); - - let start_html_header_value_pos = write_header("StartHTML", POS_PLACEHOLDER); - let end_html_header_value_pos = write_header("EndHTML", POS_PLACEHOLDER); - let start_fragment_header_value_pos = write_header("StartFragment", POS_PLACEHOLDER); - let end_fragment_header_value_pos = write_header("EndFragment", POS_PLACEHOLDER); - - let start_html_pos = buffer.len(); - buffer.push_str("\r\n\r\n"); - - let start_fragment_pos = buffer.len(); - buffer.push_str(fragment); - - let end_fragment_pos = buffer.len(); - buffer.push_str("\r\n\r\n"); - - let end_html_pos = buffer.len(); - - let start_html_pos_value = format!("{:0>10}", start_html_pos); - let end_html_pos_value = format!("{:0>10}", end_html_pos); - let start_fragment_pos_value = format!("{:0>10}", start_fragment_pos); - let end_fragment_pos_value = format!("{:0>10}", end_fragment_pos); - - let mut replace_placeholder = |value_begin_idx: usize, header_value: &str| { - let value_end_idx = value_begin_idx + POS_PLACEHOLDER.len(); - buffer.replace_range(value_begin_idx..value_end_idx, header_value); - }; - - replace_placeholder(start_html_header_value_pos, &start_html_pos_value); - replace_placeholder(end_html_header_value_pos, &end_html_pos_value); - replace_placeholder(start_fragment_header_value_pos, &start_fragment_pos_value); - replace_placeholder(end_fragment_header_value_pos, &end_fragment_pos_value); - - buffer } /// 将输入的 UTF-8 字符串转换为宽字符(UTF-16)字符串 diff --git a/src/platform/x11.rs b/src/platform/x11.rs index dd34433..2bb7414 100644 --- a/src/platform/x11.rs +++ b/src/platform/x11.rs @@ -1,8 +1,8 @@ use crate::{ common::{Result, RustImage}, - ClipboardContent, ContentFormat, RustImageData, + ClipboardContent, ClipboardHandler, ContentFormat, RustImageData, }; -use crate::{CallBack, Clipboard, ClipboardWatcher}; +use crate::{Clipboard, ClipboardWatcher}; use std::sync::mpsc::{self, Receiver, Sender}; use std::{ sync::{Arc, RwLock}, @@ -672,13 +672,15 @@ impl Clipboard for ClipboardContext { } } -pub struct ClipboardWatcherContext { - handlers: Vec, +pub struct ClipboardWatcherContext { + handlers: Vec, stop_signal: Sender<()>, stop_receiver: Receiver<()>, } -impl ClipboardWatcherContext { +unsafe impl Send for ClipboardWatcherContext {} + +impl ClipboardWatcherContext { pub fn new() -> Result { let (tx, rx) = mpsc::channel(); Ok(Self { @@ -689,8 +691,8 @@ impl ClipboardWatcherContext { } } -impl ClipboardWatcher for ClipboardWatcherContext { - fn add_handler(&mut self, f: crate::CallBack) -> &mut Self { +impl ClipboardWatcher for ClipboardWatcherContext { + fn add_handler(&mut self, f: T) -> &mut Self { self.handlers.push(f); self } @@ -737,7 +739,9 @@ impl ClipboardWatcher for ClipboardWatcherContext { } }; if let Event::XfixesSelectionNotify(_) = event { - self.handlers.iter().for_each(|f| f()); + self.handlers + .iter_mut() + .for_each(|handler| handler.on_clipboard_change()); } } } diff --git a/tests/image_test.rs b/tests/image_test.rs index 1785844..33e1b7e 100644 --- a/tests/image_test.rs +++ b/tests/image_test.rs @@ -2,24 +2,16 @@ use clipboard_rs::{ common::{RustImage, RustImageData}, Clipboard, ClipboardContext, ContentFormat, }; -use image::{ImageBuffer, Rgba, RgbaImage}; -use std::io::Cursor; #[test] fn test_image() { let ctx = ClipboardContext::new().unwrap(); - // 创建一个 100x100 大小的纯红色图像 - let width = 100; - let height = 100; - let image_buffer: RgbaImage = - ImageBuffer::from_fn(width, height, |_x, _y| Rgba([255u8, 0u8, 0u8, 255u8])); - let mut buf = Cursor::new(Vec::new()); - image_buffer - .write_to(&mut buf, image::ImageOutputFormat::Png) - .expect("Failed to encode image as PNG"); + let rust_img = RustImageData::from_path("tests/test.png").unwrap(); - let rust_img = RustImageData::from_bytes(&buf.clone().into_inner()).unwrap(); + let binding = RustImageData::from_path("tests/test.png").unwrap(); + + let rust_img_bytes = binding.to_png().unwrap(); ctx.set_image(rust_img).unwrap(); @@ -28,7 +20,7 @@ fn test_image() { let clipboard_img = ctx.get_image().unwrap(); assert_eq!( - clipboard_img.to_png().unwrap().get_bytes(), - &buf.into_inner() + clipboard_img.to_png().unwrap().get_bytes().len(), + rust_img_bytes.get_bytes().len() ); } diff --git a/tests/test.bmp b/tests/test.bmp new file mode 100644 index 0000000..5226a0b Binary files /dev/null and b/tests/test.bmp differ diff --git a/tests/test.png b/tests/test.png new file mode 100644 index 0000000..eef6125 Binary files /dev/null and b/tests/test.png differ