mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 13:26:54 -06:00
M6: Sketch import: shared styles and colour variables
Shared layer styles become a paint style for their fills, a paint style for their border colours (linked to strokes) and an effect style; shared text styles become text styles (colour stays on the layers); local and library copies alike, unused ones listed too. Layers link to them, keeping their own values where they differ. Color Variables (swatches, also library ones) become colour variables in a Color Variables collection, with fills, borders and text colours bound to them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4437d49a72
commit
4be7017ed1
@@ -16,6 +16,9 @@ mod fixture;
|
||||
mod path;
|
||||
#[cfg(test)]
|
||||
mod path_tests;
|
||||
mod shared;
|
||||
#[cfg(test)]
|
||||
mod shared_tests;
|
||||
mod style;
|
||||
#[cfg(test)]
|
||||
mod style_tests;
|
||||
|
||||
@@ -139,9 +139,10 @@ pub(crate) fn shape_group(cx: &mut Cx, l: &Value, parent: Affine, depth: usize,
|
||||
}
|
||||
let visible = group.get("visible").and_then(Value::as_bool).unwrap_or(true) && m.get("visible").and_then(Value::as_bool).unwrap_or(true);
|
||||
m.insert("visible".into(), json!(visible));
|
||||
if let Some(st) = l.get("style") {
|
||||
if l.get("style").is_some() {
|
||||
super::style::clear(m);
|
||||
super::style::apply(cx, st, cw, ch, m);
|
||||
m.remove("styles");
|
||||
super::tree::styles(cx, l, cw, ch, m);
|
||||
}
|
||||
super::tree::mask(l, m);
|
||||
}
|
||||
@@ -168,9 +169,7 @@ pub(crate) fn shape_group(cx: &mut Cx, l: &Value, parent: Affine, depth: usize,
|
||||
node.insert("booleanOperation".into(), json!(op));
|
||||
node.insert("children".into(), Value::Array(chain));
|
||||
super::tree::mask(l, &mut node);
|
||||
if let Some(st) = l.get("style") {
|
||||
super::style::apply(cx, st, w, h, &mut node);
|
||||
}
|
||||
super::tree::styles(cx, l, w, h, &mut node);
|
||||
Some(Value::Object(node))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
//! Sketch shared styles and swatches → REST style definitions and colour variables.
|
||||
//!
|
||||
//! A Sketch layer style holds fills, borders and effects together; FigCraft styles hold one kind
|
||||
//! each, so a layer style becomes up to three styles named after it: its fills (a paint style),
|
||||
//! its border colours (a paint style for strokes; widths stay on the layers) and its effects. A
|
||||
//! text style becomes a text style; its colour stays on the layers, as FigCraft text styles carry
|
||||
//! none. Layers link to them by `style.sharedObjectID`. Swatches (Color Variables, Sketch 69+)
|
||||
//! become colour variables in one collection; colours name theirs by `swatchID`.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::{Map as JsonMap, Value, json};
|
||||
|
||||
use super::tree::{Cx, color, str_field};
|
||||
|
||||
/// Most shared styles or swatches read from one file.
|
||||
const MAX_SHARED: usize = 20_000;
|
||||
/// The collection swatches become variables in.
|
||||
const COLLECTION: &str = "sketch-color-variables";
|
||||
const MODE: &str = "default";
|
||||
|
||||
/// A file's shared styles and swatches.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Shared {
|
||||
/// `(id, name, style)` of layer styles, then text styles.
|
||||
layer: Vec<(String, String, Value)>,
|
||||
text: Vec<(String, String, Value)>,
|
||||
swatches: BTreeMap<String, (String, Value)>,
|
||||
}
|
||||
|
||||
impl Shared {
|
||||
pub(crate) fn read(document: &Value) -> Shared {
|
||||
let mut s = Shared::default();
|
||||
let objects =
|
||||
|key: &str| -> Vec<Value> { document.get(key).and_then(|c| c.get("objects")).and_then(Value::as_array).cloned().unwrap_or_default() };
|
||||
let foreign = |key: &str, inner: &str| -> Vec<Value> {
|
||||
document.get(key).and_then(Value::as_array).map(Vec::as_slice).unwrap_or_default().iter().filter_map(|f| f.get(inner).cloned()).collect()
|
||||
};
|
||||
let entry = |o: &Value| {
|
||||
Some((
|
||||
str_field(o, "do_objectID")?.to_string(),
|
||||
str_field(o, "name").unwrap_or("Style").chars().take(200).collect(),
|
||||
o.get("value")?.clone(),
|
||||
))
|
||||
};
|
||||
for o in objects("layerStyles").iter().chain(&foreign("foreignLayerStyles", "localSharedStyle")).take(MAX_SHARED) {
|
||||
s.layer.extend(entry(o));
|
||||
}
|
||||
for o in objects("layerTextStyles").iter().chain(&foreign("foreignTextStyles", "localSharedStyle")).take(MAX_SHARED) {
|
||||
s.text.extend(entry(o));
|
||||
}
|
||||
let foreign_swatches = [foreign("foreignSwatches", "localSwatch"), foreign("foreignSwatches", "localSharedSwatch")].concat();
|
||||
for o in objects("sharedSwatches").iter().chain(&foreign_swatches).take(MAX_SHARED) {
|
||||
if let Some((id, name, value)) = entry(o)
|
||||
&& let Some(c) = color(&value)
|
||||
{
|
||||
s.swatches.entry(id).or_insert((name, c));
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
pub(crate) fn has_swatch(&self, id: &str) -> bool {
|
||||
self.swatches.contains_key(id)
|
||||
}
|
||||
|
||||
/// The REST `styleDefinitions` of every shared style.
|
||||
pub(crate) fn definitions(cx: &mut Cx) -> Vec<Value> {
|
||||
let layer = cx.shared.layer.clone();
|
||||
let text = cx.shared.text.clone();
|
||||
let mut out = Vec::new();
|
||||
for (id, name, style) in &layer {
|
||||
let fills = super::style::fills(cx, style, 100.0, 100.0);
|
||||
if !fills.is_empty() {
|
||||
out.push(json!({"id": format!("{id}:fill"), "name": name, "styleType": "FILL", "node": {"fills": fills}}));
|
||||
}
|
||||
let strokes = super::style::border_paints(cx, style, 100.0, 100.0);
|
||||
if !strokes.is_empty() {
|
||||
out.push(json!({"id": format!("{id}:stroke"), "name": format!("{name} border"), "styleType": "FILL", "node": {"fills": strokes}}));
|
||||
}
|
||||
let effects = super::style::effects(cx, style);
|
||||
if !effects.is_empty() {
|
||||
out.push(json!({"id": format!("{id}:effect"), "name": name, "styleType": "EFFECT", "node": {"effects": effects}}));
|
||||
}
|
||||
}
|
||||
for (id, name, style) in &text {
|
||||
let Some(enc) = style.get("textStyle").and_then(|t| t.get("encodedAttributes")) else { continue };
|
||||
let mut ts = super::text::type_style(cx, enc);
|
||||
if let Some(m) = ts.as_object_mut() {
|
||||
m.remove("fills");
|
||||
}
|
||||
out.push(json!({"id": id, "name": name, "styleType": "TEXT", "node": {"type": "TEXT", "characters": "", "style": ts}}));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Link `node` (layer `l`) to the styles made from its shared style.
|
||||
pub(crate) fn link(&self, cx_defs: &[String], l: &Value, node: &mut JsonMap<String, Value>) {
|
||||
let Some(sid) = l.get("style").and_then(|s| str_field(s, "sharedObjectID")) else { return };
|
||||
let mut refs = JsonMap::new();
|
||||
let is_text = str_field(l, "_class") == Some("text");
|
||||
if is_text && self.text.iter().any(|(id, _, _)| id == sid) {
|
||||
refs.insert("text".into(), json!(sid));
|
||||
} else if self.layer.iter().any(|(id, _, _)| id == sid) {
|
||||
for slot in ["fill", "stroke", "effect"] {
|
||||
let id = format!("{sid}:{slot}");
|
||||
if cx_defs.contains(&id) {
|
||||
refs.insert(slot.into(), json!(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !refs.is_empty() {
|
||||
node.insert("styles".into(), Value::Object(refs));
|
||||
}
|
||||
}
|
||||
|
||||
/// The REST `variableCollections` and `variables` for the swatches (none without swatches).
|
||||
pub(crate) fn variables(&self) -> Option<(Value, Value)> {
|
||||
if self.swatches.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let ids: Vec<&String> = self.swatches.keys().collect();
|
||||
let collections = json!({COLLECTION: {"id": COLLECTION, "name": "Color Variables", "modes": [{"modeId": MODE, "name": "Default"}],
|
||||
"defaultModeId": MODE, "variableIds": ids}});
|
||||
let vars: JsonMap<String, Value> = self
|
||||
.swatches
|
||||
.iter()
|
||||
.map(|(id, (name, c))| {
|
||||
(id.clone(), json!({"id": id, "name": name, "resolvedType": "COLOR", "variableCollectionId": COLLECTION, "valuesByMode": {MODE: c}}))
|
||||
})
|
||||
.collect();
|
||||
Some((collections, Value::Object(vars)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
//! Sketch shared styles → FigCraft styles; Color Variables (swatches) → colour variables.
|
||||
|
||||
use figcraft_doc::color::PaintKind;
|
||||
use figcraft_doc::{Document, Node, StyleKind};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::fixture::*;
|
||||
use super::import;
|
||||
|
||||
const BLACK: [f64; 4] = [0.0, 0.0, 0.0, 1.0];
|
||||
|
||||
fn open(doc_json: Value, layers: Vec<Value>) -> (Document, Vec<String>) {
|
||||
let z = sketch_zip("83.2", 139, doc_json, &[page("P", "Page", vec![artboard("A", "Board", 0.0, 0.0, 400.0, 400.0, layers)])], &[]);
|
||||
import(&z, "f").unwrap()
|
||||
}
|
||||
|
||||
fn node<'d>(doc: &'d Document, name: &str) -> &'d Node {
|
||||
doc.nodes().find(|n| n.name == name).unwrap_or_else(|| panic!("no {name}"))
|
||||
}
|
||||
|
||||
fn style<'d>(doc: &'d Document, id: &str) -> &'d figcraft_doc::Style {
|
||||
doc.styles.iter().find(|s| s.id == id).unwrap_or_else(|| panic!("no style {id}: {:?}", doc.styles.iter().map(|s| &s.id).collect::<Vec<_>>()))
|
||||
}
|
||||
|
||||
fn card_style() -> Value {
|
||||
json!({"_class": "style", "fills": [solid_fill(0.0, 0.0, 1.0, 1.0)], "borders": [],
|
||||
"shadows": [{"_class": "shadow", "isEnabled": true, "blurRadius": 4, "offsetX": 0, "offsetY": 2, "spread": 0, "color": color(0.0, 0.0, 0.0, 0.3)}],
|
||||
"innerShadows": []})
|
||||
}
|
||||
|
||||
fn with_shared(d: &mut Value, layer_styles: Vec<Value>, text_styles: Vec<Value>) {
|
||||
d["layerStyles"]["objects"] = Value::Array(layer_styles);
|
||||
d["layerTextStyles"]["objects"] = Value::Array(text_styles);
|
||||
}
|
||||
|
||||
fn shared(id: &str, name: &str, value: Value) -> Value {
|
||||
json!({"_class": "sharedStyle", "do_objectID": id, "name": name, "value": value})
|
||||
}
|
||||
|
||||
fn linked(mut l: Value, style: Value, shared_id: &str) -> Value {
|
||||
l["style"] = style;
|
||||
l["style"]["sharedObjectID"] = json!(shared_id);
|
||||
l
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_style_splits() {
|
||||
let mut d = document(&["P"]);
|
||||
with_shared(&mut d, vec![shared("CARD", "Card", card_style())], vec![]);
|
||||
let (doc, _) = open(d, vec![linked(rect("R", 0.0, 0.0, 50.0, 50.0), card_style(), "CARD")]);
|
||||
let n = node(&doc, "R");
|
||||
let fill = n.styles.fill.clone().expect("fill style linked");
|
||||
let effect = n.styles.effect.clone().expect("effect style linked");
|
||||
assert_eq!(style(&doc, &fill).name, "Card");
|
||||
assert!(matches!(style(&doc, &fill).kind, StyleKind::Paint { .. }));
|
||||
assert!(matches!(style(&doc, &effect).kind, StyleKind::Effect { .. }));
|
||||
assert_eq!(style(&doc, &effect).name, "Card");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_style_borders_become_a_stroke_style() {
|
||||
let mut st = card_style();
|
||||
st["borders"] =
|
||||
json!([{"_class": "border", "isEnabled": true, "fillType": 0, "color": color(1.0, 0.0, 0.0, 1.0), "position": 1, "thickness": 2}]);
|
||||
let mut d = document(&["P"]);
|
||||
with_shared(&mut d, vec![shared("CARD", "Card", st.clone())], vec![]);
|
||||
let (doc, _) = open(d, vec![linked(rect("R", 0.0, 0.0, 50.0, 50.0), st, "CARD")]);
|
||||
let stroke = node(&doc, "R").styles.stroke.clone().expect("stroke style linked");
|
||||
assert!(style(&doc, &stroke).name.starts_with("Card"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unused_shared_styles_are_listed() {
|
||||
let mut d = document(&["P"]);
|
||||
with_shared(&mut d, vec![shared("CARD", "Card", card_style())], vec![]);
|
||||
let (doc, _) = open(d, vec![]);
|
||||
assert!(doc.styles.iter().any(|s| s.name == "Card"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_style_links() {
|
||||
let enc = json!({"MSAttributedStringFontAttribute": {"_class": "fontDescriptor", "attributes": {"name": "Helvetica-Bold", "size": 32}},
|
||||
"MSAttributedStringColorAttribute": color(1.0, 0.0, 0.0, 1.0),
|
||||
"paragraphStyle": {"_class": "paragraphStyle", "alignment": 0, "maximumLineHeight": 40, "minimumLineHeight": 40}});
|
||||
let st = json!({"_class": "style", "textStyle": {"_class": "textStyle", "encodedAttributes": enc, "verticalAlignment": 0}});
|
||||
let mut d = document(&["P"]);
|
||||
with_shared(&mut d, vec![], vec![shared("H1", "Heading/H1", st.clone())]);
|
||||
let t = linked(text("Title", 0.0, 0.0, 200.0, 40.0, "Hello", &[(0, 5, "Helvetica-Bold", 32.0, [1.0, 0.0, 0.0, 1.0])]), st, "H1");
|
||||
let (doc, _) = open(d, vec![t]);
|
||||
let n = node(&doc, "Title");
|
||||
let sid = n.styles.text.clone().expect("text style linked");
|
||||
let s = style(&doc, &sid);
|
||||
assert_eq!(s.name, "Heading/H1");
|
||||
let StyleKind::Text { style: ts } = &s.kind else { panic!("not a text style") };
|
||||
assert_eq!((ts.font_family.as_str(), ts.font_style.as_str(), ts.font_size), ("Helvetica", "Bold", 32.0));
|
||||
assert!(n.fills.iter().any(|p| matches!(p.kind, PaintKind::Solid { color } if color.r == 1.0 && color.g == 0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locally_changed_style_keeps_link() {
|
||||
let mut local = card_style();
|
||||
local["fills"] = json!([solid_fill(0.0, 1.0, 0.0, 1.0)]);
|
||||
let mut d = document(&["P"]);
|
||||
with_shared(&mut d, vec![shared("CARD", "Card", card_style())], vec![]);
|
||||
let (doc, _) = open(d, vec![linked(rect("R", 0.0, 0.0, 50.0, 50.0), local, "CARD")]);
|
||||
let n = node(&doc, "R");
|
||||
assert!(n.styles.fill.is_some());
|
||||
assert!(matches!(n.fills[0].kind, PaintKind::Solid { color } if color.g == 1.0 && color.b == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreign_styles_too() {
|
||||
let mut d = document(&["P"]);
|
||||
d["foreignLayerStyles"] = json!([{"_class": "MSImmutableForeignLayerStyle", "sourceLibraryName": "Kit", "localSharedStyle": shared("FCARD", "Kit card", card_style())}]);
|
||||
let (doc, _) = open(d, vec![linked(rect("R", 0.0, 0.0, 50.0, 50.0), card_style(), "FCARD")]);
|
||||
assert_eq!(style(&doc, node(&doc, "R").styles.fill.as_deref().unwrap()).name, "Kit card");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swatches_are_color_variables() {
|
||||
let mut d = document(&["P"]);
|
||||
d["sharedSwatches"] = json!({"_class": "swatchContainer", "objects": [{"_class": "swatch", "do_objectID": "SW1", "name": "Brand/Blue", "value": color(0.06, 0.38, 1.0, 1.0)}]});
|
||||
d["foreignSwatches"] = json!([{"_class": "MSImmutableForeignSwatch", "sourceLibraryName": "Kit", "localSwatch": {"_class": "swatch", "do_objectID": "SW2", "name": "Kit/Red", "value": color(1.0, 0.0, 0.0, 1.0)}}]);
|
||||
let mut fill = solid_fill(0.06, 0.38, 1.0, 1.0);
|
||||
fill["color"]["swatchID"] = json!("SW1");
|
||||
let mut fill2 = solid_fill(1.0, 0.0, 0.0, 1.0);
|
||||
fill2["color"]["swatchID"] = json!("SW2");
|
||||
let (doc, _) = open(
|
||||
d,
|
||||
vec![styled(rect("R", 0.0, 0.0, 50.0, 50.0), "fills", json!([fill])), styled(rect("S", 0.0, 0.0, 50.0, 50.0), "fills", json!([fill2]))],
|
||||
);
|
||||
let coll = doc.variables.collections.iter().find(|c| c.name == "Color Variables").expect("collection");
|
||||
let var = doc.variables.variables.iter().find(|v| v.name == "Brand/Blue").expect("variable");
|
||||
assert_eq!(var.collection, coll.id);
|
||||
assert!(doc.variables.variables.iter().any(|v| v.name == "Kit/Red"));
|
||||
assert_eq!(node(&doc, "R").bound.get("fill"), Some(&var.id));
|
||||
assert!(node(&doc, "S").bound.contains_key("fill"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_shared_ids_are_ignored() {
|
||||
let (doc, _) = open(document(&["P"]), vec![linked(rect("R", 0.0, 0.0, 50.0, 50.0), card_style(), "NOPE")]);
|
||||
let n = node(&doc, "R");
|
||||
assert!(n.styles.fill.is_none());
|
||||
let mut f = solid_fill(0.0, 0.0, 0.0, 1.0);
|
||||
f["color"]["swatchID"] = json!("NOPE");
|
||||
let (doc, _) =
|
||||
open(document(&["P"]), vec![styled(text("T", 0.0, 0.0, 10.0, 10.0, "x", &[(0, 1, "Helvetica", 9.0, BLACK)]), "fills", json!([f]))]);
|
||||
assert!(node(&doc, "T").bound.is_empty());
|
||||
}
|
||||
@@ -17,7 +17,7 @@ const MAX_LIST: usize = 64;
|
||||
/// Write the paints, strokes, effects, opacity and blend mode of `style` into `node`, a layer of
|
||||
/// `w`×`h`. Lists the style doesn't have are left as they are.
|
||||
pub(crate) fn apply(cx: &mut Cx, style: &Value, w: f64, h: f64, node: &mut JsonMap<String, Value>) {
|
||||
let fills: Vec<Value> = list(style, "fills").iter().filter_map(|f| paint(cx, f, w, h)).collect();
|
||||
let fills = fills(cx, style, w, h);
|
||||
if !fills.is_empty() {
|
||||
node.insert("fills".into(), Value::Array(fills));
|
||||
}
|
||||
@@ -36,6 +36,16 @@ pub(crate) fn apply(cx: &mut Cx, style: &Value, w: f64, h: f64, node: &mut JsonM
|
||||
}
|
||||
}
|
||||
|
||||
/// A style's fills as REST paints.
|
||||
pub(crate) fn fills(cx: &mut Cx, style: &Value, w: f64, h: f64) -> Vec<Value> {
|
||||
list(style, "fills").iter().filter_map(|f| paint(cx, f, w, h)).collect()
|
||||
}
|
||||
|
||||
/// A style's borders as REST paints.
|
||||
pub(crate) fn border_paints(cx: &mut Cx, style: &Value, w: f64, h: f64) -> Vec<Value> {
|
||||
list(style, "borders").iter().filter_map(|b| paint(cx, b, w, h)).collect()
|
||||
}
|
||||
|
||||
/// Remove what [`apply`] writes, so another style can take its place.
|
||||
pub(crate) fn clear(node: &mut JsonMap<String, Value>) {
|
||||
for key in [
|
||||
@@ -91,6 +101,9 @@ fn paint(cx: &mut Cx, f: &Value, w: f64, h: f64) -> Option<Value> {
|
||||
}
|
||||
};
|
||||
p["visible"] = json!(f.get("isEnabled").and_then(Value::as_bool).unwrap_or(true));
|
||||
if let Some(var) = swatch(cx, f.get("color")) {
|
||||
p["boundVariables"] = var;
|
||||
}
|
||||
if let Some(ctx) = f.get("contextSettings") {
|
||||
if let Some(o) = num(ctx, "opacity") {
|
||||
p["opacity"] = json!(o.clamp(0.0, 1.0));
|
||||
@@ -131,7 +144,7 @@ fn pt((x, y): (f64, f64)) -> Value {
|
||||
/// Borders → stroke paints, and the first enabled border's thickness and position.
|
||||
fn borders(cx: &mut Cx, style: &Value, w: f64, h: f64, node: &mut JsonMap<String, Value>) {
|
||||
let list = list(style, "borders");
|
||||
let strokes: Vec<Value> = list.iter().filter_map(|b| paint(cx, b, w, h)).collect();
|
||||
let strokes = border_paints(cx, style, w, h);
|
||||
if strokes.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -201,7 +214,7 @@ fn borders(cx: &mut Cx, style: &Value, w: f64, h: f64, node: &mut JsonMap<String
|
||||
}
|
||||
|
||||
/// Shadows, inner shadows and the blur as REST effects.
|
||||
fn effects(cx: &mut Cx, style: &Value) -> Vec<Value> {
|
||||
pub(crate) fn effects(cx: &mut Cx, style: &Value) -> Vec<Value> {
|
||||
let mut out = Vec::new();
|
||||
for (key, ty) in [("shadows", "DROP_SHADOW"), ("innerShadows", "INNER_SHADOW")] {
|
||||
for s in list(style, key) {
|
||||
@@ -230,6 +243,12 @@ fn effects(cx: &mut Cx, style: &Value) -> Vec<Value> {
|
||||
out
|
||||
}
|
||||
|
||||
/// The variable binding of a colour that names a swatch the file has (`{color: alias}`).
|
||||
pub(crate) fn swatch(cx: &Cx, c: Option<&Value>) -> Option<Value> {
|
||||
let id = c?.get("swatchID").and_then(Value::as_str).filter(|id| cx.shared.has_swatch(id))?;
|
||||
Some(json!({"color": {"type": "VARIABLE_ALIAS", "id": id}}))
|
||||
}
|
||||
|
||||
/// Sketch's blend modes (0 normal … 17 plus lighter) by REST name.
|
||||
fn blend(mode: i64) -> Option<&'static str> {
|
||||
const MODES: [&str; 18] = [
|
||||
|
||||
@@ -209,8 +209,11 @@ fn apply(layers: &mut [Value], ov: &BTreeMap<String, Value>, document: &Value, d
|
||||
_ => {}
|
||||
}
|
||||
for (prop, list) in [("layerStyle", "layerStyles"), ("textStyle", "layerTextStyles")] {
|
||||
if let Some(style) = get(prop).and_then(Value::as_str).and_then(|sid| shared_style(document, list, sid)) {
|
||||
let sid = get(prop).and_then(Value::as_str);
|
||||
if let Some((sid, style)) = sid.and_then(|sid| Some((sid, shared_style(document, list, sid)?))) {
|
||||
l["style"] = style.clone();
|
||||
// The layer now uses that shared style: it links to it.
|
||||
l["style"]["sharedObjectID"] = json!(sid);
|
||||
if prop == "textStyle"
|
||||
&& let Some(enc) = style.get("textStyle").and_then(|t| t.get("encodedAttributes"))
|
||||
&& let Some(runs) = l.get_mut("attributedString").and_then(|a| a.get_mut("attributes")).and_then(Value::as_array_mut)
|
||||
|
||||
@@ -94,7 +94,7 @@ pub(crate) fn node(cx: &mut Cx, l: &Value, node: &mut JsonMap<String, Value>) {
|
||||
}
|
||||
|
||||
/// A run's attributes as a REST type style (with its colour as `fills`).
|
||||
fn type_style(cx: &mut Cx, a: &Value) -> Value {
|
||||
pub(crate) fn type_style(cx: &mut Cx, a: &Value) -> Value {
|
||||
let font = a.get("MSAttributedStringFontAttribute").and_then(|f| f.get("attributes"));
|
||||
let ps = font.and_then(|f| f.get("name")).and_then(Value::as_str).unwrap_or("");
|
||||
let size = font.and_then(|f| f.get("size")).and_then(Value::as_f64).filter(|s| s.is_finite() && *s > 0.0).unwrap_or(12.0);
|
||||
@@ -126,7 +126,11 @@ fn type_style(cx: &mut Cx, a: &Value) -> Value {
|
||||
_ => "ORIGINAL",
|
||||
});
|
||||
let c = a.get("MSAttributedStringColorAttribute").and_then(color).unwrap_or_else(|| json!({"r": 0.0, "g": 0.0, "b": 0.0, "a": 1.0}));
|
||||
s["fills"] = json!([{"type": "SOLID", "color": c}]);
|
||||
let mut paint = json!({"type": "SOLID", "color": c});
|
||||
if let Some(var) = super::style::swatch(cx, a.get("MSAttributedStringColorAttribute")) {
|
||||
paint["boundVariables"] = var;
|
||||
}
|
||||
s["fills"] = json!([paint]);
|
||||
s
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ pub(crate) struct Cx<'a> {
|
||||
/// `document.json`: shared styles and library copies.
|
||||
pub document: &'a Value,
|
||||
pub symbols: super::symbols::Symbols,
|
||||
pub shared: super::shared::Shared,
|
||||
/// Ids of the styles made from the shared styles.
|
||||
pub style_defs: Vec<String>,
|
||||
/// The symbols being expanded, outermost first (a symbol inside itself stops there).
|
||||
pub chain: Vec<String>,
|
||||
pub warnings: Vec<String>,
|
||||
@@ -49,9 +52,12 @@ impl Cx<'_> {
|
||||
/// Map the pages of `archive` to a REST file JSON named `name`.
|
||||
pub(crate) fn to_rest(archive: &Archive, name: &str, max_nodes: usize) -> Map {
|
||||
let symbols = super::symbols::Symbols::read(&archive.document, &archive.pages);
|
||||
let shared = super::shared::Shared::read(&archive.document);
|
||||
let mut cx = Cx {
|
||||
document: &archive.document,
|
||||
symbols,
|
||||
shared,
|
||||
style_defs: Vec::new(),
|
||||
chain: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
networks: Vec::new(),
|
||||
@@ -59,6 +65,8 @@ pub(crate) fn to_rest(archive: &Archive, name: &str, max_nodes: usize) -> Map {
|
||||
nodes: 0,
|
||||
max_nodes,
|
||||
};
|
||||
let defs = super::shared::Shared::definitions(&mut cx);
|
||||
cx.style_defs = defs.iter().filter_map(|d| d.get("id").and_then(Value::as_str).map(str::to_string)).collect();
|
||||
let mut canvases = Vec::new();
|
||||
for (id, page) in &archive.pages {
|
||||
let mut node = JsonMap::new();
|
||||
@@ -75,7 +83,11 @@ pub(crate) fn to_rest(archive: &Archive, name: &str, max_nodes: usize) -> Map {
|
||||
let libraries = cx.symbols.libraries.join(", ");
|
||||
cx.warn(format!("symbols from the libraries {libraries} became local components on the page Library symbols"));
|
||||
}
|
||||
let json = json!({"name": name, "document": {"type": "DOCUMENT", "id": "0:0", "children": canvases}});
|
||||
let mut json = json!({"name": name, "document": {"type": "DOCUMENT", "id": "0:0", "children": canvases}, "styleDefinitions": defs});
|
||||
if let Some((collections, variables)) = cx.shared.variables() {
|
||||
json["variableCollections"] = collections;
|
||||
json["variables"] = variables;
|
||||
}
|
||||
Map { json, warnings: cx.warnings, networks: cx.networks, too_old: cx.too_old }
|
||||
}
|
||||
|
||||
@@ -147,9 +159,7 @@ pub(crate) fn layer(cx: &mut Cx, l: &Value, parent: Affine, depth: usize, root:
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
if let Some(st) = l.get("style") {
|
||||
super::style::apply(cx, st, w, h, &mut node);
|
||||
}
|
||||
styles(cx, l, w, h, &mut node);
|
||||
if matches!(ty, "FRAME" | "GROUP") {
|
||||
let kids = children(cx, l, world, (w, h), depth, false);
|
||||
node.insert("children".into(), Value::Array(kids));
|
||||
@@ -157,6 +167,14 @@ pub(crate) fn layer(cx: &mut Cx, l: &Value, parent: Affine, depth: usize, root:
|
||||
Some(Value::Object(node))
|
||||
}
|
||||
|
||||
/// Layer `l`'s style into `node`, and its links to the styles made from its shared style.
|
||||
pub(crate) fn styles(cx: &mut Cx, l: &Value, w: f64, h: f64, node: &mut JsonMap<String, Value>) {
|
||||
if let Some(st) = l.get("style") {
|
||||
super::style::apply(cx, st, w, h, node);
|
||||
}
|
||||
cx.shared.link(&cx.style_defs, l, node);
|
||||
}
|
||||
|
||||
/// A clipping mask's flag and kind (outline or alpha).
|
||||
pub(crate) fn mask(l: &Value, node: &mut JsonMap<String, Value>) {
|
||||
if bool_field(l, "hasClippingMask") == Some(true) {
|
||||
|
||||
Reference in New Issue
Block a user