craft-vectorcraft-format
The envelope of VectorCraft's .vectorcraft file, for VectorCraft and for the apps that read its
files. The document is a type parameter: VectorCraft supplies its own document type, and a reader
that needs only the envelope uses serde::de::IgnoredAny. The crate takes and returns bytes; it
does not open files. No unsafe, no panics on malformed input, and it builds for
wasm32-unknown-unknown. The dependencies are serde, serde_json, thiserror and flate2
(its pure-Rust backend).
craft-vectorcraft-format = { git = "https://github.com/storytold/craft-libs", rev = "<commit>" }
The file
A .vectorcraft file is UTF-8 JSON, gzip-compressed when saved with Use Compression. Since
version 4, entries of INLINE_MAX (8 KiB) or more follow the JSON after BLOB_MAGIC
(\n\0VCBLOBS\0) and give "blob": [offset, length] instead of base64 data.
| Member | Type | Written |
|---|---|---|
format |
String |
"vectorcraft", or "drawcraft" in versions 1 and 2 |
version |
u32 |
1 to VERSION (4) |
generator |
Option<String> |
"VectorCraft <app version>" |
preview |
Option<Image> |
a PNG of the first artboard, at most PREVIEW_MAX (256) pixels on its longer side |
profiles |
BTreeMap<String, Image> |
ICC files by profile name, when there are any |
pdf |
Option<Image> |
a PDF of every artboard |
document |
D |
the document |
images |
BTreeMap<String, Image> |
the images the document uses, always written |
File<D> holds these members in this order. serde_json::to_vec and to_vec_pretty of a
File<D> give the bytes VectorCraft writes before the data after the JSON. File::read(json)
reads one, and Contents::read(json) reads format, version, document, images and
profiles and skips generator, preview and pdf as the JSON is read, so their base64 is not
buffered or kept. Both read the files VectorCraft opens:
Contents::read_with_pdf_preference(json) returns (contents, pdf_compatible) in that same
envelope pass. It reads an optional boolean pdfCompatible, falling back to a non-null pdf
member when the preference is absent or null. It neither retains nor decodes the PDF payload.
Repeated PDF members use their last value, and the array form returns false. A non-boolean
preference is an error in this opt-in method; Contents::read keeps ignoring that field.
- They return
ReadError::Envelope(Error::Format)for another format name andReadError::Envelope(Error::TooNew)for a version newer thanVERSION, after the JSON reads;ReadError::Jsonotherwise. - A repeated
generator,previeworpdfis accepted, andFilekeeps the last value, asserde_jsonreads a repeated key into aValue. Any other repeated member is refused. - A top-level JSON array holds
format,version,document,imagesandprofiles, in that order.
Head reads only preview and pdf. Image has mime, data (base64), blob and proxy
(a linked image's low-resolution preview). File, Contents, Head, Image, Part and
ReadError are #[non_exhaustive]: other crates build a File with File::new and an Image
with Image::inline, Tail::image or Image::default().
Use
use craft_vectorcraft_format::{Entries, File, split_tail};
// Writing
let mut file = File::new(document);
let mut tail = file.tail();
file.generator = Some(format!("VectorCraft {version}"));
file.pdf = Some(tail.image("application/pdf", &pdf, false));
let mut out = serde_json::to_vec(&file)?;
tail.write_to(&mut out)?;
// Reading (decompress first when `is_compressed(&bytes)`)
let (json, after) = split_tail(&bytes);
let file = File::<serde::de::IgnoredAny>::read(json)?;
let mut entries = Entries::new(after);
let pdf = file.pdf.map(|p| entries.bytes(&p)).transpose()?.flatten();
sniff(head)recognizes a file, plain or gzip, from its first 256 bytes after decompression: they start with{and the first"format"member is"vectorcraft"or"drawcraft", with JSON escapes in the key and value decoded.is_compressed(bytes)checks for the gzip magic bytes.split_tail(bytes)andjson_part(bytes)find the JSON and the data after it in an uncompressed or decompressed file.marker_at(bytes)gives the offset ofBLOB_MAGICwhen it follows the JSON as VectorCraft writes it, and works on the first bytes of a file that is being decompressed.File::checkandcheckreturnError::Formatfor another format name andError::TooNewfor a version newer thanVERSION.- A
generator,previeworpdfof the wrong shape reads asNoneinFileandHead.FileandHeadhold each of these values in memory while checking its shape;Contentsskips them. Image::lengives the size an entry states without decoding it;Image::bytes(after)decodesdataor copies theblobrange, and returnsNonefor an entry of more thanMAX_ENTRY(1 GiB, VectorCraft's limit on one entry of a compressed file).Image::bytes_at_mosttakes another limit.entry(after, [offset, length])returns the range.Entries::new(after)reads a file's entries:Entries::bytesandEntries::blobreturn each entry's bytes as anArc<Vec<u8>>, give entries that name the same[offset, length]one shared buffer, and returnError::TooBigonce theblobranges copied add up to more thanafterholds, as when many different ranges overlap.Entrieshas no limit on one entry.base64::decodedoes not check the unused bits before the padding:Zh==decodes likeZg==, as in VectorCraft's own decoder.File::tail(), orTail::new(version), places each entry withTail::image: after the JSON for version 4 and entries of at leastINLINE_MAXbytes, inline otherwise.Tail::write_towrites the marker and the entries.base64::encode,decodeanddecoded_lenhandle thedatamembers.Image::len_with_checkandbytes_at_most_with_checkaccept a fallible checkpoint called during bounded scans, base64 decoding and blob copying. The checkpoint error is returned unchanged;Ok(None)keepsbytes_at_most's meaning.base64::decoded_len_with_checkanddecode_with_checkexpose the same checkpoints for standalone base64 data.
Size limits on the JSON and on decompression stay with each reader.
Testing
cargo test -p craft-vectorcraft-format runs tests/envelope.rs, which compares files written
through File and Tail with JSON written out by hand in VectorCraft's layout (compact and
pretty, version 4 with data after the JSON, version 2 under the former name), reads them back,
and checks split_tail, marker_at, sniff on plain, gzip and truncated gzip input, the limits
Entries and Image::bytes put on the bytes copied, members of the wrong shape or repeated, the
array form, the format and version checks of File::read and Contents::read, and the base64
vectors of RFC 4648. The doctests check that the public structs and ReadError cannot be built or
matched exhaustively from another crate.
.github/workflows/vectorcraft-format.yml runs fmt, clippy and the tests on Linux and Windows,
and a wasm32-unknown-unknown check.