mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 12:47:13 -06:00
Package: File › Package collects and writes the package on the background thread
File › Package… read every linked file and font and wrote the package on the UI thread, so the window stopped responding until the package was written. For a document that links 50 images of 21 MB each, OK with Background Export off, which does the same work, takes a median of 2.25 s in a release build on Windows. With Enable Background Export on (Preferences › File Handling), OK now takes the options, the report's Document Info text and a snapshot of the document, and closes the dialog. The thread that writes background saves and exports then collects the links and fonts and writes the package. The same OK takes 0.001 s, and the package holds the same bytes. A save of unsaved changes that Package starts is written first. With the preference off, on the web and over MCP and scripts, the package is made at once with the same reply as before. Docs: docs/target-app-parity.md.
This commit is contained in:
@@ -50,7 +50,7 @@ mod object;
|
||||
pub(crate) mod objects_on_path;
|
||||
mod opacitymask;
|
||||
mod overprint;
|
||||
mod package;
|
||||
pub mod package;
|
||||
mod paint;
|
||||
mod panelcmds;
|
||||
pub mod params;
|
||||
|
||||
@@ -306,6 +306,51 @@ impl<'a> Collector<'a> {
|
||||
}
|
||||
|
||||
fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let package = prepare(s, p)?;
|
||||
match &mut s.package_hand_off {
|
||||
Some(slot) => {
|
||||
*slot = Some(package);
|
||||
Ok(Value::Null)
|
||||
}
|
||||
None => package.write(&mut |path, bytes| write_file(path, bytes)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A package of the active document as `file.package` takes it from the session: the options, the
|
||||
/// names, the report's Document Info text and the document as it is now
|
||||
/// ([`Session::prepare_package`]). [`Package::write`] collects and writes it without the session,
|
||||
/// on any thread.
|
||||
pub struct Package {
|
||||
o: Options,
|
||||
doc_path: String,
|
||||
stem: String,
|
||||
name: String,
|
||||
root: Option<PathBuf>,
|
||||
info: Option<String>,
|
||||
doc: std::sync::Arc<Document>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Run `file.package` with `params` as a command (checked and journaled as any other) that
|
||||
/// collects and writes nothing → the package it prepared, which [`Package::write`] collects
|
||||
/// and writes (on the desktop with Background Export on, File › Package… does this on the
|
||||
/// background thread).
|
||||
pub fn prepare_package(&mut self, params: &Value) -> Result<Package> {
|
||||
self.package_hand_off = Some(None);
|
||||
let r = self.execute(C, params);
|
||||
let handed = self.package_hand_off.take().flatten();
|
||||
match (r, handed) {
|
||||
(Err(e), _) => Err(e),
|
||||
(Ok(_), Some(package)) => Ok(package),
|
||||
// Every run that succeeds hands one over; otherwise the package is prepared here.
|
||||
(Ok(_), None) => prepare(self, params),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What `file.package` reads from the session: the options, the checks of the document and the
|
||||
/// name, the report's Document Info text and the document.
|
||||
fn prepare(s: &mut Session, p: &Value) -> Result<Package> {
|
||||
let o = Options {
|
||||
copy_links: bool_or(p, "copyLinks", true),
|
||||
links_folder: bool_or(p, "linksFolder", true),
|
||||
@@ -322,8 +367,23 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let folder = str_param(p, "folder");
|
||||
let root = folder.map(|f| Path::new(f).join(&name));
|
||||
let info = if o.report { Some(super::docinfo::report(s, false)?) } else { None };
|
||||
let st = s.doc()?;
|
||||
let mut doc = (*st.doc).clone();
|
||||
let doc = s.doc()?.doc.clone();
|
||||
Ok(Package { o, doc_path, stem, name, root, info, doc })
|
||||
}
|
||||
|
||||
impl Package {
|
||||
/// Collect the linked files and fonts and write the package's files with `write` into
|
||||
/// `folder/name`, making its folders first, or into the zip without a folder → the reply of
|
||||
/// `file.package`.
|
||||
pub fn write(self, write: &mut dyn FnMut(&str, &[u8]) -> Result<()>) -> Result<Value> {
|
||||
collect_and_write(self, write)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Package::write`].
|
||||
fn collect_and_write(package: Package, write: &mut dyn FnMut(&str, &[u8]) -> Result<()>) -> Result<Value> {
|
||||
let Package { o, doc_path, stem, name, root, info, doc } = package;
|
||||
let mut doc = (*doc).clone();
|
||||
let doc_name = format!("{stem}.{}", vectorcraft_format::EXTENSION);
|
||||
// Prepare original links for the destination *before* rebasing copied links.
|
||||
// Recalculating afterward would overwrite the portable relative paths.
|
||||
@@ -366,7 +426,7 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(dir) = path.parent() {
|
||||
create_dir(&dir.to_string_lossy())?;
|
||||
}
|
||||
write_file(&path.to_string_lossy(), bytes)?;
|
||||
write(&path.to_string_lossy(), bytes)?;
|
||||
}
|
||||
out["folder"] = json!(root.to_string_lossy());
|
||||
}
|
||||
|
||||
@@ -1378,6 +1378,9 @@ pub struct Session {
|
||||
/// While `command.batch` runs: the documents its steps closed or replaced (Revert), which an
|
||||
/// error brings back; `None` otherwise.
|
||||
pub(crate) batch_stash: Option<Vec<DocState>>,
|
||||
/// While [`Session::prepare_package`] runs `file.package`: the package the command prepared,
|
||||
/// which it leaves here instead of writing it; `None` otherwise.
|
||||
pub(crate) package_hand_off: Option<Option<cmd::package::Package>>,
|
||||
/// Where Data Recovery keeps its copies ([`cmd::recovery`]).
|
||||
pub recovery: cmd::recovery::Recovery,
|
||||
/// While the active tool's actions run: their commands aren't "a command from outside the
|
||||
@@ -1466,6 +1469,7 @@ impl Session {
|
||||
journal_note: Default::default(),
|
||||
note_depth: 1,
|
||||
batch_stash: None,
|
||||
package_hand_off: None,
|
||||
recovery: Default::default(),
|
||||
in_tool_actions: false,
|
||||
previewing: false,
|
||||
|
||||
@@ -124,6 +124,80 @@ fn without_a_folder_the_zip_has_the_same_files() {
|
||||
assert_eq!(f["files"], r["files"]);
|
||||
}
|
||||
|
||||
/// The reply of a prepared package's [`cmd::package::Package::write`] and the files it gave the
|
||||
/// writer, by path.
|
||||
fn write_prepared(package: cmd::package::Package) -> (Value, BTreeMap<String, Vec<u8>>) {
|
||||
let mut files = BTreeMap::new();
|
||||
let mut write = |path: &str, bytes: &[u8]| {
|
||||
files.insert(path.to_string(), bytes.to_vec());
|
||||
Ok(())
|
||||
};
|
||||
let r = package.write(&mut write).unwrap_or_else(|e| panic!("{e}"));
|
||||
(r, files)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prepared_package_writes_what_file_package_writes() {
|
||||
let dir = Folder::new("package-prepared");
|
||||
let mut s = poster(&dir);
|
||||
run(&mut s, "text.create", json!({"x": 10, "y": 80, "text": "Gone", "font": "No Such Font Family"}));
|
||||
save(&mut s, &dir.file("poster.vectorcraft"));
|
||||
std::fs::remove_file(dir.file("art/blue.png")).unwrap();
|
||||
open(&mut s, &dir.file("poster.vectorcraft"));
|
||||
let p = json!({"folder": dir.file("out")});
|
||||
let want = run(&mut s, "file.package", p.clone());
|
||||
assert_eq!((want["missingLinks"].clone(), want["skippedFonts"][0]["font"].clone()), (json!(["blue.png"]), json!("No Such Font Family Regular")));
|
||||
let root = std::path::Path::new(&dir.file("out")).join("poster Folder");
|
||||
let on_disk: BTreeMap<String, Vec<u8>> = want["files"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|f| root.join(f.as_str().unwrap()))
|
||||
.map(|f| (f.to_string_lossy().into_owned(), std::fs::read(&f).unwrap()))
|
||||
.collect();
|
||||
assert_eq!(on_disk.len(), 4, "{want}");
|
||||
std::fs::remove_dir_all(&root).unwrap();
|
||||
// A prepared package is journaled as the command, and nothing is written until `Package::write` runs.
|
||||
let journaled = s.journal.len();
|
||||
let package = s.prepare_package(&p).unwrap();
|
||||
assert_eq!((s.journal.len(), s.journal.last().map(|(id, _)| id.as_str())), (journaled + 1, Some("file.package")));
|
||||
assert!(!root.exists());
|
||||
let (got, files) = write_prepared(package);
|
||||
assert_eq!(got, want);
|
||||
assert_eq!(files.keys().collect::<Vec<_>>(), on_disk.keys().collect::<Vec<_>>());
|
||||
for (path, bytes) in &on_disk {
|
||||
assert!(files[path] == *bytes, "{path} differs");
|
||||
}
|
||||
// Without a folder, the package is the same zip and no file is written.
|
||||
let want = run(&mut s, "file.package", json!({}));
|
||||
let (got, files) = write_prepared(s.prepare_package(&json!({})).unwrap());
|
||||
assert!(files.is_empty(), "{:?}", files.keys());
|
||||
let unzipped = |r: &Value| unzip(&vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap());
|
||||
assert_eq!(unzipped(&got), unzipped(&want));
|
||||
for key in ["files", "links", "fonts", "missingLinks", "skippedFonts", "warnings", "name", "bytes"] {
|
||||
assert_eq!(got[key], want[key], "{key}");
|
||||
}
|
||||
// The checks come first, as the command's do.
|
||||
let e = s.prepare_package(&json!({"name": "../up"})).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(e.contains("can't name a folder"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prepared_package_holds_the_document_as_it_was() {
|
||||
let dir = Folder::new("package-snapshot");
|
||||
let mut s = poster(&dir);
|
||||
let nodes = s.doc().unwrap().doc.node_count();
|
||||
let package = s.prepare_package(&json!({})).unwrap();
|
||||
run(&mut s, "select.all", json!({}));
|
||||
run(&mut s, "edit.clear", json!({}));
|
||||
run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}));
|
||||
let (r, _) = write_prepared(package);
|
||||
let zip = unzip(&vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap());
|
||||
let doc = vectorcraft_format::load(&zip["poster Folder/poster.vectorcraft"]).unwrap();
|
||||
assert_eq!(doc.node_count(), nodes);
|
||||
assert_eq!(r["links"], 2, "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_leave_out_links_fonts_and_the_report() {
|
||||
let dir = Folder::new("package-options");
|
||||
|
||||
Reference in New Issue
Block a user