98 lines
3.2 KiB
Rust
98 lines
3.2 KiB
Rust
|
|
use std::path::PathBuf;
|
||
|
|
use tempfile::Builder;
|
||
|
|
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub enum PasteImageError {
|
||
|
|
ClipboardUnavailable(String),
|
||
|
|
NoImage(String),
|
||
|
|
EncodeFailed(String),
|
||
|
|
IoError(String),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl std::fmt::Display for PasteImageError {
|
||
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
|
|
match self {
|
||
|
|
PasteImageError::ClipboardUnavailable(msg) => write!(f, "clipboard unavailable: {msg}"),
|
||
|
|
PasteImageError::NoImage(msg) => write!(f, "no image on clipboard: {msg}"),
|
||
|
|
PasteImageError::EncodeFailed(msg) => write!(f, "could not encode image: {msg}"),
|
||
|
|
PasteImageError::IoError(msg) => write!(f, "io error: {msg}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
impl std::error::Error for PasteImageError {}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
|
|
pub enum EncodedImageFormat {
|
||
|
|
Png,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl EncodedImageFormat {
|
||
|
|
pub fn label(self) -> &'static str {
|
||
|
|
match self {
|
||
|
|
EncodedImageFormat::Png => "PNG",
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone)]
|
||
|
|
pub struct PastedImageInfo {
|
||
|
|
pub width: u32,
|
||
|
|
pub height: u32,
|
||
|
|
pub encoded_format: EncodedImageFormat, // Always PNG for now.
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Capture image from system clipboard, encode to PNG, and return bytes + info.
|
||
|
|
pub fn paste_image_as_png() -> Result<(Vec<u8>, PastedImageInfo), PasteImageError> {
|
||
|
|
tracing::debug!("attempting clipboard image read");
|
||
|
|
let mut cb = arboard::Clipboard::new()
|
||
|
|
.map_err(|e| PasteImageError::ClipboardUnavailable(e.to_string()))?;
|
||
|
|
let img = cb
|
||
|
|
.get_image()
|
||
|
|
.map_err(|e| PasteImageError::NoImage(e.to_string()))?;
|
||
|
|
let w = img.width as u32;
|
||
|
|
let h = img.height as u32;
|
||
|
|
|
||
|
|
let mut png: Vec<u8> = Vec::new();
|
||
|
|
let Some(rgba_img) = image::RgbaImage::from_raw(w, h, img.bytes.into_owned()) else {
|
||
|
|
return Err(PasteImageError::EncodeFailed("invalid RGBA buffer".into()));
|
||
|
|
};
|
||
|
|
let dyn_img = image::DynamicImage::ImageRgba8(rgba_img);
|
||
|
|
tracing::debug!("clipboard image decoded RGBA {w}x{h}");
|
||
|
|
{
|
||
|
|
let mut cursor = std::io::Cursor::new(&mut png);
|
||
|
|
dyn_img
|
||
|
|
.write_to(&mut cursor, image::ImageFormat::Png)
|
||
|
|
.map_err(|e| PasteImageError::EncodeFailed(e.to_string()))?;
|
||
|
|
}
|
||
|
|
|
||
|
|
tracing::debug!(
|
||
|
|
"clipboard image encoded to PNG ({len} bytes)",
|
||
|
|
len = png.len()
|
||
|
|
);
|
||
|
|
Ok((
|
||
|
|
png,
|
||
|
|
PastedImageInfo {
|
||
|
|
width: w,
|
||
|
|
height: h,
|
||
|
|
encoded_format: EncodedImageFormat::Png,
|
||
|
|
},
|
||
|
|
))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Convenience: write to a temp file and return its path + info.
|
||
|
|
pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImageError> {
|
||
|
|
let (png, info) = paste_image_as_png()?;
|
||
|
|
// Create a unique temporary file with a .png suffix to avoid collisions.
|
||
|
|
let tmp = Builder::new()
|
||
|
|
.prefix("codex-clipboard-")
|
||
|
|
.suffix(".png")
|
||
|
|
.tempfile()
|
||
|
|
.map_err(|e| PasteImageError::IoError(e.to_string()))?;
|
||
|
|
std::fs::write(tmp.path(), &png).map_err(|e| PasteImageError::IoError(e.to_string()))?;
|
||
|
|
// Persist the file (so it remains after the handle is dropped) and return its PathBuf.
|
||
|
|
let (_file, path) = tmp
|
||
|
|
.keep()
|
||
|
|
.map_err(|e| PasteImageError::IoError(e.error.to_string()))?;
|
||
|
|
Ok((path, info))
|
||
|
|
}
|