#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
use anyhow::{Context, Result};
use base64::Engine;
pub(super) fn encode_png(width: usize, height: usize, rgba: &[u8]) -> Result<Vec<u8>> {
let mut bytes = Vec::new();
{
let mut enc = png::Encoder::new(&mut bytes, width as u32, height as u32);
enc.set_color(png::ColorType::Rgba);
enc.set_depth(png::BitDepth::Eight);
let mut writer = enc.write_header().context("png header")?;
writer.write_image_data(rgba).context("png data")?;
}
Ok(bytes)
}
pub(super) fn png_bytes_data_url(bytes: &[u8]) -> String {
format!(
"data:image/png;base64,{}",
base64::engine::general_purpose::STANDARD.encode(bytes)
)
}
#[cfg(test)]
pub(super) fn png_data_url(width: usize, height: usize, rgba: &[u8]) -> Result<String> {
let bytes = encode_png(width, height, rgba)?;
Ok(png_bytes_data_url(&bytes))
}
impl super::App {
pub fn save_clipboard_image(&mut self, img: &arboard::ImageData) -> Option<String> {
let bytes = match encode_png(img.width, img.height, &img.bytes) {
Ok(b) => b,
Err(e) => {
self.status = format!("could not encode image: {e}");
return None;
}
};
let (dir, filename) = if self.incognito {
let d = self.incognito_img_dir.get_or_insert_with(|| {
let p =
std::env::temp_dir().join(format!("nexus-incognito-{}", uuid::Uuid::new_v4()));
let _ = std::fs::create_dir_all(&p);
p
});
(d.clone(), format!("{}.png", uuid::Uuid::new_v4()))
} else {
let dir = self.space.files_dir(&self.active_space.name);
if let Err(e) = std::fs::create_dir_all(&dir) {
self.status = format!("could not create {}: {e}", dir.display());
return None;
}
(dir, format!("{}.png", uuid::Uuid::new_v4()))
};
let path = dir.join(&filename);
if let Err(e) = std::fs::write(&path, &bytes) {
self.status = format!("could not write {}: {e}", path.display());
return None;
}
if !self.incognito {
let files_dir = self.space.files_dir(&self.active_space.name);
if std::fs::create_dir_all(&files_dir).is_ok() {
let _ = std::fs::write(files_dir.join(&filename), &bytes);
}
self.rescan_files();
}
Some(format!(""))
}
pub(crate) fn cleanup_incognito_images(&mut self) {
if let Some(d) = self.incognito_img_dir.take() {
let _ = std::fs::remove_dir_all(&d);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encodes_rgba_as_png_data_url() {
let rgba = [255, 0, 0, 255, 0, 0, 0, 0];
let url = png_data_url(2, 1, &rgba).unwrap();
assert!(url.starts_with("data:image/png;base64,"));
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD
.decode(url.strip_prefix("data:image/png;base64,").unwrap())
.unwrap();
let decoder = png::Decoder::new(std::io::Cursor::new(bytes));
let reader = decoder.read_info().unwrap();
assert_eq!(reader.info().width, 2);
assert_eq!(reader.info().height, 1);
}
#[tokio::test]
async fn save_clipboard_returns_markdown_snippet() {
let mut a = crate::app::tests::app_with_key();
a.incognito = true; let img = arboard::ImageData {
width: 2,
height: 1,
bytes: std::borrow::Cow::Owned(vec![255, 0, 0, 255, 0, 0, 0, 0]),
};
let result = a.save_clipboard_image(&img);
assert!(result.is_some());
let md = result.unwrap();
assert!(md.starts_with(");
assert!(md.ends_with(".png)"));
}
}