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 }, Imported { path: PathBuf, content: Vec }, SavedDocument { document_id: DocumentId, path: PathBuf }, ExportComplete, Cancelled, Failed(String), } pub fn spawn_open_dialog(sender: Sender) { 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) { 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, suggested_name: String, suggested_folder: Option, content: Vec) { 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, document_id: DocumentId, suggested_name: String, explicit_path: Option, suggested_folder: Option, content: Vec) { 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, 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 { 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, 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}")) } }