#[cfg(feature = "image-data")]
use crate::common::ImageData;
use crate::common::{private, Error};
use std::{borrow::Cow, marker::PhantomData, thread, time::Duration};
#[cfg(feature = "image-data")]
mod image_data {
use super::*;
use crate::common::ScopeGuard;
use image::codecs::png::PngEncoder;
use image::ExtendedColorType;
use image::ImageEncoder;
use std::{convert::TryInto, ffi::c_void, io, mem::size_of, ptr::copy_nonoverlapping};
use windows_sys::Win32::{
Foundation::HGLOBAL,
Graphics::Gdi::{
CreateDIBitmap, DeleteObject, GetDC, GetDIBits, BITMAPINFO, BITMAPINFOHEADER,
BITMAPV5HEADER, BI_BITFIELDS, BI_RGB, CBM_INIT, DIB_RGB_COLORS, HBITMAP, HDC,
LCS_GM_IMAGES, RGBQUAD,
},
System::{
DataExchange::SetClipboardData,
Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GHND},
Ole::CF_DIBV5,
},
};
fn last_error(message: &str) -> Error {
let os_error = io::Error::last_os_error();
Error::unknown(format!("{}: {}", message, os_error))
}
unsafe fn global_unlock_checked(hdata: isize) {
if GlobalUnlock(hdata) == 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() != Some(0) {
log::error!("Failed calling GlobalUnlock when writing data: {}", err);
}
}
}
pub(super) fn add_cf_dibv5(
_open_clipboard: OpenClipboard,
image: ImageData,
) -> Result<(), Error> {
#[allow(non_upper_case_globals)]
const LCS_sRGB: u32 = 0x7352_4742;
let header_size = size_of::<BITMAPV5HEADER>();
let header = BITMAPV5HEADER {
bV5Size: header_size as u32,
bV5Width: image.width as i32,
bV5Height: image.height as i32,
bV5Planes: 1,
bV5BitCount: 32,
bV5Compression: BI_BITFIELDS,
bV5SizeImage: (4 * image.width * image.height) as u32,
bV5XPelsPerMeter: 0,
bV5YPelsPerMeter: 0,
bV5ClrUsed: 0,
bV5ClrImportant: 0,
bV5RedMask: 0x00ff0000,
bV5GreenMask: 0x0000ff00,
bV5BlueMask: 0x000000ff,
bV5AlphaMask: 0xff000000,
bV5CSType: LCS_sRGB,
bV5Endpoints: unsafe { std::mem::zeroed() },
bV5GammaRed: 0,
bV5GammaGreen: 0,
bV5GammaBlue: 0,
bV5Intent: LCS_GM_IMAGES as u32, bV5ProfileData: 0,
bV5ProfileSize: 0,
bV5Reserved: 0,
};
let image = flip_v(image);
let data_size = header_size + image.bytes.len();
let hdata = unsafe { global_alloc(data_size)? };
unsafe {
let data_ptr = global_lock(hdata)?;
let _unlock = ScopeGuard::new(|| global_unlock_checked(hdata));
copy_nonoverlapping::<u8>((&header) as *const _ as *const u8, data_ptr, header_size);
let pixels_dst = (data_ptr as usize + header_size) as *mut u8;
copy_nonoverlapping::<u8>(image.bytes.as_ptr(), pixels_dst, image.bytes.len());
let dst_pixels_slice = std::slice::from_raw_parts_mut(pixels_dst, image.bytes.len());
if let Cow::Owned(new_pixels) = rgba_to_win(dst_pixels_slice) {
copy_nonoverlapping::<u8>(new_pixels.as_ptr(), data_ptr, new_pixels.len())
}
}
if unsafe { SetClipboardData(CF_DIBV5 as u32, hdata as _) } == 0 {
unsafe { DeleteObject(hdata as _) };
Err(last_error("SetClipboardData failed with error"))
} else {
Ok(())
}
}
pub(super) fn add_png_file(image: &ImageData) -> Result<(), Error> {
let mut buf = Vec::new();
let encoder = PngEncoder::new(&mut buf);
encoder
.write_image(
&image.bytes,
image.width as u32,
image.height as u32,
ExtendedColorType::Rgba8,
)
.map_err(|_| Error::ConversionFailure)?;
let format_id = match clipboard_win::register_format("PNG") {
Some(format_id) => format_id.into(),
None => return Err(last_error("Cannot register PNG clipboard format.")),
};
let data_size = buf.len();
let hdata = unsafe { global_alloc(data_size)? };
unsafe {
let pixels_dst = global_lock(hdata)?;
copy_nonoverlapping::<u8>(buf.as_ptr(), pixels_dst, data_size);
global_unlock_checked(hdata);
}
if unsafe { SetClipboardData(format_id, hdata as _) } == 0 {
unsafe { DeleteObject(hdata as _) };
Err(last_error("SetClipboardData failed with error"))
} else {
Ok(())
}
}
unsafe fn global_alloc(bytes: usize) -> Result<HGLOBAL, Error> {
let hdata = GlobalAlloc(GHND, bytes);
if hdata == 0 {
Err(last_error("Could not allocate global memory object"))
} else {
Ok(hdata)
}
}
unsafe fn global_lock(hmem: HGLOBAL) -> Result<*mut u8, Error> {
let data_ptr = GlobalLock(hmem) as *mut u8;
if data_ptr.is_null() {
Err(last_error("Could not lock the global memory object"))
} else {
Ok(data_ptr)
}
}
pub(super) fn read_cf_dibv5(dibv5: &[u8]) -> Result<ImageData<'static>, Error> {
const PROFILE_EMBEDDED: u32 = 0x4D42_4544;
const PROFILE_LINKED: u32 = 0x4C49_4E4B;
let header_size = size_of::<BITMAPV5HEADER>();
if dibv5.len() < header_size {
return Err(Error::unknown("When reading the DIBV5 data, it contained fewer bytes than the BITMAPV5HEADER size. This is invalid."));
}
let header = unsafe { &*(dibv5.as_ptr() as *const BITMAPV5HEADER) };
let has_profile =
header.bV5CSType == PROFILE_LINKED || header.bV5CSType == PROFILE_EMBEDDED;
let pixel_data_start = if has_profile {
header.bV5ProfileData as isize + header.bV5ProfileSize as isize
} else {
header_size as isize
};
unsafe {
let image_bytes = dibv5.as_ptr().offset(pixel_data_start) as *const _;
let hdc = get_screen_device_context()?;
let hbitmap = create_bitmap_from_dib(hdc, header as _, image_bytes)?;
let w = header.bV5Width;
let h = header.bV5Height.abs();
let result_size = w as usize * h as usize * 4;
let mut result_bytes = Vec::<u8>::with_capacity(result_size);
let mut output_header = BITMAPINFO {
bmiColors: [RGBQUAD { rgbRed: 0, rgbGreen: 0, rgbBlue: 0, rgbReserved: 0 }],
bmiHeader: BITMAPINFOHEADER {
biSize: size_of::<BITMAPINFOHEADER>() as u32,
biWidth: w,
biHeight: -h,
biBitCount: 32,
biPlanes: 1,
biCompression: BI_RGB as u32,
biSizeImage: 0,
biXPelsPerMeter: 0,
biYPelsPerMeter: 0,
biClrUsed: 0,
biClrImportant: 0,
},
};
let lines = convert_bitmap_to_rgb(
hdc,
hbitmap,
h as _,
result_bytes.as_mut_ptr() as _,
&mut output_header as _,
)?;
let read_len = lines as usize * w as usize * 4;
assert!(
read_len <= result_bytes.capacity(),
"Segmentation fault. Read more bytes than allocated to pixel buffer",
);
result_bytes.set_len(read_len);
let result_bytes = win_to_rgba(&mut result_bytes);
let result = ImageData {
bytes: Cow::Owned(result_bytes),
width: w as usize,
height: h as usize,
};
Ok(result)
}
}
fn get_screen_device_context() -> Result<HDC, Error> {
let hdc = unsafe { GetDC(0) };
if hdc == 0 {
Err(Error::unknown("Failed to get the device context. GetDC returned null"))
} else {
Ok(hdc)
}
}
unsafe fn create_bitmap_from_dib(
hdc: HDC,
header: *const BITMAPV5HEADER,
image_bytes: *const c_void,
) -> Result<HBITMAP, Error> {
let hbitmap = CreateDIBitmap(
hdc,
header as _,
CBM_INIT as u32,
image_bytes,
header as _,
DIB_RGB_COLORS,
);
if hbitmap == 0 {
Err(Error::unknown(
"Failed to create the HBITMAP while reading DIBV5. CreateDIBitmap returned null",
))
} else {
Ok(hbitmap)
}
}
unsafe fn convert_bitmap_to_rgb(
hdc: HDC,
hbitmap: HBITMAP,
lines: u32,
dst: *mut c_void,
header: *mut BITMAPINFO,
) -> Result<i32, Error> {
let lines = GetDIBits(hdc, hbitmap, 0, lines, dst, header, DIB_RGB_COLORS);
if lines == 0 {
Err(Error::unknown("Could not get the bitmap bits, GetDIBits returned 0"))
} else {
Ok(lines)
}
}
#[allow(clippy::identity_op, clippy::erasing_op)]
#[must_use]
unsafe fn rgba_to_win(bytes: &mut [u8]) -> Cow<'_, [u8]> {
debug_assert_eq!(bytes.len() % 4, 0);
let mut u32pixels_buffer = convert_bytes_to_u32s(bytes);
let u32pixels = match u32pixels_buffer {
ImageDataCow::Borrowed(ref mut b) => b,
ImageDataCow::Owned(ref mut b) => b.as_mut_slice(),
};
for p in u32pixels.iter_mut() {
let [mut r, mut g, mut b, mut a] = p.to_ne_bytes().map(u32::from);
r <<= 2 * 8;
g <<= 1 * 8;
b <<= 0 * 8;
a <<= 3 * 8;
*p = r | g | b | a;
}
match u32pixels_buffer {
ImageDataCow::Borrowed(_) => Cow::Borrowed(bytes),
ImageDataCow::Owned(bytes) => {
Cow::Owned(bytes.into_iter().flat_map(|b| b.to_ne_bytes()).collect())
}
}
}
fn flip_v(image: ImageData) -> ImageData<'static> {
let w = image.width;
let h = image.height;
let mut bytes = image.bytes.into_owned();
let rowsize = w * 4; let mut tmp_a = vec![0; rowsize];
for a_row_id in 0..(h / 2) {
let b_row_id = h - a_row_id - 1;
let a_byte_start = a_row_id * rowsize;
let a_byte_end = a_byte_start + rowsize;
let b_byte_start = b_row_id * rowsize;
let b_byte_end = b_byte_start + rowsize;
tmp_a.copy_from_slice(&bytes[a_byte_start..a_byte_end]);
bytes.copy_within(b_byte_start..b_byte_end, a_byte_start);
bytes[b_byte_start..b_byte_end].copy_from_slice(&tmp_a);
}
ImageData { width: image.width, height: image.height, bytes: bytes.into() }
}
#[allow(clippy::identity_op, clippy::erasing_op)]
#[must_use]
unsafe fn win_to_rgba(bytes: &mut [u8]) -> Vec<u8> {
debug_assert_eq!(bytes.len() % 4, 0);
let mut u32pixels_buffer = convert_bytes_to_u32s(bytes);
let u32pixels = match u32pixels_buffer {
ImageDataCow::Borrowed(ref mut b) => b,
ImageDataCow::Owned(ref mut b) => b.as_mut_slice(),
};
for p in u32pixels {
let mut bytes = p.to_ne_bytes();
bytes[0] = (*p >> (2 * 8)) as u8;
bytes[1] = (*p >> (1 * 8)) as u8;
bytes[2] = (*p >> (0 * 8)) as u8;
bytes[3] = (*p >> (3 * 8)) as u8;
*p = u32::from_ne_bytes(bytes);
}
match u32pixels_buffer {
ImageDataCow::Borrowed(_) => bytes.to_vec(),
ImageDataCow::Owned(bytes) => bytes.into_iter().flat_map(|b| b.to_ne_bytes()).collect(),
}
}
enum ImageDataCow<'a> {
Borrowed(&'a mut [u32]),
Owned(Vec<u32>),
}
unsafe fn convert_bytes_to_u32s(bytes: &mut [u8]) -> ImageDataCow<'_> {
let (prefix, _, suffix) = bytes.align_to::<u32>();
if prefix.is_empty() && suffix.is_empty() {
ImageDataCow::Borrowed(bytes.align_to_mut::<u32>().1)
} else {
let u32pixels_buffer = bytes
.chunks(4)
.map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap()))
.collect();
ImageDataCow::Owned(u32pixels_buffer)
}
}
#[test]
fn conversion_between_win_and_rgba() {
const DATA: [u8; 16] =
[100, 100, 255, 100, 0, 0, 0, 255, 255, 100, 100, 255, 100, 255, 100, 100];
let mut data = DATA;
let _converted = unsafe { win_to_rgba(&mut data) };
let mut data = DATA;
let _converted = unsafe { rgba_to_win(&mut data) };
let mut data = DATA;
let _converted = unsafe { win_to_rgba(&mut data) };
let _converted = unsafe { rgba_to_win(&mut data) };
assert_eq!(data, DATA);
let mut data = DATA;
let _converted = unsafe { rgba_to_win(&mut data) };
let _converted = unsafe { win_to_rgba(&mut data) };
assert_eq!(data, DATA);
}
}
pub(crate) struct Clipboard(());
impl Drop for Clipboard {
fn drop(&mut self) {}
}
struct OpenClipboard<'clipboard> {
_inner: clipboard_win::Clipboard,
_marker: PhantomData<*const ()>,
_for_shim: &'clipboard mut Clipboard,
}
impl Clipboard {
const DEFAULT_OPEN_ATTEMPTS: usize = 5;
pub(crate) fn new() -> Result<Self, Error> {
Ok(Self(()))
}
fn open(&mut self) -> Result<OpenClipboard, Error> {
let mut attempts = Self::DEFAULT_OPEN_ATTEMPTS;
let clipboard = loop {
match clipboard_win::Clipboard::new() {
Ok(this) => break Ok(this),
Err(err) => match attempts {
0 => break Err(err),
_ => attempts -= 1,
},
}
thread::sleep(Duration::from_millis(5));
}
.map_err(|_| Error::ClipboardOccupied)?;
Ok(OpenClipboard { _inner: clipboard, _marker: PhantomData, _for_shim: self })
}
}
pub(crate) struct Get<'clipboard> {
clipboard: Result<OpenClipboard<'clipboard>, Error>,
}
impl<'clipboard> Get<'clipboard> {
pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
Self { clipboard: clipboard.open() }
}
pub(crate) fn text(self) -> Result<String, Error> {
const FORMAT: u32 = clipboard_win::formats::CF_UNICODETEXT;
let _clipboard_assertion = self.clipboard?;
if !clipboard_win::is_format_avail(FORMAT) {
return Err(Error::ContentNotAvailable);
}
let text_size = clipboard_win::raw::size(FORMAT)
.ok_or_else(|| Error::unknown("failed to read clipboard text size"))?;
let mut out: Vec<u16> = vec![0u16; text_size.get() / 2];
let bytes_read = {
let out: &mut [u8] =
unsafe { std::slice::from_raw_parts_mut(out.as_mut_ptr().cast(), out.len() * 2) };
let mut bytes_read = clipboard_win::raw::get(FORMAT, out)
.map_err(|_| Error::unknown("failed to read clipboard string"))?;
bytes_read /= 2;
if let Some(last) = out.last().copied() {
if last == 0 {
bytes_read -= 1;
}
}
bytes_read
};
String::from_utf16(&out[..bytes_read]).map_err(|_| Error::ConversionFailure)
}
#[cfg(feature = "image-data")]
pub(crate) fn image(self) -> Result<ImageData<'static>, Error> {
const FORMAT: u32 = clipboard_win::formats::CF_DIBV5;
let _clipboard_assertion = self.clipboard?;
if !clipboard_win::is_format_avail(FORMAT) {
return Err(Error::ContentNotAvailable);
}
let mut data = Vec::new();
clipboard_win::raw::get_vec(FORMAT, &mut data)
.map_err(|_| Error::unknown("failed to read clipboard image data"))?;
image_data::read_cf_dibv5(&data)
}
}
pub(crate) struct Set<'clipboard> {
clipboard: Result<OpenClipboard<'clipboard>, Error>,
exclude_from_monitoring: bool,
exclude_from_cloud: bool,
exclude_from_history: bool,
}
impl<'clipboard> Set<'clipboard> {
pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
Self {
clipboard: clipboard.open(),
exclude_from_monitoring: false,
exclude_from_cloud: false,
exclude_from_history: false,
}
}
pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> {
let open_clipboard = self.clipboard?;
clipboard_win::raw::set_string(&data)
.map_err(|_| Error::unknown("Could not place the specified text to the clipboard"))?;
add_clipboard_exclusions(
open_clipboard,
self.exclude_from_monitoring,
self.exclude_from_cloud,
self.exclude_from_history,
)
}
pub(crate) fn html(self, html: Cow<'_, str>, alt: Option<Cow<'_, str>>) -> Result<(), Error> {
let open_clipboard = self.clipboard?;
let alt = match alt {
Some(s) => s.into(),
None => String::new(),
};
clipboard_win::raw::set_string(&alt)
.map_err(|_| Error::unknown("Could not place the specified text to the clipboard"))?;
if let Some(format) = clipboard_win::register_format("HTML Format") {
let html = wrap_html(&html);
clipboard_win::raw::set_without_clear(format.get(), html.as_bytes())
.map_err(|e| Error::unknown(e.to_string()))?;
}
add_clipboard_exclusions(
open_clipboard,
self.exclude_from_monitoring,
self.exclude_from_cloud,
self.exclude_from_history,
)
}
#[cfg(feature = "image-data")]
pub(crate) fn image(self, image: ImageData) -> Result<(), Error> {
let open_clipboard = self.clipboard?;
if let Err(e) = clipboard_win::raw::empty() {
return Err(Error::unknown(format!(
"Failed to empty the clipboard. Got error code: {e}"
)));
};
image_data::add_png_file(&image)?;
image_data::add_cf_dibv5(open_clipboard, image)?;
Ok(())
}
}
fn add_clipboard_exclusions(
_open_clipboard: OpenClipboard<'_>,
exclude_from_monitoring: bool,
exclude_from_cloud: bool,
exclude_from_history: bool,
) -> Result<(), Error> {
const CLIPBOARD_EXCLUSION_DATA: &[u8] = &0u32.to_ne_bytes();
if exclude_from_monitoring {
if let Some(format) =
clipboard_win::register_format("ExcludeClipboardContentFromMonitorProcessing")
{
clipboard_win::raw::set_without_clear(format.get(), CLIPBOARD_EXCLUSION_DATA)
.map_err(|_| Error::unknown("Failed to exclude data from clipboard monitoring"))?;
}
}
if exclude_from_cloud {
if let Some(format) = clipboard_win::register_format("CanUploadToCloudClipboard") {
clipboard_win::raw::set_without_clear(format.get(), CLIPBOARD_EXCLUSION_DATA)
.map_err(|_| Error::unknown("Failed to exclude data from cloud clipboard"))?;
}
}
if exclude_from_history {
if let Some(format) = clipboard_win::register_format("CanIncludeInClipboardHistory") {
clipboard_win::raw::set_without_clear(format.get(), CLIPBOARD_EXCLUSION_DATA)
.map_err(|_| Error::unknown("Failed to exclude data from clipboard history"))?;
}
}
Ok(())
}
pub trait SetExtWindows: private::Sealed {
fn exclude_from_monitoring(self) -> Self;
fn exclude_from_cloud(self) -> Self;
fn exclude_from_history(self) -> Self;
}
impl SetExtWindows for crate::Set<'_> {
fn exclude_from_monitoring(mut self) -> Self {
self.platform.exclude_from_monitoring = true;
self
}
fn exclude_from_cloud(mut self) -> Self {
self.platform.exclude_from_cloud = true;
self
}
fn exclude_from_history(mut self) -> Self {
self.platform.exclude_from_history = true;
self
}
}
pub(crate) struct Clear<'clipboard> {
clipboard: Result<OpenClipboard<'clipboard>, Error>,
}
impl<'clipboard> Clear<'clipboard> {
pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
Self { clipboard: clipboard.open() }
}
pub(crate) fn clear(self) -> Result<(), Error> {
let _clipboard_assertion = self.clipboard?;
clipboard_win::empty().map_err(|_| Error::unknown("failed to clear clipboard"))
}
}
fn wrap_html(ctn: &str) -> String {
let h_version = "Version:0.9";
let h_start_html = "\r\nStartHTML:";
let h_end_html = "\r\nEndHTML:";
let h_start_frag = "\r\nStartFragment:";
let h_end_frag = "\r\nEndFragment:";
let c_start_frag = "\r\n<html>\r\n<body>\r\n<!--StartFragment-->\r\n";
let c_end_frag = "\r\n<!--EndFragment-->\r\n</body>\r\n</html>";
let h_len = h_version.len()
+ h_start_html.len()
+ 10 + h_end_html.len()
+ 10 + h_start_frag.len()
+ 10 + h_end_frag.len()
+ 10;
let n_start_html = h_len + 2;
let n_start_frag = h_len + c_start_frag.len();
let n_end_frag = n_start_frag + ctn.len();
let n_end_html = n_end_frag + c_end_frag.len();
format!(
"{}{}{:010}{}{:010}{}{:010}{}{:010}{}{}{}",
h_version,
h_start_html,
n_start_html,
h_end_html,
n_end_html,
h_start_frag,
n_start_frag,
h_end_frag,
n_end_frag,
c_start_frag,
ctn,
c_end_frag,
)
}