diff --git a/contributors/people.toml b/contributors/people.toml index 4a5b3694..00a8796b 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -89,3 +89,7 @@ sync_github_name = false [people.Nael-Nathanael] sync_github_name = true + +[people.switch87] +real_name = "Gert Pellin" +sync_github_name = false diff --git a/crates/format/src/lib.rs b/crates/format/src/lib.rs index 5046d6ed..9ea60bf6 100644 --- a/crates/format/src/lib.rs +++ b/crates/format/src/lib.rs @@ -243,13 +243,32 @@ impl SaveOptions { Ok(()) } - /// The defaults for saving `doc` to disk: pretty when small (see [`PRETTY_MAX_OBJECTS`]). + /// The defaults for saving `doc` to disk: pretty when small (see [`PRETTY_MAX_OBJECTS`] and + /// [`PRETTY_MAX_BYTES`]). pub fn for_doc(doc: &Document) -> Self { let objects: usize = doc.layers.iter().map(|l| l.count()).sum(); - Self { pretty: objects <= PRETTY_MAX_OBJECTS, ..Self::default() } + Self { pretty: objects <= PRETTY_MAX_OBJECTS && compact_size_at_most(doc, PRETTY_MAX_BYTES), ..Self::default() } } } +/// Is `doc` at most `limit` bytes as compact JSON? Serializing stops as soon as it is past the +/// limit, so a large document costs no more than `limit` bytes of work. +fn compact_size_at_most(doc: &Document, limit: usize) -> bool { + struct Capped { + left: usize, + } + impl Write for Capped { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.left = self.left.checked_sub(buf.len()).ok_or_else(|| std::io::Error::other("over the limit"))?; + Ok(buf.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + serde_json::to_writer(Capped { left: limit }, doc).is_ok() +} + /// Serialize a document (pretty = human-diffable) in the current version. Editing-mode working /// copies are left out ([`Document::without_edit_modes`]). pub fn save(doc: &Document, pretty: bool) -> Vec { @@ -260,6 +279,11 @@ pub fn save(doc: &Document, pretty: bool) -> Vec { /// compact, where indentation would multiply the file size and slow opening down. pub const PRETTY_MAX_OBJECTS: usize = 2000; +/// Documents whose compact JSON is larger than this are saved compact even with few objects: +/// pretty-printing spreads every path anchor over several lines, so traced or generated art (in +/// layers, symbols or patterns alike) would grow about tenfold. +pub const PRETTY_MAX_BYTES: usize = 1 << 20; + /// Serialize for saving to disk: pretty when small, compact when large. pub fn save_file(doc: &Document) -> Vec { save_with(doc, &SaveOptions::for_doc(doc)).unwrap_or_default() diff --git a/crates/format/tests/save_options.rs b/crates/format/tests/save_options.rs index 44920d0b..ede14a41 100644 --- a/crates/format/tests/save_options.rs +++ b/crates/format/tests/save_options.rs @@ -8,9 +8,11 @@ use std::sync::Arc; use serde_json::Value; use vectorcraft_doc::links::LinkInfo; -use vectorcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind}; -use vectorcraft_format::{ICC_MIME, SaveOptions, load, load_file, pdf_content, save_with}; -use vectorcraft_geom::Affine; +use vectorcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind, Symbol}; +use vectorcraft_format::{ICC_MIME, PRETTY_MAX_BYTES, PRETTY_MAX_OBJECTS, SaveOptions, load, load_file, pdf_content, save_file, save_with}; +use vectorcraft_geom::path::{PathData, SubPath}; +use vectorcraft_geom::{Affine, Point}; +use vectorcraft_testkit::fixtures::DocBuilder; fn json_of(bytes: &[u8]) -> Value { serde_json::from_slice(bytes).unwrap() @@ -82,3 +84,66 @@ fn embedded_profiles_and_pdf_round_trip() { let damaged = String::from_utf8(plain).unwrap().replacen("AAECA/o=", "not base64!", 1); assert!(load_file(damaged.as_bytes()).unwrap().profiles.is_empty()); } + +/// A document of `paths` open paths with `anchors` anchors each. +fn paths_doc(paths: usize, anchors: usize) -> Document { + let mut b = DocBuilder::new(1000.0, 1000.0); + for i in 0..paths { + let points: Vec = (0..anchors).map(|j| Point::new(j as f64, i as f64 + (j % 2) as f64)).collect(); + let node = b.path_node(PathData::single(SubPath::polyline(&points, false)), Default::default()); + b.add(node); + } + b.doc +} + +fn is_pretty(bytes: &[u8]) -> bool { + bytes.windows(3).any(|w| w == b"\n ") +} + +#[test] +fn small_documents_are_saved_pretty() { + let d = paths_doc(10, 10); + assert!(SaveOptions::for_doc(&d).pretty); + assert!(is_pretty(&save_file(&d))); +} + +#[test] +fn many_objects_are_saved_compact() { + let d = paths_doc(PRETTY_MAX_OBJECTS + 1, 2); + assert!(!SaveOptions::for_doc(&d).pretty); +} + +/// Anchors per path that make a document of `paths` paths about `bytes` of compact JSON. +fn anchors_for(paths: usize, bytes: usize) -> usize { + let per_anchor = serde_json::to_vec(&paths_doc(1, 1000)).unwrap().len() / 1000; + bytes / per_anchor / paths +} + +#[test] +fn few_objects_with_many_anchors_are_saved_compact() { + let d = paths_doc(3, anchors_for(3, 2 * PRETTY_MAX_BYTES)); + assert!(!SaveOptions::for_doc(&d).pretty, "3 objects, but about {} bytes", 2 * PRETTY_MAX_BYTES); + let bytes = save_file(&d); + assert!(!is_pretty(&bytes)); + assert_eq!(load(&bytes).unwrap().layers[0].count(), d.layers[0].count()); +} + +#[test] +fn documents_under_the_size_limit_stay_pretty() { + let d = paths_doc(2, anchors_for(2, PRETTY_MAX_BYTES / 2)); + assert!(SaveOptions::for_doc(&d).pretty); +} + +#[test] +fn art_outside_the_layers_counts_too() { + // Generated art kept in a symbol: one instance-sized document, a symbol full of anchors. + let mut d = paths_doc(1, 4); + let mut art = paths_doc(1, anchors_for(1, 2 * PRETTY_MAX_BYTES)); + let path = Arc::clone(&art.layers[0]); + art.layers.clear(); + d.symbols.push(Symbol { name: "Leaves".into(), art: path }); + assert!(!SaveOptions::for_doc(&d).pretty); + let bytes = save_file(&d); + assert!(!is_pretty(&bytes)); + assert_eq!(load(&bytes).unwrap().symbols.len(), 1); +}