162 lines
5.9 KiB
Rust
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}"))
|
|
}
|
|
}
|