diff --git a/.changes/clipboard-manager-read-image-png.md b/.changes/clipboard-manager-read-image-png.md new file mode 100644 index 0000000000..bab9a8eb24 --- /dev/null +++ b/.changes/clipboard-manager-read-image-png.md @@ -0,0 +1,6 @@ +--- +"clipboard-manager": minor:feat +"clipboard-manager-js": minor:feat +--- + +Add `readImagePNG()` to read clipboard images as PNG bytes. diff --git a/Cargo.lock b/Cargo.lock index eb91344e97..e942a6bb37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6510,6 +6510,7 @@ name = "tauri-plugin-clipboard-manager" version = "2.3.2" dependencies = [ "arboard", + "image", "log", "serde", "serde_json", diff --git a/plugins/clipboard-manager/Cargo.toml b/plugins/clipboard-manager/Cargo.toml index d95c02c28a..726a5cdb6d 100644 --- a/plugins/clipboard-manager/Cargo.toml +++ b/plugins/clipboard-manager/Cargo.toml @@ -36,3 +36,4 @@ tauri = { workspace = true, features = ["wry"] } [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] arboard = { version = "3", features = ["wayland-data-control"] } +image = { version = "0.25", default-features = false, features = ["png"] } diff --git a/plugins/clipboard-manager/README.md b/plugins/clipboard-manager/README.md index b2353f9712..fe667e946c 100644 --- a/plugins/clipboard-manager/README.md +++ b/plugins/clipboard-manager/README.md @@ -62,11 +62,13 @@ Afterwards all the plugin's APIs are available through the JavaScript guest bind import { writeText, readText, + readImagePNG, writeHtml, clear } from '@tauri-apps/plugin-clipboard-manager' await writeText('Tauri is awesome!') assert(await readText(), 'Tauri is awesome!') +const pngBytes = await readImagePNG() ``` ## Contributing diff --git a/plugins/clipboard-manager/api-iife.js b/plugins/clipboard-manager/api-iife.js index 845708b061..740e1d70c4 100644 --- a/plugins/clipboard-manager/api-iife.js +++ b/plugins/clipboard-manager/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_CLIPBOARD_MANAGER__=function(e){"use strict";var n;async function t(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}"function"==typeof SuppressedError&&SuppressedError;class r{get rid(){return function(e,n,t,r){if("function"==typeof n?e!==n||!r:!n.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===t?r:"a"===t?r.call(e):r?r.value:n.get(e)}(this,n,"f")}constructor(e){n.set(this,void 0),function(e,n,t){if("function"==typeof n||!n.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");n.set(e,t)}(this,n,e)}async close(){return t("plugin:resources|close",{rid:this.rid})}}n=new WeakMap;class a extends r{constructor(e){super(e)}static async new(e,n,r){return t("plugin:image|new",{rgba:i(e),width:n,height:r}).then(e=>new a(e))}static async fromBytes(e){return t("plugin:image|from_bytes",{bytes:i(e)}).then(e=>new a(e))}static async fromPath(e){return t("plugin:image|from_path",{path:e}).then(e=>new a(e))}async rgba(){return t("plugin:image|rgba",{rid:this.rid}).then(e=>new Uint8Array(e))}async size(){return t("plugin:image|size",{rid:this.rid})}}function i(e){return null==e?null:"string"==typeof e?e:e instanceof a?e.rid:e}return e.clear=async function(){await t("plugin:clipboard-manager|clear")},e.readImage=async function(){return await t("plugin:clipboard-manager|read_image").then(e=>new a(e))},e.readText=async function(){return await t("plugin:clipboard-manager|read_text")},e.writeHtml=async function(e,n){await t("plugin:clipboard-manager|write_html",{html:e,altText:n})},e.writeImage=async function(e){await t("plugin:clipboard-manager|write_image",{image:i(e)})},e.writeText=async function(e,n){await t("plugin:clipboard-manager|write_text",{label:n?.label,text:e})},e}({});Object.defineProperty(window.__TAURI__,"clipboardManager",{value:__TAURI_PLUGIN_CLIPBOARD_MANAGER__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_CLIPBOARD_MANAGER__=function(e){"use strict";var n;async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}"function"==typeof SuppressedError&&SuppressedError;class t{get rid(){return function(e,n,r,t){if("function"==typeof n?e!==n||!t:!n.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?t:"a"===r?t.call(e):t?t.value:n.get(e)}(this,n,"f")}constructor(e){n.set(this,void 0),function(e,n,r){if("function"==typeof n||!n.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");n.set(e,r)}(this,n,e)}async close(){return r("plugin:resources|close",{rid:this.rid})}}n=new WeakMap;class a extends t{constructor(e){super(e)}static async new(e,n,t){return r("plugin:image|new",{rgba:i(e),width:n,height:t}).then(e=>new a(e))}static async fromBytes(e){return r("plugin:image|from_bytes",{bytes:i(e)}).then(e=>new a(e))}static async fromPath(e){return r("plugin:image|from_path",{path:e}).then(e=>new a(e))}async rgba(){return r("plugin:image|rgba",{rid:this.rid}).then(e=>new Uint8Array(e))}async size(){return r("plugin:image|size",{rid:this.rid})}}function i(e){return null==e?null:"string"==typeof e?e:e instanceof a?e.rid:e}return e.clear=async function(){await r("plugin:clipboard-manager|clear")},e.readImage=async function(){return await r("plugin:clipboard-manager|read_image").then(e=>new a(e))},e.readImagePNG=async function(){const e=await r("plugin:clipboard-manager|read_image_png");return e instanceof ArrayBuffer?new Uint8Array(e):Uint8Array.from(e)},e.readText=async function(){return await r("plugin:clipboard-manager|read_text")},e.writeHtml=async function(e,n){await r("plugin:clipboard-manager|write_html",{html:e,altText:n})},e.writeImage=async function(e){await r("plugin:clipboard-manager|write_image",{image:i(e)})},e.writeText=async function(e,n){await r("plugin:clipboard-manager|write_text",{label:n?.label,text:e})},e}({});Object.defineProperty(window.__TAURI__,"clipboardManager",{value:__TAURI_PLUGIN_CLIPBOARD_MANAGER__})} diff --git a/plugins/clipboard-manager/build.rs b/plugins/clipboard-manager/build.rs index 9bbeddfcbd..84c9790620 100644 --- a/plugins/clipboard-manager/build.rs +++ b/plugins/clipboard-manager/build.rs @@ -7,6 +7,7 @@ const COMMANDS: &[&str] = &[ "read_text", "write_image", "read_image", + "read_image_png", "write_html", "clear", ]; diff --git a/plugins/clipboard-manager/guest-js/index.ts b/plugins/clipboard-manager/guest-js/index.ts index a37bbfab1f..da324fb4d4 100644 --- a/plugins/clipboard-manager/guest-js/index.ts +++ b/plugins/clipboard-manager/guest-js/index.ts @@ -102,6 +102,31 @@ async function readImage(): Promise { ) } +/** + * Gets the clipboard image content as PNG bytes. + * + * #### Platform-specific + * + * - **Android / iOS:** Not supported. + * + * @example + * ```typescript + * import { readImagePNG } from '@tauri-apps/plugin-clipboard-manager'; + * + * const clipboardImage = await readImagePNG(); + * const blob = new Blob([clipboardImage], { type: 'image/png' }) + * const url = URL.createObjectURL(blob) + * ``` + * @since 2.4.0 + */ +async function readImagePNG(): Promise> { + const arr = await invoke( + 'plugin:clipboard-manager|read_image_png' + ) + + return arr instanceof ArrayBuffer ? new Uint8Array(arr) : Uint8Array.from(arr) +} + /** * * Writes HTML or fallbacks to write provided plain text to the clipboard. * @@ -148,4 +173,12 @@ async function clear(): Promise { await invoke('plugin:clipboard-manager|clear') } -export { writeText, readText, writeHtml, clear, readImage, writeImage } +export { + writeText, + readText, + writeHtml, + clear, + readImage, + readImagePNG, + writeImage +} diff --git a/plugins/clipboard-manager/permissions/autogenerated/commands/read_image_png.toml b/plugins/clipboard-manager/permissions/autogenerated/commands/read_image_png.toml new file mode 100644 index 0000000000..dd5bf4c3aa --- /dev/null +++ b/plugins/clipboard-manager/permissions/autogenerated/commands/read_image_png.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-read-image-png" +description = "Enables the read_image_png command without any pre-configured scope." +commands.allow = ["read_image_png"] + +[[permission]] +identifier = "deny-read-image-png" +description = "Denies the read_image_png command without any pre-configured scope." +commands.deny = ["read_image_png"] diff --git a/plugins/clipboard-manager/permissions/autogenerated/reference.md b/plugins/clipboard-manager/permissions/autogenerated/reference.md index 98a7fa961f..6e3bfe6ad3 100644 --- a/plugins/clipboard-manager/permissions/autogenerated/reference.md +++ b/plugins/clipboard-manager/permissions/autogenerated/reference.md @@ -70,6 +70,32 @@ Denies the read_image command without any pre-configured scope. +`clipboard-manager:allow-read-image-png` + + + + +Enables the read_image_png command without any pre-configured scope. + + + + + + + +`clipboard-manager:deny-read-image-png` + + + + +Denies the read_image_png command without any pre-configured scope. + + + + + + + `clipboard-manager:allow-read-text` diff --git a/plugins/clipboard-manager/permissions/schemas/schema.json b/plugins/clipboard-manager/permissions/schemas/schema.json index 891c6f0d8d..0ecd2780ae 100644 --- a/plugins/clipboard-manager/permissions/schemas/schema.json +++ b/plugins/clipboard-manager/permissions/schemas/schema.json @@ -318,6 +318,18 @@ "const": "deny-read-image", "markdownDescription": "Denies the read_image command without any pre-configured scope." }, + { + "description": "Enables the read_image_png command without any pre-configured scope.", + "type": "string", + "const": "allow-read-image-png", + "markdownDescription": "Enables the read_image_png command without any pre-configured scope." + }, + { + "description": "Denies the read_image_png command without any pre-configured scope.", + "type": "string", + "const": "deny-read-image-png", + "markdownDescription": "Denies the read_image_png command without any pre-configured scope." + }, { "description": "Enables the read_text command without any pre-configured scope.", "type": "string", diff --git a/plugins/clipboard-manager/src/commands.rs b/plugins/clipboard-manager/src/commands.rs index a8dd94ac06..60db86d40f 100644 --- a/plugins/clipboard-manager/src/commands.rs +++ b/plugins/clipboard-manager/src/commands.rs @@ -61,6 +61,14 @@ pub(crate) async fn read_image( Ok(rid) } +#[command] +pub(crate) async fn read_image_png( + _app: AppHandle, + clipboard: State<'_, Clipboard>, +) -> Result { + clipboard.read_image_png().map(tauri::ipc::Response::new) +} + #[command] pub(crate) async fn write_html( _app: AppHandle, diff --git a/plugins/clipboard-manager/src/desktop.rs b/plugins/clipboard-manager/src/desktop.rs index f3570cc0c7..3a74619af9 100644 --- a/plugins/clipboard-manager/src/desktop.rs +++ b/plugins/clipboard-manager/src/desktop.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT use arboard::ImageData; +use image::ImageEncoder; use serde::de::DeserializeOwned; use tauri::{image::Image, plugin::PluginApi, AppHandle, Runtime}; @@ -115,6 +116,34 @@ impl Clipboard { } } + /// Warning: This method should not be used on the main thread! Otherwise the underlying libraries may deadlock on Linux, freezing the whole app, when trying to copy data copied from this app, for example if the user copies text from the WebView. + pub fn read_image_png(&self) -> crate::Result> { + match &self.clipboard { + Ok(clipboard) => { + let image = clipboard.lock().unwrap().as_mut().unwrap().get_image()?; + let expected_len = image + .width + .checked_mul(image.height) + .and_then(|len| len.checked_mul(4)) + .ok_or_else(|| crate::Error::Clipboard("invalid image size".into()))?; + + if image.bytes.len() != expected_len { + return Err(crate::Error::Clipboard("invalid image data length".into())); + } + + let mut buffer = Vec::new(); + image::codecs::png::PngEncoder::new(&mut buffer).write_image( + &image.bytes, + image.width as u32, + image.height as u32, + image::ExtendedColorType::Rgba8, + )?; + Ok(buffer) + } + Err(e) => Err(crate::Error::Clipboard(e.to_string())), + } + } + pub(crate) fn cleanup(&self) { if let Ok(clipboard) = &self.clipboard { clipboard.lock().unwrap().take(); diff --git a/plugins/clipboard-manager/src/error.rs b/plugins/clipboard-manager/src/error.rs index 1b8cf482b4..951605470d 100644 --- a/plugins/clipboard-manager/src/error.rs +++ b/plugins/clipboard-manager/src/error.rs @@ -13,6 +13,9 @@ pub enum Error { PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), #[error("{0}")] Clipboard(String), + #[cfg(desktop)] + #[error("invalid image: {0}")] + Image(#[from] image::ImageError), #[error(transparent)] Tauri(#[from] tauri::Error), } diff --git a/plugins/clipboard-manager/src/lib.rs b/plugins/clipboard-manager/src/lib.rs index 0cbb4e41ec..5cef4f2ba0 100644 --- a/plugins/clipboard-manager/src/lib.rs +++ b/plugins/clipboard-manager/src/lib.rs @@ -47,6 +47,7 @@ pub fn init() -> TauriPlugin { commands::write_text, commands::read_text, commands::read_image, + commands::read_image_png, commands::write_image, commands::write_html, commands::clear diff --git a/plugins/clipboard-manager/src/mobile.rs b/plugins/clipboard-manager/src/mobile.rs index 72d5f6e066..bc60cf0da6 100644 --- a/plugins/clipboard-manager/src/mobile.rs +++ b/plugins/clipboard-manager/src/mobile.rs @@ -80,6 +80,12 @@ impl Clipboard { )) } + pub fn read_image_png(&self) -> crate::Result> { + Err(crate::Error::Clipboard( + "Unsupported on this platform".to_string(), + )) + } + // Treat HTML as unsupported on mobile until tested pub fn write_html<'a, T: Into>>( &self,