Skip to content

Commit

Permalink
Copy image as PNG file on Windows (#141)
Browse files Browse the repository at this point in the history
* Add image to Windows dependencies

* Set image data as PNG file on Windows
  • Loading branch information
Klavionik authored Apr 28, 2024
1 parent 0bff1e0 commit 83740b7
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 12 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ windows-sys = { version = "0.48.0", optional = true, features = [
]}
clipboard-win = "5.3.1"
log = "0.4"
image = { version = "0.25", optional = true, default-features = false, features = ["png"] }

[target.'cfg(target_os = "macos")'.dependencies]
# Use `relax-void-encoding`, as that allows us to pass `c_void` instead of implementing `Encode` correctly for `&CGImageRef`
Expand Down
70 changes: 58 additions & 12 deletions src/platform/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ use std::{borrow::Cow, marker::PhantomData, thread, time::Duration};
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,
Expand All @@ -37,6 +40,18 @@ mod image_data {
Error::unknown(format!("{}: {}", message, os_error))
}

unsafe fn global_unlock_checked(hdata: isize) {
// If the memory object is unlocked after decrementing the lock count, the function
// returns zero and GetLastError returns NO_ERROR. If it fails, the return value is
// zero and GetLastError returns a value other than NO_ERROR.
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,
Expand Down Expand Up @@ -85,17 +100,7 @@ mod image_data {
let hdata = unsafe { global_alloc(data_size)? };
unsafe {
let data_ptr = global_lock(hdata)?;
let _unlock = ScopeGuard::new(|| {
// If the memory object is unlocked after decrementing the lock count, the function
// returns zero and GetLastError returns NO_ERROR. If it fails, the return value is
// zero and GetLastError returns a value other than NO_ERROR.
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 dibv5 data: {}", err);
}
}
});
let _unlock = ScopeGuard::new(|| global_unlock_checked(hdata));

copy_nonoverlapping::<u8>((&header) as *const _ as *const u8, data_ptr, header_size);

Expand All @@ -122,6 +127,43 @@ mod image_data {
}
}

pub(super) fn add_png_file(image: &ImageData) -> Result<(), Error> {
// Try encoding the image as PNG.
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)?;

// Register PNG format.
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 {
Expand Down Expand Up @@ -609,7 +651,11 @@ impl<'clipboard> Set<'clipboard> {
)));
};

image_data::add_cf_dibv5(open_clipboard, image)
// XXX: The ordering of these functions is important, as some programs will grab the
// first format available. PNGs tend to have better compatibility on Windows, so it is set first.
image_data::add_png_file(&image)?;
image_data::add_cf_dibv5(open_clipboard, image)?;
Ok(())
}
}

Expand Down

0 comments on commit 83740b7

Please sign in to comment.