Merge pull request #1492 from switch87/fix/1484-compact-anchor-heavy-saves
Native save: documents whose JSON is large are written compact
This commit is contained in:
commit
51c18f603a
|
|
@ -89,3 +89,7 @@ sync_github_name = false
|
||||||
|
|
||||||
[people.Nael-Nathanael]
|
[people.Nael-Nathanael]
|
||||||
sync_github_name = true
|
sync_github_name = true
|
||||||
|
|
||||||
|
[people.switch87]
|
||||||
|
real_name = "Gert Pellin"
|
||||||
|
sync_github_name = false
|
||||||
|
|
|
||||||
|
|
@ -243,13 +243,32 @@ impl SaveOptions {
|
||||||
Ok(())
|
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 {
|
pub fn for_doc(doc: &Document) -> Self {
|
||||||
let objects: usize = doc.layers.iter().map(|l| l.count()).sum();
|
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
|
/// Serialize a document (pretty = human-diffable) in the current version. Editing-mode working
|
||||||
/// copies are left out ([`Document::without_edit_modes`]).
|
/// copies are left out ([`Document::without_edit_modes`]).
|
||||||
pub fn save(doc: &Document, pretty: bool) -> Vec<u8> {
|
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.
|
/// compact, where indentation would multiply the file size and slow opening down.
|
||||||
pub const PRETTY_MAX_OBJECTS: usize = 2000;
|
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.
|
/// Serialize for saving to disk: pretty when small, compact when large.
|
||||||
pub fn save_file(doc: &Document) -> Vec<u8> {
|
pub fn save_file(doc: &Document) -> Vec<u8> {
|
||||||
save_with(doc, &SaveOptions::for_doc(doc)).unwrap_or_default()
|
save_with(doc, &SaveOptions::for_doc(doc)).unwrap_or_default()
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use vectorcraft_doc::links::LinkInfo;
|
use vectorcraft_doc::links::LinkInfo;
|
||||||
use vectorcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind};
|
use vectorcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind, Symbol};
|
||||||
use vectorcraft_format::{ICC_MIME, SaveOptions, load, load_file, pdf_content, save_with};
|
use vectorcraft_format::{ICC_MIME, PRETTY_MAX_BYTES, PRETTY_MAX_OBJECTS, SaveOptions, load, load_file, pdf_content, save_file, save_with};
|
||||||
use vectorcraft_geom::Affine;
|
use vectorcraft_geom::path::{PathData, SubPath};
|
||||||
|
use vectorcraft_geom::{Affine, Point};
|
||||||
|
use vectorcraft_testkit::fixtures::DocBuilder;
|
||||||
|
|
||||||
fn json_of(bytes: &[u8]) -> Value {
|
fn json_of(bytes: &[u8]) -> Value {
|
||||||
serde_json::from_slice(bytes).unwrap()
|
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);
|
let damaged = String::from_utf8(plain).unwrap().replacen("AAECA/o=", "not base64!", 1);
|
||||||
assert!(load_file(damaged.as_bytes()).unwrap().profiles.is_empty());
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue