M6: .fig export writes list spacing, hanging punctuation and lists, and vertical trim (#2305) (#2306)

* M6: .fig export writes list spacing, hanging punctuation and lists, and vertical trim (#2305)

The export wrote none of a text's list spacing, hanging punctuation, hanging lists or vertical trim, although the import reads all four. It now writes listSpacing, hangingPunctuation, hangingList and leadingTrim, each only when it differs from its default (as Figma saves them), and an instance override back to the default clears the field. The warning narrows to OpenType features, which the import does not read from a .fig.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 briefed; Sonnet 5.5 wrote at medium

* M6: test that an instance override back to the default clears it (#2305)

Adds a round-trip test for the unset entries: an instance whose text goes back to the defaults must not take the main's list spacing, hanging flags and vertical trim again. Also scopes the comment and the docs row to a design file: a text at the defaults carries none of the four there, and a text style holds none of them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 briefed; Sonnet 5.5 wrote at medium

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Trevor BrownandClaude Sonnet 5.5 authored and GitHub committed 2026-11-13 01:46:25 +01:00
1 parent a60cb1a6fa
commit a4e330774b
5 files changed
+109 -12

No files matched your search

+2 -1
View File
@@ -100,6 +100,7 @@ enum TextCase ORIGINAL=0 UPPER=1 LOWER=2 TITLE=3 SMALL_CAPS=4
enum TextDecoration NONE=0 UNDERLINE=1 STRIKETHROUGH=2
enum NumberUnits RAW=0 PIXELS=1 PERCENT=2
enum LineType PLAIN=0 ORDERED_LIST=1 UNORDERED_LIST=2
enum LeadingTrim NONE=0 CAP_HEIGHT=1
enum SourceDirectionality AUTO=0 LTR=1 RTL=2
enum DocumentColorProfile LEGACY=0 SRGB=1 DISPLAY_P3=2
enum EmojiImageSet APPLE=0 NOTO=1
@@ -189,7 +190,7 @@ message GridTrackSizingFunction GridTrackSizingType type=1, float value=2
message GridTrackSize GridTrackSizingFunction minSizing=1, GridTrackSizingFunction maxSizing=2
message GUIDGridTrackSizeMapEntry GUID id=1, GridTrackSize trackSize=2
message GUIDGridTrackSizeMap GUIDGridTrackSizeMapEntry[] entries=1
message NodeChange GUID guid=1, NodePhase phase=2, ParentIndex parentIndex=3, NodeType type=4, string name=5, bool visible=6, bool locked=7, float opacity=8, BlendMode blendMode=9, Vector size=11, Matrix transform=12, bool mask=16, MaskType maskType=317, float backgroundOpacity=19, bool backgroundEnabled=15, bool internalOnly=142, string description=318, StyleType styleType=163, string sortPosition=320, StyleId styleIdForFill=332, StyleId styleIdForStrokeFill=333, StyleId styleIdForText=334, StyleId styleIdForEffect=335, StyleId styleIdForGrid=336, VariableDataMap variableConsumptionMap=307, VariableModeBySetMap variableModeBySetMap=316, VariableSetMode[] variableSetModes=312, VariableSetID variableSetID=313, VariableResolvedDataType variableResolvedType=314, VariableDataValues variableDataValues=315, VariableScope[] variableScopes=353, DocumentColorProfile documentColorProfile=349, Paint[] fillPaints=38, Paint[] strokePaints=39, float strokeWeight=26, StrokeAlign strokeAlign=29, StrokeJoin strokeJoin=31, StrokeCap strokeCap=30, float[] dashPattern=13, float miterLimit=25, bool borderStrokeWeightsIndependent=299, float borderTopWeight=295, float borderRightWeight=298, float borderBottomWeight=296, float borderLeftWeight=297, Effect[] effects=43, float cornerRadius=20, float cornerSmoothing=160, bool rectangleCornerRadiiIndependent=149, float rectangleTopLeftCornerRadius=145, float rectangleTopRightCornerRadius=146, float rectangleBottomRightCornerRadius=148, float rectangleBottomLeftCornerRadius=147, ConstraintType horizontalConstraint=28, ConstraintType verticalConstraint=37, bool frameMaskDisabled=115, bool resizeToFit=117, bool isStateGroup=225, StateGroupPropertyValueOrder[] stateGroupPropertyValueOrders=238, bool sectionContentsHidden=283, Color backgroundColor=50, StackMode stackMode=105, float stackSpacing=107, float stackCounterSpacing=324, StackWrap stackWrap=323, StackCounterAlignContent stackCounterAlignContent=343, float stackHorizontalPadding=209, float stackVerticalPadding=210, float stackPaddingRight=233, float stackPaddingBottom=234, StackJustify stackPrimaryAlignItems=230, StackAlign stackCounterAlignItems=231, StackSize stackPrimarySizing=229, StackSize stackCounterSizing=221, bool bordersTakeSpace=294, bool stackReverseZIndex=271, float gridColumnGap=438, float gridRowGap=437, GUIDPositionMap gridColumns=436, GUIDPositionMap gridRows=435, GUIDGridTrackSizeMap gridColumnsSizing=474, GUIDGridTrackSizeMap gridRowsSizing=475, float stackChildPrimaryGrow=232, StackCounterAlign stackChildAlignSelf=236, StackPositioning stackPositioning=269, OptionalVector minSize=325, OptionalVector maxSize=326, GUID gridColumnAnchor=440, GUID gridRowAnchor=439, uint gridColumnSpan=442, uint gridRowSpan=441, LayoutGrid[] layoutGrids=47, ExportSettings[] exportSettings=45, TextData textData=42, FontName fontName=41, float fontSize=21, Number lineHeight=40, Number letterSpacing=165, TextCase textCase=34, TextDecoration textDecoration=35, TextAlignHorizontal textAlignHorizontal=32, TextAlignVertical textAlignVertical=33, TextAutoResize textAutoResize=46, TextTruncation textTruncation=280, int maxLines=351, float paragraphSpacing=23, float paragraphIndent=22, Hyperlink hyperlink=223, bool autoRename=14, bool fontVariantCommonLigatures=127, bool fontVariantContextualLigatures=128, string fontVersion=202, uint textUserLayoutVersion=203, uint textExplicitLayoutVersion=396, uint textBidiVersion=279, float textTracking=27, bool textDecorationSkipInk=412, EmojiImageSet emojiImageSet=391, uint styleID=49, Path[] fillGeometry=51, Path[] strokeGeometry=52, VectorData vectorData=48, VectorMirror handleMirroring=44, uint count=10, float starInnerScale=24, ArcData arcData=195, BooleanOperation booleanOperation=36, string symbolDescription=144, VariableWidthPoint[] variableWidthPoints=447, PluginData[] pluginData=204, SymbolData symbolData=113, NodeChange[] derivedSymbolData=125, GUID overriddenSymbolID=143, ComponentPropDef[] componentPropDefs=266, ComponentPropRef[] componentPropRefs=267, ComponentPropAssignment[] componentPropAssignments=268, VariantPropSpec[] variantPropSpecs=483, VariableDataMap parameterConsumptionMap=445, GUIDPath guidPath=111, PrototypeInteraction[] prototypeInteractions=226, PrototypeStartingPoint prototypeStartingPoint=249, ScrollDirection scrollDirection=159, ScrollBehavior scrollBehavior=186, OverlayPositionType overlayPositionType=198, OverlayBackgroundInteraction overlayBackgroundInteraction=200, OverlayBackgroundAppearance overlayBackgroundAppearance=201
message NodeChange GUID guid=1, NodePhase phase=2, ParentIndex parentIndex=3, NodeType type=4, string name=5, bool visible=6, bool locked=7, float opacity=8, BlendMode blendMode=9, Vector size=11, Matrix transform=12, bool mask=16, MaskType maskType=317, float backgroundOpacity=19, bool backgroundEnabled=15, bool internalOnly=142, string description=318, StyleType styleType=163, string sortPosition=320, StyleId styleIdForFill=332, StyleId styleIdForStrokeFill=333, StyleId styleIdForText=334, StyleId styleIdForEffect=335, StyleId styleIdForGrid=336, VariableDataMap variableConsumptionMap=307, VariableModeBySetMap variableModeBySetMap=316, VariableSetMode[] variableSetModes=312, VariableSetID variableSetID=313, VariableResolvedDataType variableResolvedType=314, VariableDataValues variableDataValues=315, VariableScope[] variableScopes=353, DocumentColorProfile documentColorProfile=349, Paint[] fillPaints=38, Paint[] strokePaints=39, float strokeWeight=26, StrokeAlign strokeAlign=29, StrokeJoin strokeJoin=31, StrokeCap strokeCap=30, float[] dashPattern=13, float miterLimit=25, bool borderStrokeWeightsIndependent=299, float borderTopWeight=295, float borderRightWeight=298, float borderBottomWeight=296, float borderLeftWeight=297, Effect[] effects=43, float cornerRadius=20, float cornerSmoothing=160, bool rectangleCornerRadiiIndependent=149, float rectangleTopLeftCornerRadius=145, float rectangleTopRightCornerRadius=146, float rectangleBottomRightCornerRadius=148, float rectangleBottomLeftCornerRadius=147, ConstraintType horizontalConstraint=28, ConstraintType verticalConstraint=37, bool frameMaskDisabled=115, bool resizeToFit=117, bool isStateGroup=225, StateGroupPropertyValueOrder[] stateGroupPropertyValueOrders=238, bool sectionContentsHidden=283, Color backgroundColor=50, StackMode stackMode=105, float stackSpacing=107, float stackCounterSpacing=324, StackWrap stackWrap=323, StackCounterAlignContent stackCounterAlignContent=343, float stackHorizontalPadding=209, float stackVerticalPadding=210, float stackPaddingRight=233, float stackPaddingBottom=234, StackJustify stackPrimaryAlignItems=230, StackAlign stackCounterAlignItems=231, StackSize stackPrimarySizing=229, StackSize stackCounterSizing=221, bool bordersTakeSpace=294, bool stackReverseZIndex=271, float gridColumnGap=438, float gridRowGap=437, GUIDPositionMap gridColumns=436, GUIDPositionMap gridRows=435, GUIDGridTrackSizeMap gridColumnsSizing=474, GUIDGridTrackSizeMap gridRowsSizing=475, float stackChildPrimaryGrow=232, StackCounterAlign stackChildAlignSelf=236, StackPositioning stackPositioning=269, OptionalVector minSize=325, OptionalVector maxSize=326, GUID gridColumnAnchor=440, GUID gridRowAnchor=439, uint gridColumnSpan=442, uint gridRowSpan=441, LayoutGrid[] layoutGrids=47, ExportSettings[] exportSettings=45, TextData textData=42, FontName fontName=41, float fontSize=21, Number lineHeight=40, Number letterSpacing=165, TextCase textCase=34, TextDecoration textDecoration=35, TextAlignHorizontal textAlignHorizontal=32, TextAlignVertical textAlignVertical=33, TextAutoResize textAutoResize=46, TextTruncation textTruncation=280, int maxLines=351, float paragraphSpacing=23, float paragraphIndent=22, float listSpacing=264, LeadingTrim leadingTrim=322, bool hangingPunctuation=337, bool hangingList=339, Hyperlink hyperlink=223, bool autoRename=14, bool fontVariantCommonLigatures=127, bool fontVariantContextualLigatures=128, string fontVersion=202, uint textUserLayoutVersion=203, uint textExplicitLayoutVersion=396, uint textBidiVersion=279, float textTracking=27, bool textDecorationSkipInk=412, EmojiImageSet emojiImageSet=391, uint styleID=49, Path[] fillGeometry=51, Path[] strokeGeometry=52, VectorData vectorData=48, VectorMirror handleMirroring=44, uint count=10, float starInnerScale=24, ArcData arcData=195, BooleanOperation booleanOperation=36, string symbolDescription=144, VariableWidthPoint[] variableWidthPoints=447, PluginData[] pluginData=204, SymbolData symbolData=113, NodeChange[] derivedSymbolData=125, GUID overriddenSymbolID=143, ComponentPropDef[] componentPropDefs=266, ComponentPropRef[] componentPropRefs=267, ComponentPropAssignment[] componentPropAssignments=268, VariantPropSpec[] variantPropSpecs=483, VariableDataMap parameterConsumptionMap=445, GUIDPath guidPath=111, PrototypeInteraction[] prototypeInteractions=226, PrototypeStartingPoint prototypeStartingPoint=249, ScrollDirection scrollDirection=159, ScrollBehavior scrollBehavior=186, OverlayPositionType overlayPositionType=198, OverlayBackgroundInteraction overlayBackgroundInteraction=200, OverlayBackgroundAppearance overlayBackgroundAppearance=201
struct Blob byte[] bytes
message Message MessageType type=1, uint sessionID=2, NodeChange[] nodeChanges=4, Blob[] blobs=6
";
+3
View File
@@ -195,6 +195,9 @@ fn unset(key: &str) -> Option<Kv<'static>> {
"scrollBehavior" => e("SCROLLS"),
"parameterConsumptionMap" => o(vec![("entries", arr(Vec::new()))]),
"textTruncation" => e("DISABLED"),
"listSpacing" => fl(0.0),
"leadingTrim" => e("NONE"),
"hangingPunctuation" | "hangingList" => b(false),
"hyperlink" => o(vec![("url", s(""))]),
_ => return None,
})
+16 -2
View File
@@ -44,6 +44,20 @@ impl<'d> Exporter<'d> {
("textTracking", fl(0.0)),
("emojiImageSet", e("APPLE")),
]);
// Written only off their defaults: a text at the defaults carries none of these four in a design file saved by
// Figma. A text style holds none of them, so a layer at the defaults has no style value to take on import.
if td.list_spacing != 0.0 {
f.push(("listSpacing", fl(td.list_spacing)));
}
if td.hanging_punctuation {
f.push(("hangingPunctuation", b(true)));
}
if td.hanging_list {
f.push(("hangingList", b(true)));
}
if td.leading_trim == figcraft_doc::LeadingTrim::CapHeight {
f.push(("leadingTrim", e("CAP_HEIGHT")));
}
if td.truncates() {
f.push(("textTruncation", e("ENDING")));
}
@@ -162,8 +176,8 @@ impl<'d> Exporter<'d> {
if td.path.is_some() {
self.warn("text on a path was written as plain text");
}
if td.list_spacing != 0.0 || td.hanging_punctuation || td.hanging_list || td.leading_trim != Default::default() || !td.features.is_empty() {
self.warn("list spacing, hanging punctuation, leading trim and OpenType features were left out");
if !td.features.is_empty() {
self.warn("OpenType features were left out");
}
}
+84 -5
View File
@@ -111,12 +111,69 @@ fn encoded_fig_maps_paragraph_list_fields_through_native_round_trip() {
assert!(!text.hanging_list);
assert!(!text.hanging_punctuation);
assert_eq!(text.leading_trim, figcraft_doc::LeadingTrim::CapHeight);
}
let (_, export_warnings) = export(&inherited, None).unwrap();
assert!(
export_warnings.iter().any(|warning| warning.contains("list spacing, hanging punctuation, leading trim")),
"existing .fig export loss must remain visible: {export_warnings:?}"
);
#[test]
fn exported_paragraph_options_survive_a_fig_round_trip() {
let shared = vec![
("listSpacing", Kv::Float(18.0)),
("hangingList", Kv::Bool(false)),
("hangingPunctuation", Kv::Bool(false)),
("leadingTrim", Kv::Enum("CAP_HEIGHT")),
];
let reset = vec![
("listSpacing", Kv::Float(0.0)),
("hangingList", Kv::Bool(false)),
("hangingPunctuation", Kv::Bool(false)),
("leadingTrim", Kv::Enum("NONE")),
];
let (inherited, _) = import(&paragraph_fig(shared.clone(), vec![]), "Synthetic paragraph options").unwrap();
let (cleared, _) = import(&paragraph_fig(shared, reset), "Synthetic paragraph resets").unwrap();
// All four off their defaults: the export writes them and the import reads them back.
let mut styled = inherited.clone();
let id = styled.nodes().find(|node| node.name == "Label").unwrap().id;
if let Some(figcraft_doc::NodeKind::Text(td)) = styled.node_mut(id).map(|node| &mut node.kind) {
td.hanging_punctuation = true;
td.hanging_list = true;
}
let (bytes, export_warnings) = export(&styled, None).unwrap();
assert!(export_warnings.iter().all(|warning| !warning.contains("list spacing")), "{export_warnings:?}");
let (back, _) = import(&bytes, "Exported paragraph options").unwrap();
let text = back.nodes().find(|node| node.name == "Label").unwrap().kind.text().unwrap();
assert_eq!(text.list_spacing, 18.0);
assert!(text.hanging_list);
assert!(text.hanging_punctuation);
assert_eq!(text.leading_trim, figcraft_doc::LeadingTrim::CapHeight);
// Everything at its default: nothing is carried over.
let (bytes, export_warnings) = export(&cleared, None).unwrap();
assert!(export_warnings.iter().all(|warning| !warning.contains("list spacing")), "{export_warnings:?}");
let (back, _) = import(&bytes, "Exported paragraph defaults").unwrap();
let text = back.nodes().find(|node| node.name == "Label").unwrap().kind.text().unwrap();
assert_eq!(text.list_spacing, 0.0);
assert!(!text.hanging_list);
assert!(!text.hanging_punctuation);
assert_eq!(text.leading_trim, figcraft_doc::LeadingTrim::None);
// OpenType features are still left out, and only a text that has some warns.
let id = styled.nodes().find(|node| node.name == "Label").unwrap().id;
if let Some(figcraft_doc::NodeKind::Text(td)) = styled.node_mut(id).map(|node| &mut node.kind) {
td.features.insert("liga".into(), false);
}
let (_, with_features) = export(&styled, None).unwrap();
assert!(with_features.iter().any(|warning| warning.contains("OpenType features were left out")), "{with_features:?}");
let (_, without) = export(&cleared, None).unwrap();
assert!(without.iter().all(|warning| !warning.contains("OpenType")), "{without:?}");
}
#[test]
fn the_schema_numbers_the_paragraph_options() {
let s = super::kiwi::Schema::parse(super::export::SCHEMA).unwrap();
let def = |n: &str| s.defs.iter().find(|d| d.name == n).unwrap();
let id = |f: &str| def("NodeChange").fields.iter().find(|x| x.name == f).unwrap().value;
assert_eq!((id("listSpacing"), id("leadingTrim"), id("hangingPunctuation"), id("hangingList")), (264, 322, 337, 339));
let values: Vec<_> = def("LeadingTrim").fields.iter().map(|x| (x.name.as_str(), x.value)).collect();
assert_eq!(values, [("NONE", 0), ("CAP_HEIGHT", 1)]);
}
fn instance_paragraph_fig(overrides: Vec<(&'static str, Kv<'static>)>, nonfinite_spacing: bool) -> Vec<u8> {
@@ -286,3 +343,25 @@ fn encoded_instance_paragraph_overrides_fall_back_only_when_invalid() {
assert!(!text.hanging_punctuation);
assert_eq!(text.leading_trim, figcraft_doc::LeadingTrim::CapHeight);
}
#[test]
fn an_instance_layer_set_back_to_the_defaults_survives_a_fig_round_trip() {
// The main's text has all four off their defaults (from its text style); the instance's copy sets them back.
let reset = vec![
("listSpacing", Kv::Float(0.0)),
("hangingList", Kv::Bool(false)),
("hangingPunctuation", Kv::Bool(false)),
("leadingTrim", Kv::Enum("NONE")),
];
let (doc, _) = import(&instance_paragraph_fig(reset, false), "Synthetic instance paragraph resets").unwrap();
let (bytes, _) = export(&doc, None).unwrap();
let (back, _) = import(&bytes, "Exported instance paragraph resets").unwrap();
let options = |parent: &str| {
let parent = back.nodes().find(|node| node.name == parent).unwrap();
let text = back.node(*parent.children.first().unwrap()).unwrap().kind.text().unwrap();
(text.list_spacing, text.hanging_list, text.hanging_punctuation, text.leading_trim)
};
assert_eq!(options("Main"), (18.0, true, true, figcraft_doc::LeadingTrim::CapHeight));
// The override row clears each field; without it the instance's text takes the main's values again.
assert_eq!(options("Copy"), (0.0, false, false, figcraft_doc::LeadingTrim::None));
}
+4 -4
View File
@@ -1,6 +1,6 @@
# File format parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (`.fig` import and export read and write a boolean group's corner radius and smoothing (#2222); `.jam` import: stickies come in as native board stickies (#1808); `.fig` import scales per-point vector corner radii with the row that supplies their network and keeps different instance scales separate in the network cache (#2097); `.fig` export preserves scaled-instance lengths reset to their unscaled main values, including nested linked-style overrides (#2097); `.fig` import: the layers of an instance scaled with the Scale tool draw at its scale factor, not at their main's lengths (#2091); a color profile per PNG, JPG and WebP export setting (Same as file, sRGB or Display P3), the pixels converted; a `.jam` connector end with an `AUTO` or `CENTER` magnet leaves the side facing the other end, not the side its stale saved point named (#1909); a FigJam elbow connector saved with its path (`connectorControlPoints`) keeps it on import, as a board connector reshaped by hand does, instead of being routed again (#1910); JPG exports take a quality, 1 to 100 with 90 the default, and embed the file's colour profile when it is sRGB too (#2004); a FigJam connector without a saved snapshot took no stroke and was invisible; its line's paint, weight, cap and dashes now come from the file's generated line record (#1807); a Dash cap setting (none, round or square) on Dash and Custom strokes, drawn on both ends of every dash; pasted layers whose image the clipboard lacks are named and show a placeholder that reads as missing (#1646); paint-style colours on layers pasted from another design app keep their style instead of turning black (#1638); imported and pasted text finds an installed family under the other names its fonts give it, and with other case and spacing (#1292); `.fig` export writes colour calculations as the colour they show in each mode and leaves out, with a warning, one that resolves to nothing (#1280); `.fig` import budgets four decoded values per compressed byte instead of one, still within 20M–128M, so dense files within that budget open instead of being refused as having too many values (#574); `.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; folders, devices and pipes are refused before anything is read (#1114); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); a FigJam `.jam` file opens as a board (#1342); FigJam `.jam` files open like `.fig` files: File ▸ Open, drag and drop, `file.import` and the command line (#1342); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); SVG import draws each pattern tile once per scale, within the layer budget (#1080); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); offline Penpot 2.x import/export (#1143); `.sketch` import checked against Sketch's renders: master backgrounds, symbol clipping, tints before Sketch 64, mask opacity, bitmap fills, image overrides on shapes, stretching stacks, blur radii, colour adjustments, OKLab gradients, text growth, shape group borders (#1389); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); image member names are checked on read, and REST image placeholders get archive-safe keys (#1086); Android Vector Drawable (XML) export; `.fig` export: files the incumbent imports, with component set properties, and grid track sizes at its field ids; Save and auto-save don't replace changes another app made to the open file (#1330); `.sketch` import, best effort, Sketch 48 and newer; the PNG row names ignore overlapping layers and where include bounding box is offered; the Fonts table's missing-font row describes the Missing fonts window; `.figcraft` carries the file kind (boards); `.fig` import: resized instances and variable modes by asset key; atomic saves; auto-save; SVG export clip of nested frames; SVG/PDF export depth refusal (#208); `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; `.fLine truncated
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-12 · **Change:** minor (`.fig` export writes list spacing, hanging punctuation and lists, and vertical trim (#2305); `.fig` import and export read and write a boolean group's corner radius and smoothing (#2222); `.jam` import: stickies come in as native board stickies (#1808); `.fig` import scales per-point vector corner radii with the row that supplies their network and keeps different instance scales separate in the network cache (#2097); `.fig` export preserves scaled-instance lengths reset to their unscaled main values, including nested linked-style overrides (#2097); `.fig` import: the layers of an instance scaled with the Scale tool draw at its scale factor, not at their main's lengths (#2091); a color profile per PNG, JPG and WebP export setting (Same as file, sRGB or Display P3), the pixels converted; a `.jam` connector end with an `AUTO` or `CENTER` magnet leaves the side facing the other end, not the side its stale saved point named (#1909); a FigJam elbow connector saved with its path (`connectorControlPoints`) keeps it on import, as a board connector reshaped by hand does, instead of being routed again (#1910); JPG exports take a quality, 1 to 100 with 90 the default, and embed the file's colour profile when it is sRGB too (#2004); a FigJam connector without a saved snapshot took no stroke and was invisible; its line's paint, weight, cap and dashes now come from the file's generated line record (#1807); a Dash cap setting (none, round or square) on Dash and Custom strokes, drawn on both ends of every dash; pasted layers whose image the clipboard lacks are named and show a placeholder that reads as missing (#1646); paint-style colours on layers pasted from another design app keep their style instead of turning black (#1638); imported and pasted text finds an installed family under the other names its fonts give it, and with other case and spacing (#1292); `.fig` export writes colour calculations as the colour they show in each mode and leaves out, with a warning, one that resolves to nothing (#1280); `.fig` import budgets four decoded values per compressed byte instead of one, still within 20M–128M, so dense files within that budget open instead of being refused as having too many values (#574); `.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; folders, devices and pipes are refused before anything is read (#1114); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); a FigJam `.jam` file opens as a board (#1342); FigJam `.jam` files open like `.fig` files: File ▸ Open, drag and drop, `file.import` and the command line (#1342); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); SVG import draws each pattern tile once per scale, within the layer budget (#1080); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); offline Penpot 2.x import/export (#1143); `.sketch` import checked against Sketch's renders: master backgrounds, symbol clipping, tints before Sketch 64, mask opacity, bitmap fills, image overrides on shapes, stretching stacks, blur radii, colour adjustments, OKLab gradients, text growth, shape group borders (#1389); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); image member names are checked on read, and REST image placeholders get archive-safe keys (#1086); Android Vector Drawable (XML) export; `.fig` export: files the incumbent imports, with component set properties, and grid track sizes at its field ids; Save and auto-save don't replace changes another app made to the open file (#1330); `.sketch` import, best effort, Sketch 48 and newer; the PNG row names ignore overlapping layers and where include bounding box is offered; the Fonts table's missing-font row describes the Missing fonts window; `.figcraft` carries the file kind (boards); `.fig` import: resized instances and variable modes by asset key; atomic saves; auto-save; SVG export clip of nested frames; SVG/PDF export depth refusal (#208); `.Line truncated
Every format Figma reads or writes, with FigCraft's support. Sources: `Info.plist` of
`/Applications/Figma.app` (document types only; nothing inside the bundle's code was read), Figma's
@@ -106,7 +106,7 @@ scales and empty-style clears; they have not been checked in other software.
(font, with styles under the names font families give their faces: "SemiBold", "ExtraBold"…, and
"Semi Bold" in Inter; size; line height, a share of
the font size as a raw multiplier; letter spacing, case, decoration, links), alignment, auto resize,
truncation, paragraph spacing and indent, list types and styled runs with their fills (indexed
truncation, paragraph spacing and indent, list spacing, hanging punctuation and hanging lists, vertical trim, list types and styled runs with their fills (indexed
by UTF-16 unit); components (description, boolean, text and instance swap properties and the
layers bound to them, with their values also as variable data), component sets (the set holds
the definitions, variant properties included with their values in order; each variant has its
@@ -141,8 +141,7 @@ scales and empty-style clears; they have not been checked in other software.
with the width on both sides when the shape has a fill too), pattern fills, WebP export settings (written as PNG),
image adjustments, glass settings other than frost, springs other than Gentle (written as ease
out), set-variable, set-variable-mode and conditional actions, scroll-to offsets, text decoration
looks, variable font axes, text on a path, nested list levels, list spacing, hanging punctuation,
leading trim, OpenType features, per-layer operations inside boolean groups, different stroke ends
looks, variable font axes, text on a path, nested list levels, OpenType features, per-layer operations inside boolean groups, different stroke ends
on lines, the Dash cap (no public name for it). Not written at all: stroke outlines of dashed and per-side strokes and of inside or
outside strokes on polygons, stars and vectors, glyph outlines of text (a reader lays text out
with its own fonts), version history and branches.
@@ -196,6 +195,7 @@ scales and empty-style clears; they have not been checked in other software.
| Date | Change | Summary |
|---|---|---|
| 2026-11-12 | minor | M6, #2305: the `.fig` export writes a text's list spacing (`listSpacing`), hanging punctuation (`hangingPunctuation`), hanging lists (`hangingList`) and vertical trim (`leadingTrim`: none or cap height), which the importer already read. A field is written only when it differs from its default (in a design file saved by Figma, a text at the defaults carries none of the four), and an instance override that goes back to the default clears it. The warning narrows to OpenType features, which the importer does not read from a `.fig`. Checked through the importer only, not in other software. |
| 2026-11-10 | minor | M4, #2222: a boolean group's corner radius and smoothing are read and written by the `.fig` importer and exporter, and by the REST importer |
| 2026-11-07 | patch | Fonts, #2092: a font loaded with `text.loadFont` is the counterpart of a font uploaded to the organization, which Figma's help center ("Manage conflicting fonts") ranks first, ahead of installed fonts and the web fonts. FigCraft chose the first face added for a family and style, so the built-in Inter 4.x kept Regular, SemiBold and Bold after Inter 3.19 was loaded and only Medium refitted. A loaded face now wins over the other faces of its family and style, the latest load over earlier ones; style matching and fallbacks are unchanged, and `refitted` lists the text whose size changes. Installed versus built-in order is not changed here. Test: `fontdb::tests::a_loaded_face_wins_over_the_built_in_one_of_its_family_and_style`. Numbers unchanged. |
| 2026-11-08 | minor | M6, #2097 follow-up: per-point vector corner radii take the scale of the row supplying `vectorData`, including nested instance rows; override and derived rows are not scaled twice. Network caching distinguishes the instance scale even when resizing gives two vectors the same final box. Synthetic tests cover missing/unit factors, half/double scale, derived size alone, overridden and derived vector data, nested scale and equal-size cache separation. Layer-radius fallback remains the separate #2115 change. Numbers unchanged. |