Skip to main content

DesignSpace Sketch

Human-friendly alternative to DesignSpace XML

DSSketch provides a simple, intuitive text format for describing variable fonts, replacing the overcomplicated and verbose XML format with clean, readable text that font designers can easily understand and edit by hand. This makes variable font development more accessible and less error-prone.

The core philosophy: Transform complex, verbose XML into simple, human-readable format that achieves 84-97% size reduction while maintaining full functionality.

Why DSSketch?

Before: DesignSpace XML (verbose, error-prone)

<?xml version='1.0' encoding='UTF-8'?>
<designspace format="5.0">
  <axes>
    <axis tag="wght" name="weight" minimum="100" maximum="900" default="400">
      <labelname xml:lang="en">Weight</labelname>
      <map input="100" output="0"/>
      <map input="300" output="211"/>
      <map input="400" output="356"/>
      <map input="500" output="586"/>
      <map input="700" output="789"/>
      <map input="900" output="1000"/>
      <labels ordering="0">
        <label uservalue="100" name="Thin"/>
        <label uservalue="300" name="Light"/>
        <label uservalue="400" name="Regular" elidable="true"/>
        <label uservalue="500" name="Medium"/>
        <label uservalue="700" name="Bold"/>
        <label uservalue="900" name="Black"/>
      </labels>
    </axis>
    <axis tag="ital" name="italic" values="0 1" default="0">
      <labelname xml:lang="en">Italic</labelname>
      <labels ordering="1">
        <label uservalue="0" name="Upright" elidable="true"/>
        <label uservalue="1" name="Italic"/>
      </labels>
    </axis>
  </axes>
  <rules>
    <rule name="heavy alternates">
      <conditionset>
        <condition name="weight" minimum="600" maximum="1000"/>
      </conditionset>
      <sub name="cent" with="cent.rvrn"/>
      <sub name="cent.old" with="cent.old.rvrn"/>
      <sub name="cent.sc" with="cent.sc.rvrn"/>
      <sub name="cent.tln" with="cent.tln.rvrn"/>
      <sub name="cent.ton" with="cent.ton.rvrn"/>
      <sub name="dollar" with="dollar.rvrn"/>
      <sub name="dollar.old" with="dollar.old.rvrn"/>
      <sub name="dollar.sc" with="dollar.sc.rvrn"/>
      <sub name="dollar.tln" with="dollar.tln.rvrn"/>
      <sub name="dollar.ton" with="dollar.ton.rvrn"/>
    </rule>
  </rules>
  <sources>
    <source filename="sources/SuperFont-Thin.ufo" familyname="SuperFont" stylename="Thin">
      <location>
        <dimension name="Weight" xvalue="0"/>
        <dimension name="Italic" xvalue="0"/>
      </location>
    </source>
    <source filename="sources/SuperFont-Regular.ufo" familyname="SuperFont" stylename="Regular">
      <location>
        <dimension name="Weight" xvalue="356"/>
        <dimension name="Italic" xvalue="0"/>
      </location>
    </source>
    <!-- ... 50+ more lines for simple 2-axis font ... -->
  </sources>
  <instances>
    <!-- ... hundreds of lines for instance definitions ... -->
  </instances>
</designspace>

After: DSSketch (clean, intuitive)

family SuperFont
path sources

axes
    wght 100:400:900
        Thin > 0
        Light > 211
        Regular > 356 @elidable
        Medium > 586
        Bold > 789
        Black > 1000
    ital discrete
        Upright @elidable
        Italic

sources [wght, ital]
    SuperFont-Thin [0, 0]
    SuperFont-Regular [356, 0] @base
    SuperFont-Black [1000, 0]
    SuperFont-Thin-Italic [0, 1]
    SuperFont-Italic [356, 1]
    SuperFont-Black-Italic [1000, 1]

rules
    dollar* cent* > .rvrn (weight >= Bold) "heavy alternates"

instances auto

Result: 93% smaller, infinitely more readable

Key Advantages

1. Human-Friendly Syntax

  • Intuitive axis definitions: wght 100:400:900 instead of verbose XML attributes
  • Simple source coordinates: [400, 0] instead of complex XML dimension tags
  • Readable rules: dollar > .rvrn (weight >= 400) instead of nested XML structures
  • Common directory paths: path sources eliminates repetitive file paths

2. Smart Automation

  • Auto instance generation: instances auto creates all meaningful combinations
  • Standard weight mapping: Recognizes Regular > 400, Bold > 700 automatically
  • Wildcard rule expansion: * > .alt finds all glyphs with .alt variants
  • UFO validation: Automatically validates source files and extracts glyph lists

3. Label-Based Syntax

Make your font files even more readable with label-based coordinates and ranges:

Label-Based Source Coordinates

# Traditional numeric format:
sources [wght, ital]
    Font-Regular [362, 0] @base
    Font-Black [1000, 1]

# Label-based format:
sources [wght, ital]
    Font-Regular [Regular, Upright] @base
    Font-Black [Black, Italic]

Label-Based Axis Ranges

# Traditional numeric format:
axes
    wght 100:400:900
    wdth 75:100:125

# Label-based ranges for weight and width:
axes
    weight Thin:Regular:Black   # Auto-converts to 100:400:900
    width Condensed:Normal:Extended  # Auto-converts to 75:100:125

Human-Readable Axis Names

# Short tags (traditional):
axes
    wght 100:400:900
    wdth 75:100:125
    ital discrete

# Human-readable names:
axes
    weight 100:400:900    # Auto-converts to wght
    width 75:100:125      # Auto-converts to wdth
    italic discrete       # Auto-converts to ital

Supported names: weight → wght, width → wdth, italic → ital, slant → slnt, optical → opsz

Label-Based Rule Conditions

# Traditional numeric format:
rules
    dollar > dollar.heavy (weight >= 700) "heavy dollar"
    ampersand > ampersand.fancy (weight >= 700 && width >= 110) "compound"

# Label-based format:
rules
    dollar > dollar.heavy (weight >= Bold) "heavy dollar"
    ampersand > ampersand.fancy (weight >= Bold && width <= Wide) "compound"
    g > g.alt (Regular <= weight <= Bold) "range condition"

Benefits:

  • More readable: weight >= Bold vs weight >= 700
  • Self-documenting: labels show semantic meaning
  • Works with all operators: >=, <=, ==, ranges
  • Supports all axes: standard and custom
  • Can mix numeric and label values
# Complete label-based example
family SuperFont
path sources

axes
    weight Thin:Regular:Black
        Thin > 0
        Light > 211
        Regular > 356 @elidable
        Medium > 586
        Bold > 789
        Black > 1000
    italic discrete
        Upright @elidable
        Italic

sources [wght, ital]
    SuperFont-Thin [Thin, Upright]
    SuperFont-Regular [Regular, Upright]
    SuperFont-Black [Black, Upright]
    SuperFont-Thin-Italic [Thin, Italic]
    SuperFont-Italic [Regular, Italic]
    SuperFont-Black-Italic [Black, Italic]

rules
    dollar* cent* > .rvrn (weight >= Bold) "heavy alternates"
    A > A.alt (Regular <= weight <= Bold) "medium weight"

instances auto

4. Advanced Features Made Simple

Beyond axes, sources and rules, a sketch also carries what a DesignSpace 5 build needs — each covered below: OR in rule conditions (||) and rules last; explicit instances next to instances auto / skip, with localized names; variable-fonts (one per discrete value, plus sub-ranges); avar2 mappings with hidden axes; lang and elidedfallback. Editors get positioned diagnostics with quick fixes, completion, an outline, and minimal-edit operations (dssketch.Sketch, see "Editing a Sketch").

Discrete Axes

# Instead of complex XML values="0 1" attributes:
ital discrete
    Upright @elidable    # No need for > 0
    Italic               # No need for > 1

Flexible Substitution Rules

