use artifact::project::fs::Project;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use bezel::gpui::{App, Entity, Image, hash};
use editor::{Editor, ImageStore, Source};
use image::{ImageFormat, imageops::FilterType};
use markdown::BlockKind;
use std::{
borrow::Cow,
io::Cursor,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
const MARK: &str = "media-";
pub const LONG_EDGE: u32 = 1568;
static TARGET: Mutex<Option<PathBuf>> = Mutex::new(None);
#[derive(Clone)]
pub enum Attachment {
Bytes(Arc<Image>),
File(PathBuf),
}
pub fn init(cx: &mut App) {
editor::set_image_store(
cx,
ImageStore {
keep,
..ImageStore::default()
},
);
}
pub fn aim(content: Option<&Path>) {
if let Ok(mut target) = TARGET.lock() {
*target = content
.and_then(|content| content.ancestors().nth(4))
.map(|project| Project::new(project).assets());
}
}
pub fn store(dir: &Path, bytes: &[u8], extension: &str) -> Option<PathBuf> {
let file = dir.join(format!("{MARK}{:x}.{extension}", hash(&bytes)));
if !file.is_file() {
std::fs::create_dir_all(dir).ok()?;
std::fs::write(&file, bytes).ok()?;
}
Some(file)
}
pub fn keep_attachment(dir: &Path, attachment: &Attachment) -> Option<PathBuf> {
match attachment {
Attachment::Bytes(image) => store(dir, &image.bytes, image.format.extension()),
Attachment::File(path) => store(dir, &std::fs::read(path).ok()?, &extension(path)?),
}
}
pub fn line(path: &Path) -> String {
format!("", path.display())
}
pub fn attached(text: &str) -> Vec<PathBuf> {
markdown::parse(text)
.blocks
.into_iter()
.filter_map(|block| match block.kind {
BlockKind::Image { url, .. } if !url.is_empty() && !url.contains("://") => {
Some(PathBuf::from(url))
}
_ => None,
})
.collect()
}
pub fn detach(text: &str) -> (String, Vec<PathBuf>) {
let mut doc = markdown::parse(text);
let mut attachments = Vec::new();
doc.blocks.retain(|block| {
if let BlockKind::Image { url, .. } = &block.kind
&& !url.is_empty()
&& !url.contains("://")
{
attachments.push(PathBuf::from(url));
return false;
}
true
});
let text = if attachments.is_empty() {
text.to_owned()
} else {
markdown::serialize(&doc)
};
(text, attachments)
}
pub fn encode(path: &Path) -> Option<(String, &'static str)> {
let bytes = std::fs::read(path).ok()?;
let format = image::guess_format(&bytes).ok()?;
let picture = image::load_from_memory_with_format(&bytes, format).ok()?;
let fits = picture.width().max(picture.height()) <= LONG_EDGE;
let mime = match format {
ImageFormat::Png => Some("image/png"),
ImageFormat::Jpeg => Some("image/jpeg"),
ImageFormat::Gif => Some("image/gif"),
ImageFormat::WebP => Some("image/webp"),
_ => None,
};
if let (true, Some(mime)) = (fits, mime) {
return Some((STANDARD.encode(&bytes), mime));
}
let picture = match fits {
true => picture,
false => picture.resize(LONG_EDGE, LONG_EDGE, FilterType::Lanczos3),
};
let mut out = Cursor::new(Vec::new());
picture.write_to(&mut out, ImageFormat::Png).ok()?;
Some((STANDARD.encode(out.into_inner()), "image/png"))
}
fn extension(path: &Path) -> Option<String> {
Some(path.extension()?.to_str()?.to_ascii_lowercase())
}
fn keep(source: Source, _editor: &Entity<Editor>, _cx: &App) -> Option<String> {
let dir = TARGET.lock().ok()?.clone()?;
let (bytes, extension) = match source {
Source::Bytes(image) => (
Cow::Borrowed(image.bytes.as_slice()),
image.format.extension().to_owned(),
),
Source::File(path) => (Cow::Owned(std::fs::read(path).ok()?), extension(path)?),
};
let file = store(&dir, &bytes, &extension)?;
Some(file.to_string_lossy().into_owned())
}