mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Native save: documents whose JSON is large are written compact
SaveOptions::for_doc chose pretty-printed JSON by the object count in the layers alone. A document with few layer objects but many anchors, or with its art in symbols, patterns or graph designs, was therefore written pretty, about ten times the compact size: every anchor's "p", "in" and "out" spread over several indented lines. A document is now pretty only when it has at most PRETTY_MAX_OBJECTS objects and its compact JSON is at most PRETTY_MAX_BYTES (1 MiB). The size is measured by serializing into a writer that stops at the limit, so a large document costs at most 1 MiB of extra serializing. Small documents stay readable and diffable. Fixes #1484
This commit is contained in:
1 parent
c391e1dbf2
commit
37954523ae
2 files changed
+94
-5
No files matched your search
@@ -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<usize> {
|
||||
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<u8> {
|
||||
@@ -260,6 +279,11 @@ pub fn save(doc: &Document, pretty: bool) -> Vec<u8> {
|
||||
/// 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<u8> {
|
||||
save_with(doc, &SaveOptions::for_doc(doc)).unwrap_or_default()
|
||||
|
||||
@@ -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<Point> = (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);
|
||||
}
|
||||
Reference in new issue
Block a user