rules
    # Simple glyph substitution with labels
    dollar > dollar.heavy (weight >= Bold)

    # Wildcard patterns with labels
    A* > .alt (weight >= Bold)         # All glyphs starting with A
    * > .rvrn (weight >= Medium)       # All glyphs with .rvrn variants

    # Complex conditions with labels
    ampersand > .fancy (weight >= Bold && width <= Wide)
    g > g.alt (Regular <= weight <= Bold)  # Range conditions

    # Numeric conditions still work
    thin* > .ultra (weight >= -100)    # Negative coordinates supported
    b > b.alt (450 <= weight <= Bold)  # Mix labels and numbers

Explicit Axis Order Control

# Control instance generation order
axes
    wdth 60:100:200    # First in names: "Condensed Thin" - "{width} {weight}"
        Condensed > 350.0
        Normal > 560.0 @elidable
    wght 100:400:900   # Second in names
        Thin > 100
        Regular > 400
        Black > 900

sources [wght, wdth]   # Coordinates follow this order: [weight, width]
    Thin-Condensed [100, 350]
    Regular-Condensed [400, 350] @base
    Black-Condensed [900, 350]
    Thin-Normal [100, 560]
    Regular-Normal [400, 560]
    Black-Normal [900, 560]

UFO Layer Support

Store multiple masters as layers within a single UFO file:

sources [wght]
    # Default layer (foreground) - base master
    Font-Master.ufo [400] @base

    # Intermediate masters stored as layers in same UFO
    Font-Master.ufo [500] @layer="wght500"
    Font-Master.ufo [600] @layer="wght600"
    Font-Master.ufo [700] @layer="bold-layer"

    # Separate UFO for extreme weight
    Font-Black.ufo [900]

Benefits:

  • Reduces file count: Multiple masters in one UFO
  • Organized structure: Related masters kept together
  • Full bidirectional support: DesignSpace layerName ↔ DSSketch @layer

Syntax formats:

  • @layer="layer name" - with double quotes (supports spaces)
  • @layer='layer name' - with single quotes
  • @layer=layername - without quotes (no spaces)

Can be combined with @base: Font.ufo [400] @base @layer="default"

Custom Axis

# Control instance generation order
axes
    CONTRAST CNTR 0:0:100 # First in names: "C2 Condensed Thin" - "{CNTR} {width} {weight}"
        0 C0 > 100.0 @elidable
        50 C1 > 600.0
        100 C2 > 900.0
    wdth 60:100:100
        Condensed > 350.0
        Normal > 560.0 @elidable
    wght 100:400:900   # Third in names
        Thin > 100
        Regular > 400
        Black > 900

sources [wght, wdth, CONTRAST]   # Coordinates follow this order: [weight, width, CONTRAST]
    Thin-Condensed-C2 [Thin, Condensed, C2]
    Regular-Condensed-C2 [Regular, Condensed, C2] @base
    Black-Condensed-C2 [Black, Condensed, C2]

Installation & Usage

Installation

DSSketch needs Python 3.11 or newer (as current fontTools does); on older Pythons pip installs the last 1.5.x release.

# Install uv if not already installed
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install DSSketch
uv pip install dssketch

# Or install from source (for development)
uv pip install -e .

# Install with development dependencies
uv pip install -e ".[dev]"

Using pip

pip install dssketch

# Or install from source
pip install -e .

uv pip install -e . installs into the currently active virtualenv, so the dssketch command only exists while that venv is activated. To get dssketch available everywhere while still running straight from your working copy, use uv tool install:

cd /path/to/DSSketch
uv tool install -e .

