//! The system clipboard for "Copy as picture" (plan E14): the figure as the //! platform's vector type plus PNG, so it pastes as vectors into Keynote, //! PowerPoint, Word and Inkscape. Native only; the engine reports "not //! available" without one (wasm, MCP, tests). use clipboard_rs::{Clipboard, ClipboardContent, ClipboardContext}; use rustfig_engine::picture::{PictureClipboard, PictureData, PictureKind}; /// The clipboard type name of `kind` on this platform. pub fn type_name(kind: PictureKind) -> &'static str { match kind { PictureKind::Png => { if cfg!(target_os = "macos") { "public.png" } else if cfg!(target_os = "windows") { "PNG" } else { "image/png" } } PictureKind::Pdf => "com.adobe.pdf", PictureKind::Svg => "image/svg+xml", // Registered format; Office reads CF_ENHMETAFILE handles, which // clipboard-rs cannot set (see docs/architecture.md). PictureKind::Emf => "image/x-emf", } } /// The clipboard type of the cut buffer's Fig text (E7), which another /// RustFig window reads back. pub fn fig_type_name() -> &'static str { if cfg!(target_os = "macos") { "org.rustfig.fig" } else if cfg!(target_os = "windows") { "RustFig Fig" } else { "application/x-rustfig-fig" } } /// The real clipboard, opened on first use and kept (on X11 the process /// serves the data until something else is copied). #[derive(Default)] pub struct SystemPictureClipboard { ctx: Option, } impl SystemPictureClipboard { fn context(&mut self) -> Result<&ClipboardContext, String> { if self.ctx.is_none() { self.ctx = Some(ClipboardContext::new().map_err(|e| e.to_string())?); } self.ctx.as_ref().ok_or_else(|| "no clipboard".to_string()) } } impl PictureClipboard for SystemPictureClipboard { fn set_objects(&mut self, fig: &str, items: &[PictureData]) -> Result<(), String> { let ctx = self.context()?; let mut contents = vec![ClipboardContent::Other( fig_type_name().to_string(), fig.as_bytes().to_vec(), )]; contents.extend( items .iter() .map(|i| ClipboardContent::Other(type_name(i.kind).to_string(), i.bytes.clone())), ); ctx.set(contents).map_err(|e| e.to_string()) } fn get_fig(&mut self) -> Option { let ctx = self.context().ok()?; let bytes = ctx.get_buffer(fig_type_name()).ok()?; String::from_utf8(bytes).ok() } fn get_text(&mut self) -> Option { let ctx = self.context().ok()?; ctx.get_text().ok().filter(|t| !t.is_empty()) } fn get_image(&mut self) -> Option> { let ctx = self.context().ok()?; // PNG only: every platform's screenshot and image copy offers it. ctx.get_buffer(type_name(PictureKind::Png)) .ok() .filter(|b| !b.is_empty()) } fn set_pictures(&mut self, items: &[PictureData]) -> Result<(), String> { if self.ctx.is_none() { self.ctx = Some(ClipboardContext::new().map_err(|e| e.to_string())?); } let ctx = self.ctx.as_ref().ok_or("no clipboard")?; let contents = items .iter() .map(|i| ClipboardContent::Other(type_name(i.kind).to_string(), i.bytes.clone())) .collect(); ctx.set(contents).map_err(|e| e.to_string()) } } #[cfg(test)] mod tests { use super::*; #[test] fn type_names_are_distinct_and_set() { let kinds = [PictureKind::Pdf, PictureKind::Svg, PictureKind::Emf, PictureKind::Png]; let names: std::collections::BTreeSet<_> = kinds.iter().map(|k| type_name(*k)).collect(); assert_eq!(names.len(), kinds.len()); } } .