From ce5174dd40e0cdf9a95ca5d49354c0dfafe5d3a3 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 18 Apr 2023 18:08:47 -0300 Subject: [PATCH 1/3] Add iOS support --- Cargo.toml | 2 +- src/lib.rs | 24 +++++++--- src/platform/{osx.rs => apple.rs} | 75 ++++++++++++++++++++++++++----- src/platform/mod.rs | 25 ++++++++--- 4 files changed, 100 insertions(+), 26 deletions(-) rename src/platform/{osx.rs => apple.rs} (83%) diff --git a/Cargo.toml b/Cargo.toml index f9f5c018..45c17293 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ winapi = { version = "0.3.9", features = [ clipboard-win = "4.4.2" log = "0.4" -[target.'cfg(target_os = "macos")'.dependencies] +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] objc = "0.2" objc_id = "0.1" objc-foundation = "0.1" diff --git a/src/lib.rs b/src/lib.rs index fbbd0b86..cef33be1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,12 @@ mod platform; #[cfg(all( unix, - not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), + not(any( + target_os = "macos", + target_os = "ios", + target_os = "android", + target_os = "emscripten" + )), ))] pub use platform::{ClearExtLinux, GetExtLinux, LinuxClipboardKind, SetExtLinux}; @@ -94,7 +99,7 @@ impl Clipboard { /// Any image data placed on the clipboard with `set_image` will be possible read back, using /// this function. However it's of not guaranteed that an image placed on the clipboard by any /// other application will be of a supported format. - #[cfg(feature = "image-data")] + #[cfg(all(feature = "image-data", not(target_os = "ios")))] pub fn get_image(&mut self) -> Result, Error> { self.get().image() } @@ -106,7 +111,7 @@ impl Clipboard { /// - On macOS: `NSImage` object /// - On Linux: PNG, under the atom `image/png` /// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP` - #[cfg(feature = "image-data")] + #[cfg(all(feature = "image-data", not(target_os = "ios")))] pub fn set_image(&mut self, image: ImageData) -> Result<(), Error> { self.set().image(image) } @@ -151,7 +156,7 @@ impl Get<'_> { /// Any image data placed on the clipboard with `set_image` will be possible read back, using /// this function. However it's of not guaranteed that an image placed on the clipboard by any /// other application will be of a supported format. - #[cfg(feature = "image-data")] + #[cfg(all(feature = "image-data", not(target_os = "ios")))] pub fn image(self) -> Result, Error> { self.platform.image() } @@ -192,7 +197,7 @@ impl Set<'_> { /// - On macOS: `NSImage` object /// - On Linux: PNG, under the atom `image/png` /// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP` - #[cfg(feature = "image-data")] + #[cfg(all(feature = "image-data", not(target_os = "ios")))] pub fn image(self, image: ImageData) -> Result<(), Error> { self.platform.image(image) } @@ -285,7 +290,7 @@ mod tests { ctx.set_html(html, Some(alt_text)).unwrap(); assert_eq!(ctx.get_text().unwrap(), alt_text); } - #[cfg(feature = "image-data")] + #[cfg(all(feature = "image-data", not(target_os = "ios")))] { let mut ctx = Clipboard::new().unwrap(); #[rustfmt::skip] @@ -327,7 +332,12 @@ mod tests { } #[cfg(all( unix, - not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), + not(any( + target_os = "macos", + target_os = "ios", + target_os = "android", + target_os = "emscripten" + )), ))] { use crate::{LinuxClipboardKind, SetExtLinux}; diff --git a/src/platform/osx.rs b/src/platform/apple.rs similarity index 83% rename from src/platform/osx.rs rename to src/platform/apple.rs index a0fbf7b0..0f0a1039 100644 --- a/src/platform/osx.rs +++ b/src/platform/apple.rs @@ -29,18 +29,24 @@ use once_cell::sync::Lazy; use std::borrow::Cow; // Required to bring NSPasteboard into the path of the class-resolver +#[cfg(target_os = "macos")] #[link(name = "AppKit", kind = "framework")] extern "C" { static NSPasteboardTypeHTML: *const Object; static NSPasteboardTypeString: *const Object; } +#[cfg(target_os = "macos")] +const PASTEBOARD_CLASS: &str = "NSPasteboard"; +#[cfg(target_os = "ios")] +const PASTEBOARD_CLASS: &str = "UIPasteboard"; + static NSSTRING_CLASS: Lazy<&Class> = Lazy::new(|| Class::get("NSString").unwrap()); #[cfg(feature = "image-data")] static NSIMAGE_CLASS: Lazy<&Class> = Lazy::new(|| Class::get("NSImage").unwrap()); /// Returns an NSImage object on success. -#[cfg(feature = "image-data")] +#[cfg(all(feature = "image-data", target_os = "macos"))] fn image_from_pixels( pixels: Vec, width: usize, @@ -99,7 +105,8 @@ pub(crate) struct Clipboard { impl Clipboard { pub(crate) fn new() -> Result { - let cls = Class::get("NSPasteboard").expect("NSPasteboard not registered"); + let cls = Class::get(PASTEBOARD_CLASS) + .unwrap_or_else(|| panic!("{} not registered", PASTEBOARD_CLASS)); let pasteboard: *mut Object = unsafe { msg_send![cls, generalPasteboard] }; if !pasteboard.is_null() { @@ -115,7 +122,12 @@ impl Clipboard { } fn clear(&mut self) { + #[cfg(target_os = "macos")] let _: usize = unsafe { msg_send![self.pasteboard, clearContents] }; + #[cfg(target_os = "ios")] + let _: () = unsafe { + msg_send![self.pasteboard, setItems: NSArray::from_vec(Vec::>::new())] + }; } // fn get_binary_contents(&mut self) -> Result, Box> { @@ -178,6 +190,18 @@ impl<'clipboard> Get<'clipboard> { Self { pasteboard: &*clipboard.pasteboard } } + #[cfg(target_os = "ios")] + pub(crate) fn text(self) -> Result { + let obj: *mut NSString = unsafe { msg_send![self.pasteboard, string] }; + if obj.is_null() { + Err(Error::ContentNotAvailable) + } else { + let id: Id = unsafe { Id::from_ptr(obj) }; + Ok(id.as_str().to_owned()) + } + } + + #[cfg(target_os = "macos")] pub(crate) fn text(self) -> Result { let string_class = object_class(&NSSTRING_CLASS); let classes: Id> = NSArray::from_vec(vec![string_class]); @@ -200,7 +224,7 @@ impl<'clipboard> Get<'clipboard> { .ok_or(Error::ContentNotAvailable) } - #[cfg(feature = "image-data")] + #[cfg(all(feature = "image-data", target_os = "macos"))] pub(crate) fn image(self) -> Result, Error> { use std::io::Cursor; @@ -261,13 +285,26 @@ impl<'clipboard> Set<'clipboard> { pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> { self.clipboard.clear(); - let string_array = NSArray::from_vec(vec![NSString::from_str(&data)]); - let success: bool = - unsafe { msg_send![self.clipboard.pasteboard, writeObjects: string_array] }; + #[cfg(target_os = "macos")] + let success: bool = { + let string_array = NSArray::from_vec(vec![NSString::from_str(&data)]); + unsafe { msg_send![self.clipboard.pasteboard, writeObjects: string_array] } + }; + #[cfg(target_os = "ios")] + let success: bool = { + let string = NSString::from_str(&data); + unsafe { msg_send![self.clipboard.pasteboard, setString: string] } + }; + if success { Ok(()) } else { - Err(Error::Unknown { description: "NSPasteboard#writeObjects: returned false".into() }) + Err(Error::Unknown { + description: format!( + "{PASTEBOARD_CLASS}#{}: returned false", + if cfg!(target_os = "ios") { "setString" } else { "writeObjects" } + ), + }) } } @@ -284,25 +321,39 @@ impl<'clipboard> Set<'clipboard> { html ); let html_nss = NSString::from_str(&html); + #[cfg(target_os = "macos")] let mut success: bool = unsafe { msg_send![self.clipboard.pasteboard, setString: html_nss forType:NSPasteboardTypeHTML] }; + #[cfg(target_os = "ios")] + let mut success: bool = unsafe { msg_send![self.clipboard.pasteboard, setString: html_nss] }; + if success { if let Some(alt_text) = alt { let alt_nss = NSString::from_str(&alt_text); - success = unsafe { - msg_send![self.clipboard.pasteboard, setString: alt_nss forType:NSPasteboardTypeString] - }; + #[cfg(target_os = "macos")] + { + success = unsafe { + msg_send![self.clipboard.pasteboard, setString: alt_nss forType:NSPasteboardTypeString] + }; + } + + #[cfg(target_os = "ios")] + { + success = unsafe { msg_send![self.clipboard.pasteboard, setString: alt_nss] }; + } } } if success { Ok(()) } else { - Err(Error::Unknown { description: "NSPasteboard#writeObjects: returned false".into() }) + Err(Error::Unknown { + description: format!("{PASTEBOARD_CLASS}#writeObjects: returned false"), + }) } } - #[cfg(feature = "image-data")] + #[cfg(all(feature = "image-data", target_os = "macos"))] pub(crate) fn image(self, data: ImageData) -> Result<(), Error> { let pixels = data.bytes.into(); let image = image_from_pixels(pixels, data.width, data.height) diff --git a/src/platform/mod.rs b/src/platform/mod.rs index b3364632..685b089c 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -1,8 +1,21 @@ -#[cfg(all(unix, not(any(target_os = "macos", target_os = "android", target_os = "emscripten"))))] +#[cfg(all( + unix, + not(any( + target_os = "macos", + target_os = "ios", + target_os = "android", + target_os = "emscripten" + )) +))] mod linux; #[cfg(all( unix, - not(any(target_os = "macos", target_os = "android", target_os = "emscripten")) + not(any( + target_os = "macos", + target_os = "ios", + target_os = "android", + target_os = "emscripten" + )) ))] pub use linux::*; @@ -11,7 +24,7 @@ mod windows; #[cfg(windows)] pub use windows::*; -#[cfg(target_os = "macos")] -mod osx; -#[cfg(target_os = "macos")] -pub(crate) use osx::*; +#[cfg(any(target_os = "macos", target_os = "ios"))] +mod apple; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub(crate) use apple::*; From 0b43c5f9c417839c277c6571873944d89e0b3cb7 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Sun, 23 Apr 2023 08:35:09 -0300 Subject: [PATCH 2/3] fix write type --- src/platform/apple.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/platform/apple.rs b/src/platform/apple.rs index 0f0a1039..ceed0c45 100644 --- a/src/platform/apple.rs +++ b/src/platform/apple.rs @@ -126,7 +126,10 @@ impl Clipboard { let _: usize = unsafe { msg_send![self.pasteboard, clearContents] }; #[cfg(target_os = "ios")] let _: () = unsafe { - msg_send![self.pasteboard, setItems: NSArray::from_vec(Vec::>::new())] + msg_send![ + self.pasteboard, + setItems: NSArray::>::from_vec(Vec::new()) + ] }; } From b3d5120b29d18594ffb211ed0e79c05217b4e0c3 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Thu, 27 Apr 2023 11:08:34 -0300 Subject: [PATCH 3/3] feat: image APIs on iOS --- src/lib.rs | 10 +++--- src/platform/apple.rs | 77 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index cef33be1..b9596ee5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,7 +99,7 @@ impl Clipboard { /// Any image data placed on the clipboard with `set_image` will be possible read back, using /// this function. However it's of not guaranteed that an image placed on the clipboard by any /// other application will be of a supported format. - #[cfg(all(feature = "image-data", not(target_os = "ios")))] + #[cfg(feature = "image-data")] pub fn get_image(&mut self) -> Result, Error> { self.get().image() } @@ -111,7 +111,7 @@ impl Clipboard { /// - On macOS: `NSImage` object /// - On Linux: PNG, under the atom `image/png` /// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP` - #[cfg(all(feature = "image-data", not(target_os = "ios")))] + #[cfg(feature = "image-data")] pub fn set_image(&mut self, image: ImageData) -> Result<(), Error> { self.set().image(image) } @@ -156,7 +156,7 @@ impl Get<'_> { /// Any image data placed on the clipboard with `set_image` will be possible read back, using /// this function. However it's of not guaranteed that an image placed on the clipboard by any /// other application will be of a supported format. - #[cfg(all(feature = "image-data", not(target_os = "ios")))] + #[cfg(feature = "image-data")] pub fn image(self) -> Result, Error> { self.platform.image() } @@ -197,7 +197,7 @@ impl Set<'_> { /// - On macOS: `NSImage` object /// - On Linux: PNG, under the atom `image/png` /// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP` - #[cfg(all(feature = "image-data", not(target_os = "ios")))] + #[cfg(feature = "image-data")] pub fn image(self, image: ImageData) -> Result<(), Error> { self.platform.image(image) } @@ -290,7 +290,7 @@ mod tests { ctx.set_html(html, Some(alt_text)).unwrap(); assert_eq!(ctx.get_text().unwrap(), alt_text); } - #[cfg(all(feature = "image-data", not(target_os = "ios")))] + #[cfg(feature = "image-data")] { let mut ctx = Clipboard::new().unwrap(); #[rustfmt::skip] diff --git a/src/platform/apple.rs b/src/platform/apple.rs index ceed0c45..0f62a577 100644 --- a/src/platform/apple.rs +++ b/src/platform/apple.rs @@ -36,17 +36,26 @@ extern "C" { static NSPasteboardTypeString: *const Object; } +#[cfg(target_os = "ios")] +#[link(name = "UIKit", kind = "framework")] +extern "C" { + fn UIImagePNGRepresentation(ui_image: *const Object) -> *const Object; +} + #[cfg(target_os = "macos")] const PASTEBOARD_CLASS: &str = "NSPasteboard"; #[cfg(target_os = "ios")] const PASTEBOARD_CLASS: &str = "UIPasteboard"; +#[cfg(target_os = "macos")] static NSSTRING_CLASS: Lazy<&Class> = Lazy::new(|| Class::get("NSString").unwrap()); -#[cfg(feature = "image-data")] -static NSIMAGE_CLASS: Lazy<&Class> = Lazy::new(|| Class::get("NSImage").unwrap()); +#[cfg(all(feature = "image-data", target_os = "macos"))] +static IMAGE_CLASS: Lazy<&Class> = Lazy::new(|| Class::get("NSImage").unwrap()); +#[cfg(all(feature = "image-data", target_os = "ios"))] +static IMAGE_CLASS: Lazy<&Class> = Lazy::new(|| Class::get("UIImage").unwrap()); /// Returns an NSImage object on success. -#[cfg(all(feature = "image-data", target_os = "macos"))] +#[cfg(feature = "image-data")] fn image_from_pixels( pixels: Vec, width: usize, @@ -89,11 +98,19 @@ fn image_from_pixels( false, kCGRenderingIntentDefault, ); - let size = NSSize { width: width as CGFloat, height: height as CGFloat }; - let image: Id = unsafe { Id::from_ptr(msg_send![*NSIMAGE_CLASS, alloc]) }; + + let image: Id = unsafe { Id::from_ptr(msg_send![*IMAGE_CLASS, alloc]) }; #[allow(clippy::let_unit_value)] { - let _: () = unsafe { msg_send![image, initWithCGImage:cg_image size:size] }; + #[cfg(target_os = "macos")] + { + let size = NSSize { width: width as CGFloat, height: height as CGFloat }; + let _: () = unsafe { msg_send![image, initWithCGImage:cg_image size:size] }; + } + #[cfg(target_os = "ios")] + { + let _: () = unsafe { msg_send![image, initWithCGImage: cg_image] }; + } } Ok(image) @@ -227,11 +244,44 @@ impl<'clipboard> Get<'clipboard> { .ok_or(Error::ContentNotAvailable) } + #[cfg(all(feature = "image-data", target_os = "ios"))] + pub(crate) fn image(self) -> Result, Error> { + use std::io::Cursor; + + let ui_image: *mut NSObject = unsafe { msg_send![self.pasteboard, image] }; + + if ui_image.is_null() { + return Err(Error::ContentNotAvailable); + } + + let data = unsafe { UIImagePNGRepresentation(ui_image as _) }; + let data = unsafe { + let len: usize = msg_send![data, length]; + let bytes: *const u8 = msg_send![data, bytes]; + + Cursor::new(std::slice::from_raw_parts(bytes, len)) + }; + let reader = image::io::Reader::with_format(data, image::ImageFormat::Png); + match reader.decode() { + Ok(img) => { + let rgba = img.into_rgba8(); + let (width, height) = rgba.dimensions(); + + Ok(ImageData { + width: width as usize, + height: height as usize, + bytes: rgba.into_raw().into(), + }) + } + Err(_) => Err(Error::ConversionFailure), + } + } + #[cfg(all(feature = "image-data", target_os = "macos"))] pub(crate) fn image(self) -> Result, Error> { use std::io::Cursor; - let image_class: Id = object_class(&NSIMAGE_CLASS); + let image_class: Id = object_class(&IMAGE_CLASS); let classes = vec![image_class]; let classes: Id> = NSArray::from_vec(classes); let options: Id> = NSDictionary::new(); @@ -248,7 +298,7 @@ impl<'clipboard> Get<'clipboard> { }; let obj = match contents.first_object() { - Some(obj) if obj.is_kind_of(&NSIMAGE_CLASS) => obj, + Some(obj) if obj.is_kind_of(&IMAGE_CLASS) => obj, Some(_) | None => return Err(Error::ContentNotAvailable), }; @@ -356,7 +406,7 @@ impl<'clipboard> Set<'clipboard> { } } - #[cfg(all(feature = "image-data", target_os = "macos"))] + #[cfg(feature = "image-data")] pub(crate) fn image(self, data: ImageData) -> Result<(), Error> { let pixels = data.bytes.into(); let image = image_from_pixels(pixels, data.width, data.height) @@ -365,7 +415,15 @@ impl<'clipboard> Set<'clipboard> { self.clipboard.clear(); let objects: Id> = NSArray::from_vec(vec![image]); + + #[cfg(target_os = "macos")] let success: bool = unsafe { msg_send![self.clipboard.pasteboard, writeObjects: objects] }; + #[cfg(target_os = "ios")] + let success: bool = unsafe { + let _: () = msg_send![self.clipboard.pasteboard, setImages: objects]; + true + }; + if success { Ok(()) } else { @@ -395,6 +453,7 @@ impl<'clipboard> Clear<'clipboard> { /// Convenience function to get an Objective-C object from a /// specific class. +#[cfg(target_os = "macos")] fn object_class(class: &'static Class) -> Id { // SAFETY: `Class` is a valid object and `Id` will not mutate it unsafe { Id::from_ptr(class as *const Class as *mut NSObject) }