use std::collections::HashMap;
use std::io::Cursor;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::{mem, ptr};
use crate::common::{ContentData, Result};
#[cfg(feature = "image")]
use crate::common::{RustImage, RustImageData};
use crate::{Clipboard, ClipboardContent, ClipboardHandler, ClipboardWatcher, ContentFormat};
use clipboard_win::raw::{set_file_list_with, set_string_with, set_without_clear};
use clipboard_win::types::c_uint;
use clipboard_win::{
formats, get, get_clipboard, options, raw, set_clipboard, Clipboard as ClipboardWin, Monitor,
SysResult,
};
#[cfg(feature = "image")]
use image::codecs::bmp::BmpDecoder;
#[cfg(feature = "image")]
use image::DynamicImage;
use windows::Win32::Foundation::{HANDLE, HWND};
use windows::Win32::Graphics::Gdi::{
CreateDIBitmap, DeleteObject, GetDC, ReleaseDC, BITMAPFILEHEADER, BITMAPINFO, BITMAPINFOHEADER,
BITMAPV5HEADER, CBM_INIT, DIB_RGB_COLORS, HDC, HGDIOBJ,
};
use windows::Win32::System::DataExchange::SetClipboardData;
pub struct WatcherShutdown {
state: Arc<Mutex<ShutdownState>>,
}
static UNKNOWN_FORMAT: &str = "unknown format";
static CF_RTF: &str = "Rich Text Format";
static CF_HTML: &str = "HTML Format";
static CF_PNG: &str = "PNG";
pub struct ClipboardContext {
format_map: HashMap<&'static str, c_uint>,
html_format: formats::Html,
}
enum ShutdownState {
NotStarted,
Running(clipboard_win::monitor::Shutdown),
StopRequested,
}
pub struct ClipboardWatcherContext<T: ClipboardHandler> {
handlers: Vec<T>,
state: Arc<Mutex<ShutdownState>>,
running: bool,
}
unsafe impl Send for ClipboardContext {}
unsafe impl Sync for ClipboardContext {}
unsafe impl<T: ClipboardHandler> Send for ClipboardWatcherContext<T> {}
unsafe impl<T: ClipboardHandler> Sync for ClipboardWatcherContext<T> {}
impl ClipboardContext {
pub fn new() -> Result<ClipboardContext> {
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_format {
m.insert(CF_HTML, cf_html.code());
}
if let Some(cf_rtf) = cf_rtf_uint {
m.insert(CF_RTF, cf_rtf.get());
}
if let Some(cf_png) = cf_png_uint {
m.insert(CF_PNG, cf_png.get());
}
(m, cf_html_format)
};
Ok(ClipboardContext {
format_map,
html_format: html_format.ok_or("register html format error")?,
})
}
fn get_format(&self, format: &ContentFormat) -> c_uint {
match format {
ContentFormat::Text => formats::CF_UNICODETEXT,
ContentFormat::Rtf => *self.format_map.get(CF_RTF).unwrap(),
ContentFormat::Html => *self.format_map.get(CF_HTML).unwrap(),
#[cfg(feature = "image")]
ContentFormat::Image => formats::CF_DIB,
ContentFormat::Files => formats::CF_HDROP,
ContentFormat::Other(format) => clipboard_win::register_format(format).unwrap().get(),
}
}
}
impl<T: ClipboardHandler> ClipboardWatcherContext<T> {
pub fn new() -> Result<Self> {
Ok(Self {
handlers: Vec::new(),
state: Arc::new(Mutex::new(ShutdownState::NotStarted)),
running: false,
})
}
pub fn new_with_interval(_interval: Duration) -> Result<Self> {
Self::new()
}
}
impl Clipboard for ClipboardContext {
fn available_formats(&self) -> Result<Vec<String>> {
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());
}
let mut res = Vec::new();
let enum_formats = clipboard_win::raw::EnumFormats::new();
enum_formats.into_iter().for_each(|format| {
let f_name = raw::format_name_big(format);
match f_name {
Some(name) => res.push(name),
None => {
res.push(UNKNOWN_FORMAT.to_string());
}
}
});
Ok(res)
}
fn has(&self, format: ContentFormat) -> bool {
match format {
ContentFormat::Text => clipboard_win::is_format_avail(formats::CF_UNICODETEXT),
ContentFormat::Rtf => {
let cf_rtf_uint = self.format_map.get(CF_RTF).unwrap();
clipboard_win::is_format_avail(*cf_rtf_uint)
}
ContentFormat::Html => {
let cf_html_uint = self.format_map.get(CF_HTML).unwrap();
clipboard_win::is_format_avail(*cf_html_uint)
}
#[cfg(feature = "image")]
ContentFormat::Image => {
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) => {
let format_uint = clipboard_win::register_format(format.as_str());
if let Some(format_uint) = format_uint {
return clipboard_win::is_format_avail(format_uint.get());
}
false
}
}
}
fn clear(&self) -> Result<()> {
let _clip = ClipboardWin::new_attempts(10)
.map_err(|code| format!("Open clipboard error, code = {code}"));
let res = clipboard_win::empty();
if let Err(e) = res {
return Err(format!("Empty clipboard error, code = {e}").into());
}
Ok(())
}
fn get_buffer(&self, format: &str) -> Result<Vec<u8>> {
let format_uint = clipboard_win::register_format(format);
if format_uint.is_none() {
return Err("register format error".into());
}
let format_uint = format_uint.unwrap().get();
let buffer = get_clipboard(formats::RawData(format_uint));
match buffer {
Ok(data) => Ok(data),
Err(e) => Err(format!("Get buffer error, code = {e}").into()),
}
}
fn get_text(&self) -> Result<String> {
let string: SysResult<String> = get_clipboard(formats::Unicode);
match string {
Ok(s) => Ok(s),
Err(e) => Err(format!("Get text error, code = {e}").into()),
}
}
fn get_rich_text(&self) -> Result<String> {
let rtf_raw_data = self.get_buffer(CF_RTF)?;
Ok(String::from_utf8_lossy(&rtf_raw_data).to_string())
}
fn get_html(&self) -> Result<String> {
let buffer = get_clipboard(formats::RawData(self.html_format.code()));
match buffer {
Ok(data) => {
let html_res = String::from_utf8(data);
if let Ok(html_full_str) = html_res {
let html = extract_html_from_clipboard_data(html_full_str.as_str());
if let Ok(html) = html {
return Ok(html);
}
}
Err("Get html error".into())
}
Err(e) => Err(format!("Get buffer error, code = {e}").into()),
}
}
#[cfg(feature = "image")]
fn get_image(&self) -> Result<RustImageData> {
let cf_png_format = self.format_map.get(CF_PNG);
if cf_png_format.is_some() && clipboard_win::is_format_avail(*cf_png_format.unwrap()) {
let image_raw_data = self.get_buffer(CF_PNG)?;
RustImageData::from_bytes(&image_raw_data)
} else if clipboard_win::is_format_avail(formats::CF_DIBV5) {
let res = get_clipboard(formats::RawData(formats::CF_DIBV5));
match res {
Ok(data) => {
let decoder = {
BmpDecoder::new_without_file_header(Cursor::new(data.as_slice()))
};
let decoder = decoder.map_err(|e| format!("{e}"))?;
let dynamic_image =
DynamicImage::from_decoder(decoder).map_err(|e| format!("{e}"))?;
Ok(RustImageData::from_dynamic_image(dynamic_image))
}
Err(e) => Err(format!("Get image error, code = {e}").into()),
}
} else if clipboard_win::is_format_avail(formats::CF_DIB) {
let res = get_clipboard(formats::Bitmap);
match res {
Ok(data) => RustImageData::from_bytes(&data),
Err(e) => Err(format!("Get image error, code = {e}").into()),
}
} else {
Err("No image data in clipboard".into())
}
}
fn get_files(&self) -> Result<Vec<String>> {
let files: SysResult<Vec<String>> = get_clipboard(formats::FileList);
match files {
Ok(f) => Ok(f),
Err(e) => Err(format!("Get files error, code = {e}").into()),
}
}
fn get(&self, formats: &[ContentFormat]) -> Result<Vec<ClipboardContent>> {
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 {
ContentFormat::Text => {
let r = get(formats::Unicode);
match r {
Ok(txt) => {
res.push(ClipboardContent::Text(txt));
}
Err(_) => continue,
}
}
ContentFormat::Rtf => {
let format_uint = self.get_format(format);
let buffer = get(formats::RawData(format_uint));
match buffer {
Ok(buffer) => {
let rtf = String::from_utf8_lossy(&buffer);
res.push(ClipboardContent::Rtf(rtf.to_string()));
}
Err(_) => continue,
}
}
ContentFormat::Html => {
let html_buffer = get(formats::RawData(self.html_format.code()));
match html_buffer {
Ok(html) => {
let html_res = String::from_utf8(html);
if let Ok(html_full_str) = html_res {
let html = extract_html_from_clipboard_data(html_full_str.as_str());
if let Ok(html) = html {
res.push(ClipboardContent::Html(html));
}
}
}
Err(_) => continue,
}
}
#[cfg(feature = "image")]
ContentFormat::Image => {
let img = self.get_image();
match img {
Ok(img) => {
res.push(ClipboardContent::Image(img));
}
Err(_) => continue,
}
}
ContentFormat::Other(fmt) => {
let format_uint = self.get_format(format);
let buffer = get(formats::RawData(format_uint));
match buffer {
Ok(buffer) => {
res.push(ClipboardContent::Other(fmt.clone(), buffer));
}
Err(_) => continue,
}
}
ContentFormat::Files => {
let files = self.get_files();
match files {
Ok(files) => {
res.push(ClipboardContent::Files(files));
}
Err(_) => continue,
}
}
}
}
Ok(res)
}
fn set_buffer(&self, format: &str, buffer: Vec<u8>) -> Result<()> {
let format_uint = clipboard_win::register_format(format);
if format_uint.is_none() {
return Err("register format error".into());
}
let format_uint = format_uint.unwrap().get();
let res = set_clipboard(formats::RawData(format_uint), buffer);
if res.is_err() {
return Err("set buffer error".into());
}
Ok(())
}
fn set_text(&self, text: String) -> Result<()> {
let res = set_clipboard(formats::Unicode, text);
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());
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 = set_clipboard(
formats::RawData(self.html_format.code()),
cf_html.as_bytes(),
);
res.map_err(|e| format!("set html error, code = {e}").into())
}
#[cfg(feature = "image")]
fn set_image(&self, image: RustImageData) -> Result<()> {
let _clip = ClipboardWin::new_attempts(10)
.map_err(|code| format!("Open clipboard error, code = {code}"));
let res = clipboard_win::empty();
if let Err(e) = res {
return Err(format!("Empty clipboard error, code = {e}").into());
}
let cf_png_format = self.format_map.get(CF_PNG);
if let Some(cf_png) = cf_png_format {
let png = image.to_png()?;
if let Err(e) = set_without_clear(*cf_png, png.get_bytes()) {
eprintln!("set png image error, code = {e}");
}
}
let bmp = image
.to_bitmap()
.map_err(|e| format!("transform to bitmap error, code = {e}"))?;
set_bitmap_inner(bmp.get_bytes()).map_err(|e| format!("set image error, code = {e}").into())
}
fn set_files(&self, files: Vec<String>) -> Result<()> {
let _clip = ClipboardWin::new_attempts(10)
.map_err(|code| format!("Open clipboard error, code = {code}"));
let res = set_file_list_with(&files, options::DoClear);
res.map_err(|e| format!("set files error, code = {e}").into())
}
fn set(&self, contents: Vec<ClipboardContent>) -> Result<()> {
let _clip = ClipboardWin::new_attempts(10)
.map_err(|code| format!("Open clipboard error, code = {code}"));
let res = clipboard_win::empty();
if let Err(e) = res {
return Err(format!("Empty clipboard error, code = {e}").into());
}
for content in contents {
match content {
ClipboardContent::Text(txt) => {
let res = set_string_with(txt.as_str(), options::NoClear);
if res.is_err() {
continue;
}
}
ClipboardContent::Html(html) => {
let format_uint_html = self.html_format.code();
let cf_html = plain_html_to_cf_html(&html);
let res = set_without_clear(format_uint_html, cf_html.as_bytes());
if res.is_err() {
continue;
}
}
#[cfg(feature = "image")]
ClipboardContent::Image(img) => {
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() {
continue;
}
}
ClipboardContent::Files(file_list) => {
let res = set_file_list_with(&file_list, options::NoClear);
if res.is_err() {
continue;
}
}
}
}
Ok(())
}
}
impl<T: ClipboardHandler> ClipboardWatcher<T> for ClipboardWatcherContext<T> {
fn add_handler(&mut self, f: T) -> &mut Self {
self.handlers.push(f);
self
}
fn start_watch(&mut self) {
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 mut state = self.state.lock().unwrap();
if matches!(*state, ShutdownState::StopRequested) {
self.running = false;
return;
}
*state = ShutdownState::Running(monitor.shutdown_channel());
}
loop {
match monitor.recv() {
Ok(true) => {
self.handlers.iter_mut().for_each(|f| {
f.on_clipboard_change();
});
}
Ok(false) => break,
Err(e) => {
eprintln!("watch error, code = {e}");
break;
}
}
}
*self.state.lock().unwrap() = ShutdownState::NotStarted;
self.running = false;
}
fn get_shutdown_channel(&self) -> WatcherShutdown {
WatcherShutdown {
state: self.state.clone(),
}
}
}
impl Drop for WatcherShutdown {
fn drop(&mut self) {
let taken = {
let mut state = self.state.lock().unwrap();
std::mem::replace(&mut *state, ShutdownState::StopRequested)
};
match taken {
ShutdownState::Running(shutdown) => drop(shutdown),
ShutdownState::NotStarted | ShutdownState::StopRequested => {}
}
}
}
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();
if !fragment.starts_with("<html>") {
buffer.push_str("<html>\r\n<body>\r\n<!--StartFragment-->");
}
let start_fragment_pos = buffer.len();
buffer.push_str(fragment);
let end_fragment_pos = buffer.len();
if !fragment.ends_with("</html>") {
buffer.push_str("<!--EndFragment-->\r\n</body>\r\n</html>");
}
let end_html_pos = buffer.len();
let start_html_pos_value = format!("{start_html_pos:0>10}");
let end_html_pos_value = format!("{end_html_pos:0>10}");
let start_fragment_pos_value = format!("{start_fragment_pos:0>10}");
let end_fragment_pos_value = format!("{end_fragment_pos:0>10}");
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
}
const SEP: char = ':';
const START_HTML: &str = "StartHTML";
const END_HTML: &str = "EndHTML";
fn extract_html_from_clipboard_data(data: &str) -> Result<String> {
let mut start_idx = 0usize;
let mut end_idx = data.len();
for line in data.lines() {
let mut split = line.split(SEP);
let key = match split.next() {
Some(key) => key,
None => break,
};
let value = match split.next() {
Some(value) => value,
None => break,
};
match key {
START_HTML => match value.trim_start_matches('0').parse() {
Ok(value) => {
start_idx = value;
continue;
}
Err(_) => break,
},
END_HTML => match value.trim_start_matches('0').parse() {
Ok(value) => {
end_idx = value;
continue;
}
Err(_) => break,
},
_ => continue,
}
}
if start_idx > data.len() {
return Err("Invalid HTML offsets: start index exceeds data length".into());
}
if end_idx > data.len() {
return Err("Invalid HTML offsets: end index exceeds data length".into());
}
if end_idx < start_idx {
return Err("Invalid HTML offsets: end index before start index".into());
}
Ok(data[start_idx..end_idx].to_string())
}
fn set_bitmap_inner(data: &[u8]) -> Result<()> {
const FILE_HEADER_LEN: usize = mem::size_of::<BITMAPFILEHEADER>();
const INFO_HEADER_LEN: usize = mem::size_of::<BITMAPV5HEADER>();
if data.len() <= (FILE_HEADER_LEN + INFO_HEADER_LEN) {
return Err("Invalid bitmap data".into());
}
let mut file_header = mem::MaybeUninit::<BITMAPFILEHEADER>::uninit();
let mut info_header = mem::MaybeUninit::<BITMAPV5HEADER>::uninit();
let (file_header, info_header) = unsafe {
ptr::copy_nonoverlapping(
data.as_ptr(),
file_header.as_mut_ptr() as _,
FILE_HEADER_LEN,
);
ptr::copy_nonoverlapping(
data.as_ptr().add(FILE_HEADER_LEN),
info_header.as_mut_ptr() as _,
INFO_HEADER_LEN,
);
(file_header.assume_init(), info_header.assume_init())
};
if data.len() <= file_header.bfOffBits as usize {
return Err("Invalid bitmap data".into());
}
let bitmap = &data[file_header.bfOffBits as _..];
if bitmap.len() < info_header.bV5SizeImage as usize {
return Err("Invalid bitmap data".into());
}
let dc = DeviceContext::new()?;
let handle = unsafe {
CreateDIBitmap(
dc.0,
Some(&info_header as *const _ as *const BITMAPINFOHEADER),
CBM_INIT as u32,
Some(bitmap.as_ptr() as _),
Some(&info_header as *const _ as *const BITMAPINFO),
DIB_RGB_COLORS,
)
};
if handle.is_invalid() {
return Err("Failed to create DIB".into());
}
if let Err(err) = unsafe { SetClipboardData(formats::CF_BITMAP, Some(HANDLE(handle.0))) } {
let _ = unsafe { DeleteObject(HGDIOBJ(handle.0)) };
Err(err.into())
} else {
Ok(())
}
}
struct DeviceContext(HDC);
impl DeviceContext {
fn new() -> Result<Self> {
let dc = unsafe { GetDC(Some(HWND::default())) };
if dc.is_invalid() {
return Err("Failed to get DC".into());
}
Ok(Self(dc))
}
}
impl Drop for DeviceContext {
fn drop(&mut self) {
unsafe { ReleaseDC(Some(HWND::default()), self.0) };
}
}