This creates an isolated environment under ~/.local/share/uv/tools/dssketch/ and links all three entry points — dssketch, dss, dssketch-data — into ~/.local/bin/ (make sure it is on your PATH; uv tool update-shell adds it). Because the install is editable, edits to src/dssketch/**.py and to src/dssketch/data/*.yaml take effect immediately — no reinstall needed.

uv tool list                          # show installed tools
uv tool install -e . --force          # reinstall (needed after changing
                                      # [project.scripts] or dependencies)
uv tool install -e ".[dev]" --force   # include dev deps (pytest, ruff, mypy)
uv tool uninstall dssketch            # remove

The pipx equivalent, if you prefer it over uv:

pipx install --editable /path/to/DSSketch

Command Line

# Convert DesignSpace → DSSketch (with UFO validation)
dssketch font.designspace

# Convert DSSketch → DesignSpace
dssketch font.dssketch

# With explicit output
dssketch input.designspace -o output.dssketch

# Skip UFO validation (not recommended)
dssketch font.dssketch --no-validation

# avar2 format options
dssketch font.designspace --matrix    # Matrix format (default)
dssketch font.designspace --linear    # Linear format

# Without installation (using Python module directly)
python -m dssketch.cli font.designspace

Python API

import dssketch
from fontTools.designspaceLib import DesignSpaceDocument

# High-level API functions (recommended)
# Convert DesignSpace object to DSSketch file
ds = DesignSpaceDocument()
ds.read("MyFont.designspace")
dssketch.convert_to_dss(ds, "MyFont.dssketch")

# With options: vars_threshold (0=disabled, 3=default), avar2_format ("matrix"/"linear")
dssketch.convert_to_dss(ds, "MyFont.dssketch", vars_threshold=0)  # no variables
dssketch.convert_to_dss(ds, "MyFont.dssketch", avar2_format="linear")  # linear format

# Convert DSSketch file to DesignSpace object
ds = dssketch.convert_to_designspace("MyFont.dssketch")

# Convert DesignSpace to DSSketch string
dss_string = dssketch.convert_designspace_to_dss_string(ds)
dss_string = dssketch.convert_designspace_to_dss_string(ds, vars_threshold=2)  # more variables

# Work with DSSketch strings (for programmatic generation)
dss_content = """
family MyFont
axes
    wght 100:400:900
        Thin > 100
        Regular > 400
        Black > 900
sources
    Thin [100]
    Regular [400] @base
    Black [900]
"""

# Convert DSSketch string to DesignSpace object
ds = dssketch.convert_dss_string_to_designspace(dss_content, base_path="./")

# Convert DesignSpace object to DSSketch string
dss_string = dssketch.convert_designspace_to_dss_string(ds)

DSSketch Format Examples

Basic 2-Axis Font

family MyFont
path path_to_sources

axes
    wght 300:400:700
        Light > 300
        Regular > 390 @elidable
        Bold > 700
    ital discrete
        Upright @elidable
        Italic

sources [wght, ital]
    Light [Light, 0]
    Regular [Regular, 0] @base
    Bold [Bold, 0]
    LightItalic [Light, 1]
    Italic [Regular, 1]
    BoldItalic [Bold, 1]

instances auto
    skip
        # Skip Light Italic (optional - removes unwanted combinations)
        Light Italic

Complex Multi-Axis Font

family SuperFont
suffix VF

axes
    wght Thin:Regular:Black         # user space 100:400:900
        Thin > 0                    # 100
        Light > 196                 # 300
        Regular > 362 @elidable     # 400
        Medium > 477                # 500
        Bold > 732                  # 700
        Black > 1000                # 900
    wdth Condensed:Normal:Extended
        Condensed > 60
        Normal > 100 @elidable
        Extended > 200
    ital discrete
        Upright @elidable
        Italic

sources [wght, wdth, ital]
    Thin [Thin, Condensed, Upright]
    Regular [Regular, Condensed, Upright] @base
    Black [Black, Condensed, Upright]
    ThinItalic [Thin, Condensed, Italic]
    Italic [Regular, Condensed, Italic]
    BlackItalic [Black, Condensed, Italic]
    ThinExtended [Thin, Extended, Upright]
    RegularExtended [Regular, Extended, Upright]
    BlackExtended [Black, Extended, Upright]
    ThinExtendedItalic [Thin, Extended, Italic]
    ExtendedItalic [Regular, Extended, Italic]
    BlackExtendedItalic [Black, Extended, Italic]


rules
    # Currency symbols get heavy alternates
    dollar cent > .rvrn (weight >= Medium)

    # Wildcard patterns
    A* > .alt (weight >= Bold)      # All A-glyphs get alternates
    dollar cent at number > .fancy (weight >= 700 && width >= 150)  # Complex conditions

instances auto
    skip
        # Skip extreme combinations
        Thin Italic
        Extended Bold Italic

Advanced Rules and Patterns

family AdvancedFont

axes
    wght 100:400:900
    wdth 60:100:200
    CONTRAST CNTR 0:50:100  # Custom axis (uppercase)

sources [wght, wdth, CONTRAST]
    Light [100, 100, 0] @base
    Bold [900, 100, 100]

rules
    # Exact glyph substitution
    dollar > dollar.heavy (weight >= 500)

    # Multiple glyphs with same target
    dollar cent > .currency (weight >= 600)

    # Prefix wildcards (all glyphs starting with pattern)
    A* > .stylistic (weight >= 700)      # A, AE, Aacute, etc.
    num* > .proportional (CONTRAST >= 50)    # number variants

    # Universal wildcard (all glyphs with matching targets)
    S* G*  > .rvrn (weight >= Regular)         # Only creates rules where .rvrn exists
    Q* > .alt (weight >= 600 && width >= 150)  # Complex conditions

    # Range conditions
    o > o.round (Regular <= weight <= Bold)

    # Negative coordinates (supported in design space)
    ultra* > .thin (weight >= -100)
    back* > .forward (CONTRAST <= -25)

instances auto

When rules apply: rules last. By default the substitutions run before every other OpenType feature (fontTools puts them in rvrn): dollar becomes dollar.heavy first, and smcp, liga … then work on dollar.heavy, so the alternates need their own feature entries. Write rules last to run them after the other features instead (rclt, DesignSpace processing="last"); the rules then have to name what those features produce (dollar.sc > dollar.sc.heavy).

rules last
    dollar dollar.sc > .heavy (weight >= Bold)

Font with UFO Layers

# Using layers to store intermediate masters in single UFO files
# Reduces file count while maintaining full design flexibility

family FontWithLayers

axes
    wght 100:400:900
        Thin > 100
        Regular > 400 @elidable
        Bold > 700
        Black > 900
    wdth 75:100:125
        Condensed > 75
        Normal > 100 @elidable
        Wide > 125

sources [wght, wdth]
    # Main master files (default layer)
    Font-Regular.ufo [400, 100] @base
    Font-Thin.ufo [100, 100]
    Font-Black.ufo [900, 100]

    # Width extremes
    Font-Condensed.ufo [400, 75]
    Font-Wide.ufo [400, 125]

    # Intermediate weight masters as layers in Font-Regular.ufo
    Font-Regular.ufo [300, 100] @layer="wght300"
    Font-Regular.ufo [500, 100] @layer="wght500"
    Font-Regular.ufo [600, 100] @layer="wght600"
    Font-Regular.ufo [700, 100] @layer="wght700"

    # Condensed intermediates as layers
    Font-Condensed.ufo [300, 75] @layer="wght300-condensed"
    Font-Condensed.ufo [700, 75] @layer="wght700-condensed"

instances auto

Key Concepts

User Space vs Design Space

User Space = Values users see (CSS font-weight: 400) = OS/2 table
Design Space = Actual coordinates where sources are located

Mapping example:
Regular > 362  means:
- User requests font-weight: 400 (Regular)
- Master is located at coordinate 362 in design space
- CSS 400 maps to design space 362

Rule Conditions Use Design Space

Important: All rule conditions use design space coordinates, not user space values.

axes
    wght 300:400:700
        Light > 0        # User 300 → Design 0
        Regular > 362    # User 400 → Design 362
        Bold > 1000      # User 700 → Design 1000

rules
    # This condition uses design space coordinate 362, not user space 400
    dollar > .heavy (weight >= 362)  # Activates at Regular and heavier

OR in Rule Conditions

Inside one pair of parentheses conditions are joined with && (all must hold). Several groups joined with || make the rule apply where any group holds — a DesignSpace rule with several conditionsets:

rules
    dollar > dollar.alt (weight >= Bold) || (width <= Condensed && weight >= 600) "heavy dollar"

Each group is one conditionset; a converted DesignSpace keeps them in one rule (DSSketch 1.5 and earlier wrote one rule per conditionset). The Sketch editing API addresses conditions by position across the groups and adds add_rule_group, remove_rule_group and set_rule_conditions(index, groups); add_rule_condition(index, condition, group=0) and add_rule(..., alternatives=[...]) take a group. rule_regions() gives the further groups as RuleRegion.alternatives.

Axis Mapping Formats

DSSketch supports three formats for defining axis mappings, giving you full control over user-space and design-space coordinates:

1. Standard Label (inferred user-space)

axes
    wght 100:400:900
        Light > 251     # Uses standard user-space value (300) → design-space value 251
        Regular > 398   # Uses standard user-space value (400) → design-space value 398
        Bold > 870      # Uses standard user-space value (700) → design-space value 870

How it works: For known labels (Light, Regular, Bold, etc.), user-space values are automatically taken from standard mappings in data/unified-mappings.yaml.

2. Custom Label (design-space as user-space)

axes
    wght 100:400:900
        MyCustom > 500  # Custom label, user_value = design_value = 500

How it works: For unknown labels, user-space value equals design-space value.

3. Explicit User-Space Mapping (full control)

axes
    wght 50:500:980
        50 UltraThin > 0       # Explicit: user=50, design=0
        200 Light > 230        # Override: user=200 instead of standard 300
        500 Regular > 420      # Override: user=500 instead of standard 400
        980 DeepBlack > 1000   # Custom: user=980, design=1000

    wdth 60:100:200
        Condensed > 380        # Standard: user=75 (from mappings)
        Normal > 560           # Standard: user=100 (from mappings)
        150 Wide > 700         # Override: user=150 instead of standard 112.5
        200 Extended > 1000    # Override: user=200 instead of standard 125

    CUSTOM CSTM 0:50:100
        0 Low > 0              # Custom axis: user=0, design=0
        50 Medium > 100        # Custom axis: user=50, design=100
        100 High > 200         # Custom axis: user=100, design=200

Format: user_value label > design_value

Use cases:

  • Create custom labels with explicit user-space values for non-standard scales
  • Override standard mappings with different user-space coordinates
  • Define custom axes with meaningful user-space values
  • Fine-tune weight/width scales beyond standard CSS values

Example from examples/MegaFont-3x5x7x3-Variable.dssketch:

axes
    wdth 60:100:200
        60 Compressed > 0      # user=60 (standard is 50), design=0
        Condensed > 380
        Normal > 560 @elidable
        150 Wide > 700         # user=150 (custom), design=700
        200 Extended > 1000    # user=200 (custom), design=1000
    wght Thin:Regular:Black
        Thin > 0
        200 Light > 230        # user=200 (override standard 300), design=230
        Regular > 420 @elidable
        Bold > 725
        Black > 1000

Family Auto-Detection

The family field is optional in DSSketch. If not specified, DSSketch will automatically detect the family name from the base source UFO file:

# Family name is optional - will be detected from UFO
path sources

axes
    wght 100:400:900
        Thin > 100
        Regular > 400
        Black > 900

sources [wght]
    Thin [100]
    Regular [400] @base    # Family name detected from this UFO's font.info.familyName
    Black [900]

instances auto

How it works:

  • When family is missing, DSSketch reads the base source UFO (@base flag)
  • Extracts font.info.familyName from the UFO using defcon
  • Falls back to "Unknown" if UFO is not found or has no familyName
  • Logs a warning if auto-detection is used (non-critical)

This is useful for quick prototyping or when the family name should always match the UFO metadata.

Discrete Axes

Traditional XML requires complex values="0 1" attributes. DSSketch makes it simple:

# Old way (still supported):
ital 0:0:1
    Upright > 0
    Italic > 1

# New way (recommended):
ital discrete
    Upright @elidable
    Italic

Automatic Instance Generation

The instances auto feature intelligently creates all possible combinations of axis labels using combinatorial logic (itertools.product):

axes
    ital discrete        # Controls name order: Italic first
        Upright @elidable
        Italic
    wght 100:400:900     # Weight second in names
        Thin > 100
        Regular > 400 @elidable
        Black > 900

instances auto  # Generates: "Thin", "Regular", "Black", "Italic Thin", "Italic", "Italic Black"

How it works:

  1. Combinatorial generation: Creates cartesian product of all axis labels
    • Axis 1 (ital): [Upright, Italic] × Axis 2 (wght): [Thin, Regular, Black]
    • Result: 2 × 3 = 6 combinations
  2. Elidable name cleanup: Removes redundant @elidable labels
    • Upright Thin → Thin
    • Upright Regular → Regular (both parts elidable)
    • Italic Regular → Italic (Regular is elidable)
  3. Final instances: Thin, Regular, Black, Italic, Italic Thin, Italic Black

Fallback for Axes Without Labels

When axes have no mappings defined (only min:def:max), DSSketch automatically generates instances from the axis range values:

family QuickPrototype

axes
    wght 100:400:900    # No labels defined
    wdth 75:100:125     # No labels defined

instances auto
# Generates 9 instances (3 × 3):
# wght100 wdth75, wght100 wdth100, wght100 wdth125
# wght400 wdth75, wght400 wdth100, wght400 wdth125
# wght900 wdth75, wght900 wdth100, wght900 wdth125

How fallback works:

  • Uses axis minimum, default, and maximum values as instance points
  • Instance names use tag+value format (e.g., wght400 wdth100)
  • Useful for quick prototyping without defining full axis mappings

This also works with avar2 fonts, where additional points from avar2 input mappings are included.

Instance Skip Functionality

When using instances auto, you can exclude specific instance combinations with the skip subsection. This is useful for removing impractical or unwanted style combinations:

axes
    wdth 60:100:200
        Condensed > 60
        Normal > 100 @elidable
        Extended > 200
    wght 100:400:900
        Thin > 100
        Light > 300
        Regular > 400 @elidable
        Bold > 700
    ital discrete
        Upright @elidable
        Italic

instances auto
    skip
        # Skip extreme thin italic combinations (too fragile)
        Thin Italic
        Light Italic

        # Skip extremely wide and heavy combinations
        Extended Bold

# Without skip: 3 widths × 4 weights × 2 italics = 24 instances
# With 3 skipped: 24 - 3 = 21 instances

Important rules for skip:

  1. Use FINAL instance names (after elidable cleanup):

    axes
        wdth 60:100:200
            Condensed > 60
            Normal > 100 @elidable
        wght 100:400:900
            Regular > 400 @elidable
            Bold > 700
    
    instances auto
        skip
            # ✅ CORRECT: Uses final name after "Normal" and "Regular" are removed
            Bold
    
            # ❌ WRONG: Would not match because "Normal Regular Bold" becomes "Bold"
            Normal Regular Bold
    
  2. Follow axis order from axes section:

    # Axes order: wdth → wght → ital
    skip
        Condensed Thin Italic      # ✅ Correct: width → weight → italic
        Thin Condensed Italic      # ❌ Wrong: doesn't match axis order
    
  3. Comments supported: Use # for inline comments explaining skip rules

Example with multiple elidable labels:

family MegaFont

axes
    CONTRAST CNTR 0:0:100
        NonContrast > 0 @elidable
        HighContrast > 100
    wdth 60:100:200
        Condensed > 60
        Normal > 100 @elidable
        Extended > 200
    wght 100:400:900
        Thin > 100
        Regular > 400 @elidable
        Bold > 700

instances auto
    skip
        # "NonContrast Normal Thin" → "Thin" (after cleanup) - so we skip "Thin"
        Thin

        # "NonContrast Normal Regular" → "Regular" (after cleanup)
        Regular

        # "HighContrast Normal Thin" → "HighContrast Thin" (after cleanup)
        HighContrast Thin

        # "NonContrast Extended Thin" → "Extended Thin" (after cleanup)
        Extended Thin

# Generation process:
# 1. Create all combinations: 2 contrasts × 3 widths × 3 weights = 18 combinations
# 2. Apply elidable cleanup (remove NonContrast, Normal, Regular where appropriate)
# 3. Check skip rules on FINAL names
# 4. Generate remaining instances

Production example from examples/MegaFont-WithSkip.dssketch:

instances auto
    skip
        # Skip extreme thin weights with reverse slant (too fragile)
        Compressed Thin Reverse
        Condensed Thin Reverse
        Extended Thin Reverse

        # Skip low contrast with compressed thin (readability issues)
        LowContrast Compressed Thin
        LowContrast Compressed Thin Slant
        LowContrast Compressed Thin Reverse

        # Skip high contrast extended black (too heavy/wide)
        HighContrast Extended Black Slant
        HighContrast Extended Black Reverse

        # Skip middle-weight compressed (redundant)
        Compressed Medium Reverse
        Compressed Extrabold

# Result: 315 total combinations - 14 skipped = 301 instances generated

Explicit Instances

instances auto covers the instances the axis labels generate, and skip leaves some out. An instance the generator does not make — a position between labels, a hand-made name, its own family, PostScript or style-map name — is written as a block next to skip, every key spelled out:

instances auto
    skip
        Display Thin
    Display Bold                      # the block's name
        location: opsz=Display, wght=700
        family: MyFont Display
        style: Bold
        postscript: MyFontDisplay-Bold
        filename: instances/MyFontDisplay-Bold.ufo
  • location is required: design space, like source coordinates, a label for its design value; an axis left out is at its default. Hidden axes may be given
  • style defaults to the block's name, family to the document's, postscript to Family-Style (spaces removed), filename to instances/<postscript>.ufo; stylemapfamily and stylemapstyle are written only when set
  • A name or value with #, quotes or edge spaces is quoted: "Wide # 2"
  • instances without auto holds only blocks; under instances off they are ignored (with a warning)
  • A block where instances auto puts an instance too is a warning; the quick fix adds that instance to skip
  • Localized names: the name keys take a language — style.de: Fett, family.de, stylemapstyle.de, stylemapfamily.de. With lang de a block without style.de gets the dictionary's name; a key overrides it for that language only

Converting a DesignSpace writes this form: the instances instances auto reproduces (same position, style and family name, PostScript name, no style-map names, localized names as lang gives them) are left to it, a generated position the DesignSpace lacks becomes a skip entry, and every other instance becomes a block. When the generator reproduces none of them, the sketch lists them all under a plain instances. Converting back gives the same instances.

Variable Fonts (variable-fonts)

Which variable fonts a build makes from one design space (DesignSpace 5 <variable-fonts>). variable-fonts auto makes one font per value of the discrete axes — every continuous axis whole, hidden ones too, the discrete ones frozen — named after the family and the discrete labels (an @elidable label left out; suffix appended). Blocks add the rest:

variable-fonts auto              # SuperFont, SuperFont-Italic
    SuperFont-Text               # a sub-range, the italic axis frozen
        axes: wght=Light:Bold, ital=Upright
        filename: SuperFont-Text.ttf
  • axes: lists wght (the whole axis), ital=Italic (frozen at a value) and opsz=10:14:24 or wght=Light:Bold (a sub-range; without a middle value the axis default). Values are user space, as in the axis ranges; labels stand for their user value
  • An axis a block leaves out is frozen at its default, as in DesignSpace; a discrete axis left out is warned about
  • Without a variable-fonts section no <variable-fonts> is written and the build decides (fontTools makes the same split as auto, named VF-ital1…)
  • Converting a DesignSpace writes variable-fonts auto when every font auto makes is there, plus a block for each other font; otherwise every font is a block. A variable font's lib is reported, not carried

Skip Rule Validation

DSSketch validates skip rules at two levels to ensure correctness and provide helpful feedback:

1. ERROR Level - Invalid Label Detection

Stops conversion if skip rules contain labels that don't exist in axis definitions:

# Example DSSketch with error:
axes
    wght 100:700
        Thin > 100
        Bold > 700
    ital discrete
        Upright
        Italic

instances auto
    skip
        Heavy Italic  # ERROR: "Heavy" not defined in any axis

Error message:

ERROR: Skip rule 'Heavy Italic' contains label 'Heavy' which is not defined in any axis.
Available labels: Bold, Italic, Thin, Upright

2. WARNING Level - Unused Skip Rule Detection

Logs warnings for skip rules that never match any generated instance (may indicate typo or elidable cleanup):

# Example DSSketch with warning:
axes
    wght 100:700
        Thin > 100
        Bold > 700
    ital discrete
        Upright @elidable
        Italic

instances auto
    skip
        Bold Upright  # WARNING: "Upright" is @elidable, so "Bold Upright" becomes just "Bold"

Warning message:

WARNING: Skip validation: 1 skip rule(s) were never used. This may indicate a typo or that elidable cleanup changed the instance names.
  - Unused skip rule: 'Bold Upright'

Label Naming Rules:

Labels cannot contain spaces. Use camelCase for compound names:

# ✅ CORRECT - camelCase labels
axes
    wght 100:900
        ExtraLight > 100    # camelCase, no spaces
        SemiBold > 900      # camelCase, no spaces

instances auto
    skip
        ExtraLight Italic   # Two labels: "ExtraLight" + "Italic"

# ❌ INCORRECT - spaces in labels
axes
    wght 100:900
        Extra Light > 100   # ERROR: spaces not allowed
        Semi Bold > 900     # ERROR: spaces not allowed

This matches standard font naming conventions (ExtraLight, SemiBold) from data/unified-mappings.yaml.

Benefits:

  • Catches typos: Detects misspelled labels before they cause silent failures
  • Identifies unreachable rules: Warns about skip rules affected by elidable cleanup
  • Clear error messages: Shows available labels for easy correction
  • Simple and predictable: Each space separates labels, no ambiguity
  • Production-ready: All validation tested on large MegaFont example (15 skip rules)

Axis order controls name sequence:

# Order 1: Width first, then Weight
axes
    wdth 60:100:200
        Condensed > 60
        Normal > 100 @elidable
    wght 100:400:900
        Thin > 100
        Regular > 400
        Black > 900

# Result: "Condensed Thin", "Condensed Regular", "Condensed Black", "Thin", "Regular", "Black"

# Order 2: Weight first, then Width
axes
    wght 100:400:900
        Thin > 100
        Regular > 400
        Black > 900
    wdth 60:100:200
        Condensed > 60
        Normal > 100 @elidable

# Result: "Thin Condensed", "Regular Condensed", "Black Condensed", "Thin", "Regular", "Black"

Complex multi-axis example:

axes
    wdth 60:100:100
        Condensed > 60
        Normal > 100 @elidable
    wght 100:400:900
        Thin > 100
        Regular > 400 @elidable
        Black > 900
    ital discrete
        Upright @elidable
        Italic

instances auto
# Generates: 2 × 3 × 2 = 12 combinations
# Result: Thin, Regular, Black,
#         Condensed Thin, Condensed, Condensed Black,
#         Thin Italic, Italic, Black Italic,
#         Condensed Thin Italic, Condensed Italic, Condensed Black Italic

Result: Automatic generation of all meaningful style combinations with proper PostScript names, file paths, and style linking based on axes order.

Elided Fallback Name (elidedfallback)

Where every label is elided (Regular + Upright + Normal …) the style name is empty, so a DesignSpace names that location with its elidedFallbackName. DSSketch derives it — the weight axis's @elidable label, else Regular — and always writes it. A sketch can name it explicitly:

family MyFont
elidedfallback Book

DesignSpace → DSSketch writes the line only when the DesignSpace's value is not the derived one.

Localized Names (lang)

One line asks for every derived name in more languages:

family SuperFont
lang de, ru, es-419

axes
    wght 100:400:900
        Thin > 100
        Regular > 400 @elidable
        Bold > 700
    ital discrete
        Upright @elidable
        Italic

The generated DesignSpace then carries, in each listed language:

  • instance style names: Bold Italic → Fett Kursiv / Жирный Курсив. These become the variable font's named instances and nameID 17 of static fonts;
  • STAT label names: Bold → Fett / Жирный;
  • axis names: Weight → Gewicht / Вес.

Words come from data/font-resources-translations.json, which covers 40 languages: az, be, bg, ca, cs, da, de, el, es, es-419, et, fi, fr, hr, hu, id, is, it, kk, ky, lt, lv, mk, nb, nl, pl, pt, pt-PT, ro, ru, sk, sl, sr, sr-Latn, sv, tr, tt, uk, uz, vi. es-419 is valid in a DesignSpace, but fontTools (4.66) cannot write it to a font's name table yet. A style name is translated word by word, keeping the English order and elisions. A word the dictionary lacks, such as a custom label like HighContrast, stays in English and is listed in the conversion report. To add or change words, copy the dictionary and edit it:

dssketch-data copy font-resources-translations.json
dssketch-data edit

Converting a DesignSpace back writes lang per language: a language goes in when the dictionary gives its axis and label names exactly and the names of most instances; instances with names of their own become explicit blocks with style.de: keys. A language that fails is not carried (its instance names still travel in blocks), and the axis and label names in it are reported.

Disabling Instance Generation (instances off)

When you want to completely disable automatic instance generation (e.g., for avar2 fonts where instances are not needed or should be managed externally):

family MyFont

axes
    wght 100:400:900
    wdth 75:100:125

instances off

This produces a DesignSpace file with zero instances, which is useful for:

  • avar2 variable fonts where instances may be generated differently
  • Build pipelines that generate instances externally
  • Testing axis configurations without instance overhead

avar2 Support (OpenType 1.9)

DSSketch provides comprehensive support for avar2 (axis variations version 2), enabling non-linear axis mappings and inter-axis dependencies. This is essential for sophisticated variable fonts like parametric fonts.

User Space vs Design Space in avar2

Critical concept: avar2 mappings have a clean separation between spaces:

  • Input ([axis=value]): Always USER space — the CSS values that applications request
  • Output (axis=value): Always DESIGN space — the internal font coordinates

Labels always mean user space:

axes
    wght 100:400:900
        Regular > 435     # Axis mapping: user=400 → design=435 (default)
    wdth 75:100:125
        Condensed > 75
        Normal > 100

avar2
    # Input uses USER space: Regular=400, Condensed=75 (CSS standard)
    # Output uses DESIGN space: wght=385
    [wght=Regular, wdth=Condensed] > wght=385

Interpretation:

  • When user requests Regular and Condensed
  • The font will use design coordinate 385 instead of the default 435
  • This allows the font to optically compensate for the condensed width

The axis mapping Regular > 435 defines the DEFAULT design value. avar2 can OVERRIDE it for specific axis combinations.

How avar2 mappings combine

An avar2 row reads like an assignment, and at its own input location it is one: the output axes land exactly on the values written. Everywhere else fontTools builds the result from all the rows, and a few consequences surprise people:

  • Between rows, values are interpolated; at a combination nobody wrote, changes add up. [wght=Bold] > XOPQ=120 and [wdth=Condensed] > XOPQ=60 each move XOPQ away from its default; at Bold Condensed both changes apply on top of each other. If the sum is not what you want, write the corner: [wght=Bold, wdth=Condensed] > XOPQ=90.
  • The default location is the base. A row whose inputs all sit at their defaults must leave every output at its default ($): fontTools takes that row as the starting point of the axis's changes, so a different value there is lost and offsets that axis in every other row. DSSketch warns about it.
  • Mappings do not chain. Inputs are the location the user asked for, never the output of another row. A row whose input is a hidden axis applies only when something outside the font sets that axis directly. DSSketch warns about it.
  • Values outside an axis's range are clamped: an input to the axis's user range, an output to its design range. Two rows that end up at the same input location cannot be built at all.

diagnose() reports all of these with codes (AVAR2_DEFAULT_INPUT 5.54, AVAR2_OUTPUT_OUT_OF_RANGE 5.55, AVAR2_DUPLICATE_INPUT 5.56, AVAR2_HIDDEN_INPUT 5.58, AVAR2_INPUT_OUT_OF_RANGE 5.45), plus an axis name no axis has (AVAR2_UNKNOWN_AXIS 5.42, an error that stops conversion) and a visible axis that only avar2 outputs drive (AVAR2_OUTPUT_ONLY_VISIBLE 5.57; a parametric axis usually belongs under axes hidden). Only the unknown axis, two rows at one location and an input that looks like a design value from an old sketch are errors; the rest are warnings, because fontTools builds such a font, just not the one the rows describe.

Basic avar2 Syntax

Mapping structure: [input] > output

  • Input: [axis=value] — USER space coordinate (what CSS/apps request)
  • Output: axis=value — DESIGN space value (internal font coordinate)

Example 1: Non-linear weight curve (from examples/avar2.dssketch)

axes
    wght 1:400:1000 "Weight"
    wdth 50:100:150 "Width"

avar2 matrix
    outputs      wght  wdth
    [wght=100]   300   -      # user asks wght=100 → font uses wght=300
    [wght=400]   $     -      # user asks wght=400 → font uses default (400)
    [wght=700]   600   -      # user asks wght=700 → font uses wght=600
    [wdth=75]    -     90     # user asks wdth=75  → font uses wdth=90
    [wdth=100]   -     $      # user asks wdth=100 → font uses default (100)

Here - means "no change for this axis", $ means "use axis default".

Example 2: Optical size affects weight and width (from examples/avar2OpticalSize.dssketch)

axes
    wght 1:400:1000 "Weight"
    wdth 50:100:150 "Width"
    opsz 6:16:144 "Optical size"

avar2 matrix
    outputs                         wght  wdth
    [opsz=6, wght=400, wdth=100]    600   125   # small text: heavier, wider
    [opsz=144, wght=400, wdth=100]  200   75    # large display: lighter, narrower

At small sizes (opsz=6), text needs more weight to be readable. At large sizes (opsz=144), less weight looks better.

Example 3: Hidden parametric axes (from examples/avar2QuadraticRotation.dssketch)

axes
    ZROT 0:0:90 "Rotation"

axes hidden
    AAAA 0:0:90
    BBBB 0:0:90

avar2 matrix
    outputs    AAAA  BBBB
    [ZROT=0]   $     $      # at rotation=0: use defaults
    [ZROT=90]  90    90     # at rotation=90: set both to 90

User controls ZROT, font internally adjusts hidden axes AAAA and BBBB.

Example 4: Cross-axis dependency with labels

axes
    wght 100:400:900
        Light > 300
        Regular > 435      # user=400 → design=435 (default)
        Bold > 700
    wdth 75:100:125
        Condensed > 75
        Normal > 100
        Wide > 125

sources [wght, wdth]
    Regular-Normal [Regular, Normal] @base
    Bold [Bold, Normal]
    .....

avar2
    # Labels resolve to USER space: Regular=400, Condensed=75
    # Output is DESIGN space
    [wght=Regular, wdth=Condensed] > wght=385
    [wght=Bold, wdth=Condensed] > wght=650

What this means:

  • Regular > 435 defines the DEFAULT design value for Regular weight
  • At Condensed width, Regular needs a lighter design value (385) for optical balance
  • The label Regular always means user=400 everywhere in DSSketch
  • The converter automatically translates user→design for DesignSpace XML

Sources for avar2 fonts — use AXIS=value format (from examples/avar2-RobotoDelta-Roman.dssketch):

sources
    Roboto-Regular opsz=0 @base
    Roboto-GRAD-250 opsz=0, GRAD=-250
    Roboto-GRAD150 opsz=0, GRAD=150
    Roboto-VROT13 opsz=0, VANG=13, VROT=13
    Roboto-XTUC741-wght100 opsz=1, wght=100, wdth=151, XTUC=741

Format: SourceName AXIS=value, AXIS=value, ... — list only axes that differ from defaults. Values are design-space coordinates (same as in DesignSpace XML xvalue).

Variables (avar2 vars) — define reusable values (from examples/avar2Fences.dssketch)

avar2 vars
    $wght1 = 600           # define variable

avar2 matrix
    outputs                  wght    wdth
    [wght=1000, wdth=50]     $wght1  50     # use variable (= 600)
    [wght=1000, wdth=90]     $wght1  90     # same value reused
    [wght=600, wdth=50]      $wght1  50
    [wght=600, wdth=90]      $wght1  90

Variables start with $, useful when same value appears many times.

Linear vs Matrix Format

Linear format — one mapping per line:

avar2
    [wght=100] > wght=300
    [wght=700] > wght=600

    # With optional description name:
    "opsz144_wght1000" [opsz=144, wght=1000] > XOUC=244, XOLC=234

Matrix format — tabular, better for multiple output axes:

avar2 matrix
    outputs      wght  wdth
    [wght=100]   300   -
    [wght=700]   600   -

Complex matrix (from examples/avar2-RobotoDelta-Roman.dssketch):

avar2 matrix
    outputs                                       XOPQ  XOUC  XOLC  XTUC  YOPQ  YOUC
    [opsz=-1, wght=100, wdth=25, slnt=0, GRAD=0]  50    50    50    451   48    48
    [opsz=-1, wght=400, wdth=25, slnt=0, GRAD=0]  100   100   100   430   85    85
    [opsz=-1, wght=1000, wdth=25, slnt=0, GRAD=0] 150   150   150   400   105   105
    [opsz=0, wght=100, wdth=100, slnt=0, GRAD=0]  47    47    47    516   44    44
    [opsz=0, wght=400, wdth=100, slnt=0]          $     $     $     $     $     $
    [opsz=1, wght=100, wdth=25, slnt=0, GRAD=0]   2     2     2     278   2     2

Each row: [input conditions] → values for all output columns. $ = axis default, - = no output. A row may start with its own name, as a linear one does: "opsz144" [opsz=144] 84 78.

Rows at one input location combine. A matrix wider than 8 output columns is written as several avar2 matrix sections, each repeating the input rows it needs, so a line stays readable:

avar2 matrix
    outputs       XOUC  XOLC  XOFI  XTUC  XTUR  XTUD  XTLC  XTLR
    [opsz=144]    84    78    79    348   470   $     224   308

avar2 matrix
    outputs       YOUC  YOLC  YOFI  YTUC  YTLC  YTAS  YTDE  YTFI
    [opsz=144]    $     $     $     $     500   $     $     $

The two [opsz=144] rows are one mapping. Two rows at one location may not set the same output (an error). Rows at the default location never combine — fontTools allows several — so the writer puts them in a linear section after the matrices. A linear row longer than 100 characters is written as a { } block, 8 outputs a line.

Both formats produce identical results. Matrix is default for output, linear is easier to read for simple cases.

CLI Options for avar2

# Format options
dssketch font.designspace --matrix    # matrix format (default)
dssketch font.designspace --linear    # linear format

# Variable generation options
dssketch font.designspace             # auto-generate vars (threshold=3, default)
dssketch font.designspace --novars    # disable variable generation
dssketch font.designspace --vars 2    # threshold=2 (more variables)
dssketch font.designspace --vars 5    # threshold=5 (fewer variables)

Variable generation: Values appearing N+ times become variables ($var1, $var2, etc.)

Instances with avar2

For avar2 fonts, instances off is often the better choice:

instances off   # recommended for most avar2 fonts

Why? avar2 fonts often have complex axis interactions where automatic instance generation produces too many or inappropriate combinations. Use instances auto only when you understand which combinations make sense.

When using instances auto with avar2 fonts that have axes without labels, DSSketch automatically generates instance points from:

  1. Axis min, default, and max values
  2. Unique input points from avar2 mappings
axes
    wght 1:400:1000  # No labels defined
    opsz 6:16:144    # No labels defined

avar2
    [wght=100] > wght=300
    [wght=700] > wght=600
    [opsz=144] > wght=200

instances auto
# Generates instances at: wght=[1, 100, 400, 700, 1000] × opsz=[6, 16, 144]
# Instance names: wght1 opsz6, wght100 opsz6, wght400 opsz16, etc.

Hidden axes are excluded from instance generation - only user-facing axes contribute to instance combinations.

Architecture & API

Core Components

  • High-level API: convert_to_dss(), convert_to_designspace(), convert_dss_string_to_designspace()
  • Parsers: DSSParser with comprehensive validation and error detection
  • Writers: DSSWriter with optimization and compression
  • Converters: Bidirectional DesignSpaceToDSS ↔ DSSToDesignSpace
  • Validation: UFOValidator, UFOGlyphExtractor for robust master file handling
  • Instances: createInstances() for intelligent automatic instance generation

Data Management

# After pip install -e . (recommended):
dssketch-data info                              # Show data file locations
dssketch-data copy unified-mappings.yaml        # Copy default file for editing
dssketch-data edit                              # Open user data directory
dssketch-data reset --file unified-mappings.yaml # Reset specific file
dssketch-data reset --all                       # Reset all files

# Without installation (using Python module directly):
python -m dssketch.data_cli info
python -m dssketch.data_cli copy unified-mappings.yaml
python -m dssketch.data_cli edit

Error Handling

from src.dssketch.parsers.dss_parser import DSSParser

# Strict mode (stops on first error)
parser = DSSParser(strict_mode=True)
try:
    result = parser.parse(content)
except ValueError as e:
    print(f"Parsing failed: {e}")

# Non-strict mode (collects all issues)
parser = DSSParser(strict_mode=False)
result = parser.parse(content)

# Review all validation issues
for error in parser.validator.errors:
    print(f"ERROR: {error}")
for warning in parser.validator.warnings:
    print(f"WARNING: {warning}")

Editor Support (highlighting, errors, completion)

For a code editor that edits .dssketch — the syntax knowledge stays in DSSketch, the editor only maps token types to styles:

import dssketch

for token in dssketch.tokenize(text):      # every non-blank character, in order
    print(token.line, token.column, token.length, token.type, token.text)
    # e.g. 3 4 4 axis-tag wght

for d in dssketch.diagnose(text):          # never raises; all problems at once
    print(d.severity, d.line, d.column, d.end_column, d.message)
    # in text order; a check about an axis, mapping or source sits on its line
    # line=None: about the document as a whole (no axes, no base source …)

for c in dssketch.complete(text, line, column):   # the word at the cursor
    print(c.text, c.type, c.detail, c.column, c.end_column)
    # in "    Font-Thin [Re|": Regular label "design 420" 15 17

complete() knows the place: section keywords at the start of a line, the axis's own labels in source coordinates and rule conditions (with their design value), standard weight/width labels in an axis's mappings, flags after a source, unused axes, avar2 axes and variables. text replaces columns column..end_column of the cursor line.

GtkSourceView: DSSketch ships a language definition, generated from the same tables, for instant highlighting without calling Python. Its style ids are the token types (dssketch:axis-tag, dssketch:label …), so one style scheme styles both; tokenize() then refines what a static definition cannot know.

from dssketch.gtksourceview import language_spec_path
import os

manager = GtkSource.LanguageManager.get_default()
manager.set_search_path([os.path.dirname(language_spec_path())] + manager.get_search_path())
buffer.set_language(manager.get_language("dssketch"))

Token types (dssketch.syntax.TOKEN_TYPES) are only ever added, never renamed: keyword, axis-name, axis-tag, label, number, source, glyph, text, string, flag, variable, operator, punctuation, comment, unknown. All of it loads only the standard library and PyYAML.

Every diagnostic has a stable code ("5.31" = SOURCE_COORD_COUNT; see dssketch.codes) to key documentation on, and fixes an editor can offer:

for d in dssketch.diagnose(text):
    for fix in d.fixes:            # e.g. "Change to 'Black'", "Keep @base only on 'Font-Regular'"
        print(fix.title, fix.edits)  # TextEdits against `text`

Codes are only ever added.

Editing a Sketch (minimal edits, comments kept)

For tools whose panels change a hand-written .dssketch — an axis ruler, a map of sources, an instance list. Sketch is an immutable snapshot of the text; every operation returns the minimal text edits, so comments, blank lines, indentation and the spelling of everything else survive, and an unchanged sketch gives back its text byte for byte:

from dssketch import Sketch, SourceKey, EditError

sk = Sketch.parse(text)                       # never raises; sk.diagnostics
result = sk.set_source_coordinate("Font-Bold", "wght", 700)
result.edits     # [TextEdit(line=18, col=16, end_line=18, end_col=21, new_text='Bold')]
result.text      # the new text
sk = result.sketch

sk.add_mapping("wght", "Bold", 700)           # "        Bold > 700", in user order
sk.rename_label("wght", "Black", "Heavy")     # also in sources, rules, skip, avar2
sk.add_axis("wdth", 75, 100, 125)             # adds the column to every source
sk.add_skip("Thin Italic")
sk.add_rule("dollar", "dollar.alt", "weight >= Bold", name="heavy dollar")
sk.move_rule(2, 0)                            # rules apply in order
sk.add_rule_condition(0, "italic == 1")       # also set_rule_condition / remove_rule_condition
sk.add_instance("Display Bold", {"opsz": "Display", "wght": 700}, family="MyFont Display")
sk.set_instance_field(0, "postscript", "MyFontDisplay-Bold")   # None removes a key
sk.set_rules_processing(True)                 # `rules last`
sk.add_avar2_mapping({"opsz": 144}, {"XOUC": 84, "YTUC": "$"})
sk.set_avar2_output(0, "XOUC", 86)            # where that input location sets it, in any matrix
sk.set_avar2_var("XO", 91)                    # avar2 vars; None removes one not in use

sk.item_at(18, 20)   # ("source", SourceKey("Font-Bold.ufo"), "coord", "wght")
sk.span(("mapping", "wght", "Bold", "design"))   # Span(line, col, end_line, end_col)

Objects are addressed by stable keys — an axis by its tag, a mapping by axis and label, a source by SourceKey(filename, layer) (or a name that is unique), a rule, an explicit instance or an avar2 row by its position, a skip entry by its text. Values follow the document's own spelling (prefer="keep": a label where the neighbouring lines write labels), or prefer="label" / "number".

An operation is all or nothing: one the parser would reject (a removed label still in use, a base source off the defaults, a value out of range) raises EditError with a code and changes nothing, and it works on text with errors elsewhere. Parsed models carry span and spans (their fields). Light: standard library and PyYAML only.

Font data from the host: DSSketch reads family, style, glyph and layer names from the UFOs on disk. An application with the fonts open passes a provider instead — to the conversion, diagnose(), complete() and Sketch.parse():

from dssketch import FontData

class OpenFonts(FontData):                  # every method may return None: unknown
    def glyph_names(self, source):          # source: "sources/Font-Bold.ufo"
        font = app.font_for(source)
        return font.keys() if font else None
    # family_name(), style_name(source), layer_names(source), source_exists(source)

ds = dssketch.convert_dss_string_to_designspace(text, font_data=OpenFonts())
dssketch.diagnose(text, font_data=OpenFonts())   # + missing UFO / layer, rule glyphs
dssketch.complete(text, line, col, font_data=OpenFonts())  # + glyph and layer names
dssketch.expand_rule(rule, glyph_names)    # [(glyph, target, target_exists), ...]

Without a provider, diagnose() and complete() never read the disk.

What the sketch means, in numbers (1.5.1): for rulers, a map of sources, an instance list and a preview — without UFOs and without a conversion. Axes are keyed by their DesignSpace name ("Weight", "opsz"), as fontTools and an interpolator see them, and accepted by that name, the tag or the sketch's name:

sk.user_to_design("wght", "Bold")      # 625.0 (axis: Regular > 400, 700 Bold > 625)
sk.design_to_user("wght", 512.5)       # 550.0

for s in sk.source_locations():        # SourceKey, name, user and design location
    print(s.key, s.design_location)    # {'Weight': 625.0, 'italic': 0.0}

for i in sk.instances():               # what `instances auto` generates, and what `skip` removed
    print(i.index, i.style_name, i.design_location, i.skipped, i.skip_span)

sk.rule_regions()                      # RuleRegion(index, name, {'Weight': (625.0, 1000.0)})
sk.substitutions_at({"Weight": 700}, glyph_names, space="user")   # {'dollar': 'dollar.alt'}
sk.hover(line, col)                    # "Bold (Weight): user 700, design 625"
sk.outline()                           # Symbol tree: sections > axes > mappings, sources, rules …

instances() gives the same names, order and locations as DSS → DS (one generator serves both); a skipped instance has index=None and the span of the skip entry that removed it. substitutions_at() applies the rules in order, as fontTools does; pattern rules expand against glyph_names, or the font data's default master. The free functions dssketch.expand_instances(text), source_locations, rule_regions, substitutions_at, user_to_design, design_to_user, hover and outline take a Sketch or a text. None of them raises.

Writing a changed DesignSpace back (apply_designspace): an application that edits the DesignSpaceDocument (a validator's fix, a master moved on a map) gets the edits that make the text describe it, with the text's comments and spelling kept:

result, report = sk.apply_designspace(ds_doc)   # or dssketch.apply_designspace(text, ds_doc)
result.edits      # minimal TextEdits; result.text, result.sketch
for issue in report.warnings:                   # what was not written
    print(issue.id, issue.description)          # "6.0" Not applied: …

Axes (range, name, mappings — a renamed label follows into its uses), sources (coordinates, @base, @sparse, added and removed) and rules (conditions, glyphs, added, removed, order, rules last) are written. A pattern rule (a* > .ss01) stays as written while the DesignSpace's substitutions fit it. Instances are written as a conversion would describe them (instances auto/off, skip entries, explicit blocks), but only when the DesignSpace's instances differ from what the text generates: an axis label added in the DesignSpace makes new instances, not skip entries. avar2 is written by input location: an output is set where that location already has it (a matrix cell or a linear assignment), new locations are added and removed ones removed. The report (category 6, "Apply") lists an edit the sketch refuses.

Comments: a comment starts at the first # outside quotes ("Sans #1" is a name). When a line is removed, the comments directly above it (no blank line between) go with it; a comment followed by a blank line stays.

Intelligent Typo Detection

DSSketch uses advanced Levenshtein distance algorithm to detect typos and suggest corrections, similar to how git, npm, and bash help users. Three severity levels ensure robust validation:

1. CRITICAL - Duplicate Mapping Labels

Prevents labels used across multiple axes, which breaks instance generation:

axes
    wght 100:900
        Light > 100   # ❌ CRITICAL ERROR
    wdth 75:125
        Light > 75    # Same label "Light" in different axes!

Error: CRITICAL: Mapping label 'Light' is used in multiple axes: 'weight' (wght), 'width' (wdth)

2. ERROR - Axis Tag Typos

Detects typos in standard axis tags:

axes
    wgth 100:900      # ❌ ERROR: Typo detected → suggests 'wght'
    widht 75:125      # ❌ ERROR: Typo detected → suggests 'wdth'

    # ✅ Human-readable names are VALID (automatically converted):
    weight 100:900    # ✅ OK: auto-converts to 'wght'
    width 75:125      # ✅ OK: auto-converts to 'wdth'
    italic discrete   # ✅ OK: auto-converts to 'ital'

Detection:

  • Typos in 4-char lowercase tags: wgth → suggests wght, widht → suggests wdth
  • Human-readable names supported: weight, width, italic, slant, optical → auto-converted to standard tags
  • UPPERCASE tags treated as custom axes (not checked for typos)

3. WARNING - Mapping Label Typos

Detects typos in standard weight/width mapping labels:

axes
    wght 100:400:900
        Lite > 300        # ⚠️ Warning: Lite → Light
        Reguler > 400     # ⚠️ Warning: Reguler → Regular
        Bol > 700         # ⚠️ Warning: Bol → Bold

Smart Cross-Axis Logic:

  • Only wght: Allows both weight AND width labels (Light, Bold, Condensed, Wide)
  • Only wdth: Allows both width AND weight labels (Condensed, Wide, Light, Bold)
  • Both wght and wdth: Each axis restricted to its own standard labels

How it works:

  • Uses edit distance with threshold of 2 characters
  • Suggests closest standard label if typo detected
  • Custom labels (distance > 2) are accepted without warnings
# Test typo detection
python -m pytest tests/test_typo_validation.py -v

Real-World Benefits

For Font Designers

  • Edit by hand: Simple text format vs complex XML
  • Fewer errors: Robust validation catches typos and mistakes
  • Faster iteration: Quick edits without XML complexity
  • Better version control: Clean diffs, readable changes

For Developers

  • Easy integration: Simple Python API
  • Smaller files: 84-97% compression improves performance
  • Reliable parsing: Comprehensive error detection and handling
  • Flexible automation: Programmatic generation and processing

For Teams

  • Better collaboration: Human-readable format for reviews
  • Reduced errors: Validation prevents broken DesignSpace files
  • Simplified workflows: Less complex tooling needed
  • Knowledge sharing: Format is self-documenting

Testing

# Test with provided examples (after pip install -e .)
dssketch examples/SuperFont-6x2.designspace
dssketch examples/MegaFont-3x5x7x3-Variable.dssketch

# Or without installation
python -m dssketch.cli examples/SuperFont-6x2.designspace

# Run validation tests
python -m pytest tests/test_parser_validation.py -v

# Test specific validation features
python -m pytest tests/test_parser_validation.py::TestParserValidation::test_keyword_typo_detection -v

Examples

The examples/ directory contains:

  • examples/MegaFont-3x5x7x3-Variable.designspace → Complex multi-axis font
  • examples/SuperFont-6x2.dssketch → Equivalent DSSketch format (93% smaller)
  • examples/FontWithLayers.dssketch → UFO layer support demo
  • Various avar2 examples (avar2*.dssketch) → OpenType 1.9 axis variations
  • Various test files showing edge cases and features

DSSketch makes variable font development human-friendly. Simple syntax, powerful features, dramatic size reduction.

Author & Credits

DSSketch was developed by Alexander Lubovenko in collaboration with Claude (Anthropic AI).

This project represents a collaborative effort combining human expertise in font design and engineering with AI-assisted development to create more accessible and efficient tooling for the variable font community.

License

MIT License - see LICENSE file for details.

Copyright (c) 2025 Alexander Lubovenko

Metadata

Release files for dssketch 1.6.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dssketch 1.6.1
File Size Uploaded Provenance
dssketch-1.6.1.tar.gz 265.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for dssketch 1.6.1
File Interpreter ABI Platform
dssketch-1.6.1-py3-none-any.whl Python 3 none any Details

Total release size: 503.2 kB

Release files / dssketch-1.6.1.tar.gz

Download URL dssketch-1.6.1.tar.gz
Size 265.6 kB
Tags Source
SHA-256 checksum
How to use checksums
5e0364137e2b9057a94b968595e497b4dd5139455c03bf64013876f92adefe57
BLAKE2b-256 checksum
How to use checksums
22546721458c47a28ee570f25c05191b78294f5492fded457245027b71aafb9f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Release files / dssketch-1.6.1-py3-none-any.whl

Download URL dssketch-1.6.1-py3-none-any.whl
Size 237.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
08ae0c92f104c3ba9c12586a864bd28795d4a2b64e1d10e872df1e16278daf39
BLAKE2b-256 checksum
How to use checksums
ed56629e9b464ec53de53470b1c578b6dd9250ff3c8c4e0b25659ae9e8063e2b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Security for dssketch 1.6.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for dssketch-1.6.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.6.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page