clipboard_rs/
common.rs

1use image::imageops::FilterType;
2use image::{ColorType, DynamicImage, GenericImageView, ImageFormat, RgbaImage};
3use std::error::Error;
4use std::io::Cursor;
5pub type Result<T> = std::result::Result<T, Box<dyn Error + Send + Sync + 'static>>;
6
7pub trait ContentData {
8	fn get_format(&self) -> ContentFormat;
9
10	fn as_bytes(&self) -> &[u8];
11
12	fn as_str(&self) -> Result<&str>;
13}
14
15pub trait ClipboardHandler {
16	fn on_clipboard_change(&mut self);
17}
18
19pub enum ClipboardContent {
20	Text(String),
21	Rtf(String),
22	Html(String),
23	Image(RustImageData),
24	Files(Vec<String>),
25	Other(String, Vec<u8>),
26}
27
28impl ContentData for ClipboardContent {
29	fn get_format(&self) -> ContentFormat {
30		match self {
31			ClipboardContent::Text(_) => ContentFormat::Text,
32			ClipboardContent::Rtf(_) => ContentFormat::Rtf,
33			ClipboardContent::Html(_) => ContentFormat::Html,
34			ClipboardContent::Image(_) => ContentFormat::Image,
35			ClipboardContent::Files(_) => ContentFormat::Files,
36			ClipboardContent::Other(format, _) => ContentFormat::Other(format.clone()),
37		}
38	}
39
40	fn as_bytes(&self) -> &[u8] {
41		match self {
42			ClipboardContent::Text(data) => data.as_bytes(),
43			ClipboardContent::Rtf(data) => data.as_bytes(),
44			ClipboardContent::Html(data) => data.as_bytes(),
45			// dynamic image is not supported to as bytes
46			ClipboardContent::Image(_) => &[],
47			ClipboardContent::Files(data) => {
48				// use first file path as data
49				if let Some(path) = data.first() {
50					path.as_bytes()
51				} else {
52					&[]
53				}
54			}
55			ClipboardContent::Other(_, data) => data.as_slice(),
56		}
57	}
58
59	fn as_str(&self) -> Result<&str> {
60		match self {
61			ClipboardContent::Text(data) => Ok(data),
62			ClipboardContent::Rtf(data) => Ok(data),
63			ClipboardContent::Html(data) => Ok(data),
64			ClipboardContent::Image(_) => Err("can't convert image to string".into()),
65			ClipboardContent::Files(data) => {
66				// use first file path as data
67				if let Some(path) = data.first() {
68					Ok(path)
69				} else {
70					Err("content is empty".into())
71				}
72			}
73			ClipboardContent::Other(_, data) => std::str::from_utf8(data).map_err(|e| e.into()),
74		}
75	}
76}
77
78#[derive(Clone)]
79pub enum ContentFormat {
80	Text,
81	Rtf,
82	Html,
83	Image,
84	Files,
85	Other(String),
86}
87
88pub struct RustImageData {
89	width: u32,
90	height: u32,
91	data: Option<DynamicImage>,
92}
93
94/// 此处的 `RustImageBuffer` 已经是带有图片格式的字节流,例如 png,jpeg;
95pub struct RustImageBuffer(Vec<u8>);
96
97pub trait RustImage: Sized {
98	/// create an empty image
99	fn empty() -> Self;
100
101	fn is_empty(&self) -> bool;
102
103	/// Read image from file path
104	fn from_path(path: &str) -> Result<Self>;
105
106	/// Create a new image from a byte slice
107	fn from_bytes(bytes: &[u8]) -> Result<Self>;
108
109	fn from_dynamic_image(image: DynamicImage) -> Self;
110
111	/// width and height
112	fn get_size(&self) -> (u32, u32);
113
114	/// Scale this image down to fit within a specific size.
115	/// Returns a new image. The image's aspect ratio is preserved.
116	/// The image is scaled to the maximum possible size that fits
117	/// within the bounds specified by `nwidth` and `nheight`.
118	///
119	/// This method uses a fast integer algorithm where each source
120	/// pixel contributes to exactly one target pixel.
121	/// May give aliasing artifacts if new size is close to old size.
122	fn thumbnail(&self, width: u32, height: u32) -> Result<Self>;
123
124	/// en: Adjust the size of the image without retaining the aspect ratio
125	/// zh: 调整图片大小,不保留长宽比
126	fn resize(&self, width: u32, height: u32, filter: FilterType) -> Result<Self>;
127
128	fn encode_image(
129		&self,
130		target_color_type: ColorType,
131		format: ImageFormat,
132	) -> Result<RustImageBuffer>;
133
134	fn to_jpeg(&self) -> Result<RustImageBuffer>;
135
136	/// en: Convert to png format, the returned image is a new image, and the data itself will not be modified
137	/// zh: 转为 png 格式,返回的为新的图片,本身数据不会修改
138	fn to_png(&self) -> Result<RustImageBuffer>;
139
140	#[cfg(target_os = "windows")]
141	fn to_bitmap(&self) -> Result<RustImageBuffer>;
142
143	fn save_to_path(&self, path: &str) -> Result<()>;
144
145	fn get_dynamic_image(&self) -> Result<DynamicImage>;
146
147	fn to_rgba8(&self) -> Result<RgbaImage>;
148}
149
150impl RustImage for RustImageData {
151	fn empty() -> Self {
152		RustImageData {
153			width: 0,
154			height: 0,
155			data: None,
156		}
157	}
158
159	fn is_empty(&self) -> bool {
160		self.data.is_none()
161	}
162
163	fn from_path(path: &str) -> Result<Self> {
164		let image = image::open(path)?;
165		let (width, height) = image.dimensions();
166		Ok(RustImageData {
167			width,
168			height,
169			data: Some(image),
170		})
171	}
172
173	fn from_bytes(bytes: &[u8]) -> Result<Self> {
174		let image = image::load_from_memory(bytes)?;
175		let (width, height) = image.dimensions();
176		Ok(RustImageData {
177			width,
178			height,
179			data: Some(image),
180		})
181	}
182
183	fn from_dynamic_image(image: DynamicImage) -> Self {
184		let (width, height) = image.dimensions();
185		RustImageData {
186			width,
187			height,
188			data: Some(image),
189		}
190	}
191
192	fn get_size(&self) -> (u32, u32) {
193		(self.width, self.height)
194	}
195
196	fn thumbnail(&self, width: u32, height: u32) -> Result<Self> {
197		match &self.data {
198			Some(image) => {
199				let resized = image.thumbnail(width, height);
200				Ok(RustImageData {
201					width: resized.width(),
202					height: resized.height(),
203					data: Some(resized),
204				})
205			}
206			None => Err("image is empty".into()),
207		}
208	}
209
210	fn resize(&self, width: u32, height: u32, filter: FilterType) -> Result<Self> {
211		match &self.data {
212			Some(image) => {
213				let resized = image.resize_exact(width, height, filter);
214				Ok(RustImageData {
215					width: resized.width(),
216					height: resized.height(),
217					data: Some(resized),
218				})
219			}
220			None => Err("image is empty".into()),
221		}
222	}
223
224	fn save_to_path(&self, path: &str) -> Result<()> {
225		match &self.data {
226			Some(image) => {
227				image.save(path)?;
228				Ok(())
229			}
230			None => Err("image is empty".into()),
231		}
232	}
233
234	fn get_dynamic_image(&self) -> Result<DynamicImage> {
235		match &self.data {
236			Some(image) => Ok(image.clone()),
237			None => Err("image is empty".into()),
238		}
239	}
240
241	fn to_rgba8(&self) -> Result<RgbaImage> {
242		match &self.data {
243			Some(image) => Ok(image.to_rgba8()),
244			None => Err("image is empty".into()),
245		}
246	}
247
248	// 私有辅助函数,处理图像格式转换和编码
249	fn encode_image(
250		&self,
251		target_color_type: ColorType,
252		format: ImageFormat,
253	) -> Result<RustImageBuffer> {
254		let image = self.data.as_ref().ok_or("image is empty")?;
255
256		let mut bytes = Vec::new();
257		match (image.color(), target_color_type) {
258			(ColorType::Rgba8, ColorType::Rgb8) => image
259				.to_rgb8()
260				.write_to(&mut Cursor::new(&mut bytes), format)?,
261			(_, ColorType::Rgba8) => image
262				.to_rgba8()
263				.write_to(&mut Cursor::new(&mut bytes), format)?,
264			_ => image.write_to(&mut Cursor::new(&mut bytes), format)?,
265		};
266		Ok(RustImageBuffer(bytes))
267	}
268
269	fn to_jpeg(&self) -> Result<RustImageBuffer> {
270		// JPEG 需要 RGB 格式(不支持 alpha 通道)
271		self.encode_image(ColorType::Rgb8, ImageFormat::Jpeg)
272	}
273
274	fn to_png(&self) -> Result<RustImageBuffer> {
275		// PNG 使用 RGBA 格式以支持透明度
276		self.encode_image(ColorType::Rgba8, ImageFormat::Png)
277	}
278
279	#[cfg(target_os = "windows")]
280	fn to_bitmap(&self) -> Result<RustImageBuffer> {
281		// BMP 使用 RGBA 格式
282		self.encode_image(ColorType::Rgba8, ImageFormat::Bmp)
283	}
284}
285
286impl RustImageBuffer {
287	pub fn get_bytes(&self) -> &[u8] {
288		&self.0
289	}
290
291	pub fn save_to_path(&self, path: &str) -> Result<()> {
292		std::fs::write(path, &self.0)?;
293		Ok(())
294	}
295}