use std::path::PathBuf;
#[derive(Debug, Clone)]
pub enum PasteImageError {
ClipboardUnavailable(String),
NoImage(String),
EncodeFailed(String),
IoError(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncodedImageFormat {
Png,
Jpeg,
Other,
}
impl EncodedImageFormat {
pub fn label(&self) -> &'static str {
match self {
EncodedImageFormat::Png => "PNG",
EncodedImageFormat::Jpeg => "JPEG",
EncodedImageFormat::Other => "unknown",
}
}
}
#[derive(Debug, Clone)]
pub struct PastedImageInfo {
pub width: u32,
pub height: u32,
pub encoded_format: EncodedImageFormat,
}
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}")
}
}
}
}
#[cfg(not(target_os = "android"))]
pub fn paste_image_as_png() -> Result<(Vec<u8>, PastedImageInfo), PasteImageError> {
let _span = tracing::debug_span!("paste_image_as_png").entered();
tracing::debug!("attempting clipboard image read");
let mut cb = arboard::Clipboard::new()
.map_err(|e| PasteImageError::ClipboardUnavailable(e.to_string()))?;
let dyn_img = if let Ok(img) = cb.get_image() {
let w = img.width as u32;
let h = img.height as u32;
tracing::debug!("clipboard image from data: {}x{}", w, h);
let Some(rgba_img) = image::RgbaImage::from_raw(w, h, img.bytes.into_owned()) else {
return Err(PasteImageError::EncodeFailed("invalid RGBA buffer".into()));
};
image::DynamicImage::ImageRgba8(rgba_img)
} else if let Ok(files) = cb.get().file_list() {
if let Some(img) = files.into_iter().find_map(|f| image::open(f).ok()) {
tracing::debug!(
"clipboard image from file: {}x{}",
img.width(),
img.height()
);
img
} else {
return Err(PasteImageError::NoImage(
"no valid image file in clipboard".into(),
));
}
} else {
return Err(PasteImageError::NoImage(
"clipboard does not contain image data or image files".into(),
));
};
let mut png: Vec<u8> = Vec::new();
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()))?;
Ok((
png,
PastedImageInfo {
width: dyn_img.width(),
height: dyn_img.height(),
encoded_format: EncodedImageFormat::Png,
},
))
}
#[cfg(not(target_os = "android"))]
pub fn paste_image_to_temp_png() -> Result<(PathBuf, PastedImageInfo), PasteImageError> {
match paste_image_as_png() {
Ok((png, info)) => {
let tmp = tempfile::Builder::new()
.prefix("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()))?;
let (_file, path) = tmp
.keep()
.map_err(|e| PasteImageError::IoError(e.error.to_string()))?;
Ok((path, info))
}
Err(e) => {
#[cfg(target_os = "linux")]
{
try_wsl_clipboard_fallback(&e).ok_or(e)
}
#[cfg(not(target_os = "linux"))]
{
Err(e)
}
}
}
}
#[cfg(target_os = "linux")]
fn try_wsl_clipboard_fallback(error: &PasteImageError) -> Option<(PathBuf, PastedImageInfo)> {
use PasteImageError::{ClipboardUnavailable, NoImage};
if !super::clipboard_text::is_probably_wsl()
|| !matches!(error, ClipboardUnavailable(_) | NoImage(_))
{
return None;
}
tracing::debug!("attempting Windows PowerShell clipboard fallback");
let Some(win_path) = try_dump_windows_clipboard_image() else {
return None;
};
tracing::debug!("powershell produced path: {}", win_path);
let Some(mapped_path) = convert_windows_path_to_wsl(&win_path) else {
return None;
};
let Ok((w, h)) = image::image_dimensions(&mapped_path) else {
return None;
};
Some((
mapped_path,
PastedImageInfo {
width: w,
height: h,
encoded_format: EncodedImageFormat::Png,
},
))
}
#[cfg(target_os = "linux")]
fn try_dump_windows_clipboard_image() -> Option<String> {
let script = r#"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;
$img = Get-Clipboard -Format Image;
if ($img -ne $null) {
$p=[System.IO.Path]::GetTempFileName();
$p = [System.IO.Path]::ChangeExtension($p,'png');
$img.Save($p,[System.Drawing.Imaging.ImageFormat]::Png);
Write-Output $p
} else {
exit 1
}
"#;
for cmd in ["powershell.exe", "pwsh", "powershell"] {
match std::process::Command::new(cmd)
.args(["-NoProfile", "-Command", script])
.output()
{
Ok(output) if output.status.success() => {
let win_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !win_path.is_empty() {
return Some(win_path);
}
}
_ => continue,
}
}
None
}
#[cfg(target_os = "linux")]
fn convert_windows_path_to_wsl(input: &str) -> Option<PathBuf> {
if input.starts_with("\\\\") {
return None;
}
let drive_letter = input.chars().next()?.to_ascii_lowercase();
if !drive_letter.is_ascii_lowercase() {
return None;
}
if input.get(1..2) != Some(":") {
return None;
}
let mut result = PathBuf::from(format!("/mnt/{drive_letter}"));
for component in input
.get(2..)?
.trim_start_matches(['\\', '/'])
.split(['\\', '/'])
.filter(|c| !c.is_empty())
{
result.push(component);
}
Some(result)
}
#[cfg(all(test, not(target_os = "android")))]
mod tests {
#[allow(unused_imports)]
use super::*;
#[test]
fn test_normalize_file_url() {
let input = "file:///tmp/example.png";
assert!(input.starts_with("file://"));
}
}