Graphite/frontend/iced/src/file_io.rs

162 lines
5.9 KiB
Rust

use graphite_editor::messages::prelude::DocumentId;
use std::path::PathBuf;
use std::sync::mpsc::Sender;
use std::thread;
const GRAPHITE_EXTENSION: &str = "graphite";
const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "svg", "bmp", "gif"];
#[derive(Debug, Clone)]
pub enum FileIoResult {
Opened { path: PathBuf, content: Vec<u8> },
Imported { path: PathBuf, content: Vec<u8> },
SavedDocument { document_id: DocumentId, path: PathBuf },
ExportComplete,
Cancelled,
Failed(String),
}
pub fn spawn_open_dialog(sender: Sender<FileIoResult>) {
thread::spawn(move || {
let mut all_exts: Vec<&str> = Vec::with_capacity(IMAGE_EXTENSIONS.len() + 1);
all_exts.push(GRAPHITE_EXTENSION);
all_exts.extend_from_slice(IMAGE_EXTENSIONS);
let dialog = rfd::FileDialog::new()
.set_title("Open File")
.add_filter("Graphite & Images", &all_exts)
.add_filter("Graphite Document", &[GRAPHITE_EXTENSION])
.add_filter("Images", IMAGE_EXTENSIONS);
let result = match dialog.pick_file() {
Some(path) => match std::fs::read(&path) {
Ok(content) => FileIoResult::Opened { path, content },
Err(e) => FileIoResult::Failed(format!("Failed to read {}: {e}", path.display())),
},
None => FileIoResult::Cancelled,
};
let _ = sender.send(result);
});
}
pub fn spawn_import_dialog(sender: Sender<FileIoResult>) {
thread::spawn(move || {
let dialog = rfd::FileDialog::new().set_title("Import Image").add_filter("Images", IMAGE_EXTENSIONS);
let result = match dialog.pick_file() {
Some(path) => match std::fs::read(&path) {
Ok(content) => FileIoResult::Imported { path, content },
Err(e) => FileIoResult::Failed(format!("Failed to read {}: {e}", path.display())),
},
None => FileIoResult::Cancelled,
};
let _ = sender.send(result);
});
}
pub fn spawn_save_dialog(sender: Sender<FileIoResult>, suggested_name: String, suggested_folder: Option<PathBuf>, content: Vec<u8>) {
thread::spawn(move || {
let result = match resolve_save_path(&suggested_name, suggested_folder.as_deref(), None) {
Some(path) => match std::fs::write(&path, &content) {
Ok(()) => FileIoResult::ExportComplete,
Err(e) => FileIoResult::Failed(format!("Failed to write {}: {e}", path.display())),
},
None => FileIoResult::Cancelled,
};
let _ = sender.send(result);
});
}
pub fn spawn_save_document(sender: Sender<FileIoResult>, document_id: DocumentId, suggested_name: String, explicit_path: Option<PathBuf>, suggested_folder: Option<PathBuf>, content: Vec<u8>) {
thread::spawn(move || {
let chosen = match explicit_path {
Some(path) => Some(path),
None => resolve_save_path(&suggested_name, suggested_folder.as_deref(), Some(GRAPHITE_EXTENSION)),
};
let result = match chosen {
Some(path) => match std::fs::write(&path, &content) {
Ok(()) => FileIoResult::SavedDocument { document_id, path },
Err(e) => FileIoResult::Failed(format!("Failed to write {}: {e}", path.display())),
},
None => FileIoResult::Cancelled,
};
let _ = sender.send(result);
});
}
pub fn spawn_export_image(sender: Sender<FileIoResult>, svg: String, name: String, mime: String, size: (f64, f64)) {
thread::spawn(move || {
let extension = match mime.as_str() {
"image/svg+xml" => "svg",
"image/png" => "png",
"image/jpeg" => "jpg",
_ => "bin",
};
let chosen = resolve_save_path(&name, None, Some(extension));
let Some(path) = chosen else {
let _ = sender.send(FileIoResult::Cancelled);
return;
};
let bytes = match mime.as_str() {
"image/svg+xml" => Ok(svg.into_bytes()),
"image/png" => rasterise_svg(&svg, size, false),
"image/jpeg" => rasterise_svg(&svg, size, true),
other => Err(format!("Unsupported export mime: {other}")),
};
let result = match bytes.and_then(|b| std::fs::write(&path, &b).map_err(|e| format!("Failed to write {}: {e}", path.display()))) {
Ok(()) => FileIoResult::ExportComplete,
Err(message) => FileIoResult::Failed(message),
};
let _ = sender.send(result);
});
}
fn resolve_save_path(suggested_name: &str, suggested_folder: Option<&std::path::Path>, force_extension: Option<&str>) -> Option<PathBuf> {
let mut dialog = rfd::FileDialog::new().set_title("Save File").set_file_name(suggested_name);
if let Some(folder) = suggested_folder {
dialog = dialog.set_directory(folder);
}
if let Some(ext) = force_extension {
let label = format!("{} file", ext.to_ascii_uppercase());
dialog = dialog.add_filter(label.as_str(), &[ext]);
}
let mut path = dialog.save_file()?;
if let Some(ext) = force_extension {
if path.extension().and_then(|e| e.to_str()).map(|e| !e.eq_ignore_ascii_case(ext)).unwrap_or(true) {
path.set_extension(ext);
}
}
Some(path)
}
fn rasterise_svg(svg: &str, size: (f64, f64), jpeg: bool) -> Result<Vec<u8>, String> {
let (width, height) = (size.0.max(1.0) as u32, size.1.max(1.0) as u32);
let opts = usvg::Options::default();
let tree = usvg::Tree::from_str(svg, &opts).map_err(|e| format!("SVG parse error: {e}"))?;
let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height).ok_or_else(|| "Pixmap allocation failed (zero or oversized dimensions)".to_string())?;
if jpeg {
pixmap.fill(resvg::tiny_skia::Color::WHITE);
}
let tree_size = tree.size();
let scale_x = width as f32 / tree_size.width();
let scale_y = height as f32 / tree_size.height();
let transform = resvg::tiny_skia::Transform::from_scale(scale_x, scale_y);
resvg::render(&tree, transform, &mut pixmap.as_mut());
if jpeg {
let rgba = pixmap.data();
let buffer = image::RgbaImage::from_raw(width, height, rgba.to_vec()).ok_or_else(|| "RGBA buffer mismatch".to_string())?;
let rgb = image::DynamicImage::ImageRgba8(buffer).to_rgb8();
let mut out = Vec::with_capacity((width * height * 3) as usize);
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, 92);
encoder.encode(rgb.as_raw(), width, height, image::ExtendedColorType::Rgb8).map_err(|e| format!("JPEG encode error: {e}"))?;
Ok(out)
} else {
pixmap.encode_png().map_err(|e| format!("PNG encode error: {e}"))
}
}