mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-27 11:56:50 -06:00
Save the PDF from File > Export frames to PDF
Fixes #437 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
8482c3e85b
commit
ec9b1197c9
@@ -84,6 +84,7 @@ pub fn invoke(app: &mut FigApp, id: &str) {
|
||||
"file.saveVersion" => crate::versions::start_save(app),
|
||||
"branch.create" => crate::versions::start_branch(app),
|
||||
"file.export" | "file.exportSvg" => app.ui.export_preview = true,
|
||||
"file.exportFramesPdf" => export_frames_pdf(app),
|
||||
"edit.copyAsCss" => {
|
||||
let ids = app.session.selection();
|
||||
if let (Some(first), Ok(doc)) = (ids.first(), app.session.doc()) {
|
||||
@@ -156,6 +157,15 @@ pub fn invoke(app: &mut FigApp, id: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Every visible top-level frame of the page as one PDF page each, through a save dialog.
|
||||
fn export_frames_pdf(app: &mut FigApp) {
|
||||
let name = app.session.doc().map(|d| d.name.clone()).unwrap_or_else(|_| "Untitled".into());
|
||||
let Some(p) = app.services.pick_save(tr("Export"), &format!("{name}.pdf"), &["pdf"]) else { return };
|
||||
if app.run("file.exportFramesPdf", json!({"path": p.to_string_lossy()})).is_some() {
|
||||
app.toast(trf("Exported {name}", &[("name", &p.file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default())]));
|
||||
}
|
||||
}
|
||||
|
||||
fn figcraft_svg_export(doc: &figcraft_doc::Document, ids: &[figcraft_doc::NodeId]) -> Option<String> {
|
||||
let bytes = figcraft_engine::cmd::file::export_bytes(doc, ids, figcraft_doc::ExportFormat::Svg, 1.0).ok()?;
|
||||
String::from_utf8(bytes).ok()
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//! File > Export frames to PDF: the menu asks where to save and writes the frames' PDF there,
|
||||
//! one visible top-level frame per page.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use figcraft_engine::Session;
|
||||
use figcraft_ui_egui::{FigApp, Services, menus};
|
||||
use serde_json::json;
|
||||
|
||||
/// Services whose save dialog always returns the given path.
|
||||
struct SaveTo(PathBuf);
|
||||
impl Services for SaveTo {
|
||||
fn pick_save(&mut self, _title: &str, _name: &str, _exts: &[&str]) -> Option<PathBuf> {
|
||||
Some(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn out_path(name: &str) -> PathBuf {
|
||||
let p = std::env::temp_dir().join(format!("figcraft-{}-{name}.pdf", std::process::id()));
|
||||
let _ = std::fs::remove_file(&p);
|
||||
p
|
||||
}
|
||||
|
||||
fn frame(app: &mut FigApp, x: i32, y: i32) -> serde_json::Value {
|
||||
app.session.execute("node.create", &json!({"type": "frame", "x": x, "y": y, "width": 100, "height": 80})).unwrap()
|
||||
}
|
||||
|
||||
/// The number of `/Type /Page` objects (not `/Pages`) in a PDF.
|
||||
fn page_count(pdf: &[u8]) -> usize {
|
||||
let s = String::from_utf8_lossy(pdf).replace("/Type/Page", "/Type /Page");
|
||||
s.match_indices("/Type /Page").filter(|(i, _)| !s[i + 11..].starts_with('s')).count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frames_pdf_menu_saves_the_pdf() {
|
||||
let path = out_path("one");
|
||||
let mut app = FigApp::new(Session::new(), Box::new(SaveTo(path.clone())));
|
||||
frame(&mut app, 0, 0);
|
||||
menus::invoke(&mut app, "file.exportFramesPdf");
|
||||
assert!(app.ui.last_error.is_none(), "{:?}", app.ui.last_error);
|
||||
let bytes = std::fs::read(&path).expect("the menu writes the PDF where the user chose");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert!(bytes.starts_with(b"%PDF"));
|
||||
assert_eq!(page_count(&bytes), 1);
|
||||
|
||||
// The command still returns the data when called without a destination.
|
||||
let value = app.session.execute("file.exportFramesPdf", &json!({})).unwrap();
|
||||
assert_eq!(value["format"], "pdf");
|
||||
assert_eq!(value["pages"], 1);
|
||||
assert!(value["data"].as_str().unwrap().starts_with("JVBERi0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frames_pdf_menu_one_page_per_visible_frame() {
|
||||
let path = out_path("two");
|
||||
let mut app = FigApp::new(Session::new(), Box::new(SaveTo(path.clone())));
|
||||
frame(&mut app, 0, 0);
|
||||
frame(&mut app, 300, 0);
|
||||
let hidden = frame(&mut app, 600, 0);
|
||||
let id = hidden["id"].clone();
|
||||
app.session.execute("node.set", &json!({"id": id, "visible": false})).unwrap();
|
||||
menus::invoke(&mut app, "file.exportFramesPdf");
|
||||
assert!(app.ui.last_error.is_none(), "{:?}", app.ui.last_error);
|
||||
let bytes = std::fs::read(&path).expect("the menu writes the PDF where the user chose");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert_eq!(page_count(&bytes), 2);
|
||||
}
|
||||
Reference in New Issue
Block a user