* Add craft-figma-rest: a typed model of the Figma REST file JSON
The neutral model for design files: the .fig and .sketch readers map onto
it and an application's importer takes it. Typed records for the document
tree, paints, effects, text styles, layout grids, constraints, export
settings and the component and style metadata; every record keeps the
fields it has no name for, so a file read and written loses nothing.
The vectorNetwork extension is typed and documented with the others.
The layer tree is converted level by level in both directions, and the
depth and node limits are checked on the JSON before any typed value is
built.
* craft-figma-rest: Warnings and style_name for the readers that build this shape
Moved from FigCraft's REST importer and .fig reader (crates/format/src/rest.rs
Warnings, fig.rs style_name), which the .fig and Sketch mappings share.
* craft-sketch: map the layers onto the REST file JSON
FigCraft's Sketch mapping (crates/format/src/sketch/{tree,style,text,
symbols,path,shared,stack,proto}.rs at e8b53e33) moved onto
craft-figma-rest: rest::to_rest turns an Archive's pages and layers into
the REST API file JSON, with the vector networks of its paths handed over
beside it as craft_figma_rest::ext::VectorNetwork.
The star and polygon point generators it compares a path with are copied
from FigCraft's geometry; comments and two warnings no longer name the
app.
* craft-sketch: reach the nesting limit with nested groups, not 210 masters
The same limit and warning, in 1 second instead of 58 in a debug build.
* craft-sketch: a nesting limit that fits a 1 MiB stack, in small frames
The mapping recurses once per level of layer nesting, symbol expansion
included. Nested symbol instances reach its limit with shallow page JSON,
and a debug build overflowed a 2 MiB stack at about 140 levels: a crash
on hostile input.
The work on one layer, master or instance now happens in functions that
return before its children are mapped, which takes the stack per level
from about 4.5 KB to about 2 KB for an instance and its group in a debug
build. That maps about 140 levels of nested instances on 512 KiB, so the
limit is now 128 (was 200), leaving twice the room on the 1 MiB stack of
a WebAssembly build. The test at the limit runs on a 1 MiB thread.
* Cargo.lock: craft-sketch's craft-figma-rest and kurbo after the merge with main
craft-sketch
A clean-room reader for the .sketch container, for files saved by Sketch 48 and newer.
No unsafe.
This crate is the container layer. It opens the archive, applies the version cut, and hands out
document.json, meta.json and the pages as the JSON the file holds (serde_json::Value, every
object with its kind in _class) and the images as bytes. What the layers mean, and how they map
onto an application's document, stays with the caller.
Provenance
Written by FigCraft's contributors (the .sketch import is Jindřich Šaršon's work), from the
public description of the format only, and files built for each case, checked against Sketch's
own renders and real files (Sketch 50, 63 and 2026). No code of Sketch or of any other product was copied, and no GPL, LGPL
or AGPL code was consulted. Moved here from figcraft/crates/format/src/sketch.rs (the archive
half).
The format
- A
.sketchfile is a ZIP archive:document.json(the pages in order as_refs topages/<id>, shared styles, swatches, library symbols copied into the document),meta.json(appVersion, the formatversion),user.json, onepages/<id>.jsonper page (the layer tree),images/<name>and, in newer files,fonts/. - Before version 43 a Sketch file was a SQLite database; until 47 text was a binary property
list inside the JSON. Both are refused as too old (
Error::TooOld, with a message saying to save the file again in a newer Sketch). meta.jsonversions above 139 (Sketch 83, the newest checked against real files) open with a warning.
Reading
if craft_sketch::is_sketch(&bytes) {
let archive = craft_sketch::Archive::open(&bytes)?;
for (id, page) in &archive.pages { // in the document's order
let layers = page.get("layers");
}
let styles = archive.document.get("layerStyles");
let image = archive.image("images/0a1b….png"); // Option<Vec<u8>>
let fonts = craft_sketch::embedded_font_families(&archive.document);
for warning in &archive.warnings { /* pages skipped, a newer format than tested */ }
}
A document.json that can't be read (damaged, nested too deep) loses the shared styles, not the
pages: they are then taken in archive order, with a warning. A page that can't be read is
skipped with a warning; the others open.
Limits
Input is hostile. Nothing panics; every limit has a test.
| Limit | Value |
|---|---|
meta.json, document.json (MAX_JSON_MEMBER) |
512 MiB |
A page (Limits::max_page) |
512 MiB |
JSON read in all (MAX_JSON_TOTAL) |
2 GiB (512 MiB on 32-bit targets) |
Unpacked size per compressed byte (MAX_RATIO), past a 16 MiB floor (MIN_ALLOWANCE) |
100 |
Pages (MAX_PAGES) |
1000 |
An image (Limits::max_image) |
256 MiB |
Image bytes in all (Limits::max_image_total) |
1 GiB (256 MiB on 32-bit targets) |
| A page id or image name | 128 characters of letters, digits, - and _ (an image may have one extension) |
| JSON nesting | serde_json's 128 levels |
A member is refused before unpacking when its declared size is past its allowance, and again if it unpacks past it. A read that fails is charged in full, and a page listed twice is read once, so a page list can't repeat an unpacking within the budget.
ZIP
The archive is read with the zip crate directly, with the bounds above applied to each member.
A shared bounded ZIP reader is planned (craft-libs #126); this crate moves to it when it lands.
The layers as REST file JSON (rest)
rest::to_rest maps the pages and layers onto the REST API file JSON that
craft-figma-rest models: artboards and frames, groups, shape paths
and shape groups with their boolean operations, text with its attributed runs, bitmaps, symbol
masters (components) and instances (expanded from their master with the instance's overrides,
including those in nested instances and swaps), stacks (auto layout), shared styles, swatches
(colour variables), prototype links, export options and layout grids. Paths come as vector
networks beside the JSON (vectorNetworkRef); stars and polygons whose points are still the
generated ones carry pointCount and innerRadius. What isn't understood is skipped with a
warning.
let archive = craft_sketch::Archive::open(&bytes)?;
let mapped = craft_sketch::rest::to_rest(&archive, "Untitled", craft_sketch::rest::Options::default());
if mapped.too_old { /* text stored the pre-48 way */ }
Options::fonts finds the installed face a PostScript name belongs to; without it, families
are read from the names.
| Limit | Value |
|---|---|
Layers (Options::max_nodes) |
200 000 by default |
| Layer nesting, symbol expansion included | 128 levels |
| Work spent expanding symbols (one unit per JSON value copied) | 60 million (15 million on 32-bit targets) |
| Points of one path | 100 000 |
| Distinct warnings | 200 |
The walk recurses once per level of nesting, in small frames (each layer's own work is done in functions that return before its children are mapped). An unoptimised build maps about 140 levels of nested instances on a 512 KiB stack; the limit of 128 leaves twice the room on the 1 MiB stack of a WebAssembly build, and a test at the limit runs on a 1 MiB thread. FigCraft followed 200 levels.
The mapping was written in FigCraft (crates/format/src/sketch), mostly by Jindřich Šaršon, from
the public description of the format and files built for each case.
Not here yet
- A typed model of the Sketch document itself (
_classobjects): the mapping reads the JSON. - Writing.