Installation

SMufoLib requires Python 3.10 or later. It is listed in the Python Package Index (PyPI) and can be installed with pip:

$ python -m pip install smufolib

First Steps

Start by importing SMufoLib:

>>> from smufolib import Font

Then instantiate a font object:

>>> font = Font("path/to/MyFont.ufo")  

A font may also be instantiated from another fontParts.base.BaseFont object. This allows SMufoLib to be used seamlessly within other FontParts-based environments, such as RoboFont:

from smufolib import Font
from mojo.roboFont import CurrentFont

font = Font(CurrentFont())

Before going further, it’s a good idea to review the FontParts Object Reference. SMufoLib’s Font, Layer, and Glyph classes extend FontPart’s defcon-based reference implementation and serve as the foundation for the features described below.

Note

SMufoLib is designed to work with the default layer of a UFO font. SMuFL metadata is stored in the font-level and glyph-level Lib objects, which are shared across all layers. As such, SMufoLib does not interact with or expose any layer-specific functionality.

Configuring SMufoLib

SMufoLib supports customization through a configuration file, allowing you to tailor the library’s behavior to project- or user-specific needs.

Configuration File Structure

The configuration file uses an INI-style format with sections and key-value pairs.

A minimal example:

[color.marks]
mark1 = (1.0, 0.0, 1.0, 1.0)

[engravingDefaults]
auto = false

This example changes the primary mark color and disables automatic calculation of EngravingDefaults.

For a complete list of sections, options, and default values, see the Sections and Options section of the API documentation.

Configuration File Discovery

SMufoLib reads configuration files in layers, allowing local project or user-specific settings to override the default package configuration.

You can pass a specific path argument when calling config.load(), which will take ultimate precedence over any other sources (see Reading Configurations). Otherwise, the following locations are checked in order of increasing precedence:

  1. Current working directory (./smufolib.cfg)

  2. Home directory (~/.smufolib.cfg) as returned by os.path.expanduser()

  3. Environment variable SMUFOLIB_CFG

Files found later in the list override values from earlier files. You only need to include the keys you want to change; all unspecified values are inherited from the defaults.

To define the environment variable SMUFOLIB_CFG:

  • On macOS or Linux:

    export SMUFOLIB_CFG=/path/to/smufolib.cfg
    

    Add this to your shell startup file (e.g., ~/.zshrc or ~/.bashrc) to make it persistent.

  • On Windows, use the set command:

    set SMUFOLIB_CFG=C:\path\to\smufolib.cfg
    

See also

For details on which filenames are auto-discovered, see About Configuration File Naming.

Reading Configurations

If you want to access the configurations in your scripts, the config.load() function will return a parsed instance of it as a dict:

>>> from smufolib import config
>>> cfg = config.load()
>>> cfg["request"]
{'encoding': 'utf-8', 'warn': True}

See the argparse documentation for more info on working with configuration files.

Glyph Management

The Smufl class provides a SMuFL-aware interface to the font, enabling glyph access and manipulation by canonical SMuFL names, [1] using familiar dictionary operations.

To insert the glyph U+E000 (brace):

>>> from smufolib import Glyph
>>> glyph = Glyph()
>>> font.smufl["brace"] = glyph

For glyphs in the recommended SMuFL range, this automatically assigns the correct Unicode and font glyph name:

>>> glyph = font.smufl["brace"]
>>> glyph.name
'uniE000'
>>> glyph.unicode
57344

For optional or custom SMuFL glyphs, the given name is used as the glyph name, and no Unicode value is assigned unless explicitly set.

You can also create new glyphs via the Smufl.newGlyph() method, which represents a SMuFL-specific equivalent to Font.newGlyph, allowing creation by SMuFL name rather than glyph name:

>>> glyph = font.smufl.newGlyph("brace")

To remove a SMuFL glyph, use the del statement:

>>> del font.smufl["brace"]

Setting Attributes

SMufoLib stores SMuFL-specific font and glyph metadata directly within the font file. Attributes [2] can either be set individually during the design process or imported from metadata files.

Manually Setting Attributes

Attributes are accessed through the Smufl object, and may be set for the font and individual glyphs:

>>> font.smufl.name = "MyFont"
>>> font.smufl.version = 1.0
>>> font.smufl.designSize = 20
>>> font.smufl.sizeRange = (16, 24)
>>> glyph = font["uniE050"]
>>> glyph.smufl.name = "gClef"
>>> glyph.smufl.description = "G clef"
>>> glyph.smufl.classes = ("clefs",)

Note

  • Some attributes, like Smufl.name, will return different values depending on whether they are accessed through Font.smufl or Glyph.smufl.

  • FontParts maintains consistent references to parent-level objects. As a result, font-specific Smufl attributes remain accessible from both the font itself and any of its glyphs.

Importing Attributes

The essential glyph identification attributes (Smufl.name, Smufl.description and Smufl.classes) may be imported from preexisting metadata files using the importID script. See Running Scripts for more information.

Working with Metadata

Once SMuFL specific glyph names and other attributes have been set, SMufoLib provides useful features like:

Glyph Ranges

You can introspect the SMuFL glyph ranges that apply to the font as a whole or to individual glyphs are accessible via the following attributes:

  • font.smufl.ranges – Returns a tuple containing all SMuFL ranges in the font.

  • glyph.smufl.ranges – Returns a tuple containing the SMuFL range that the glyph belongs to.

Like Smufl, the Range object behaves like a collection of Glyph instances. It is particularly useful when working with multiple glyphs by type:

>>> for glyph in font:
...     for range in glyph.smufl.ranges:
...         if range.name == "staffBracketsAndDividers":
...             glyph.moveBy = (12, 0)

Coloring glyphs by range is also straightforward when using this object:

import random

def get_random_color():
    r = random.random()
    g = random.random()
    b = random.random()
    return (r, g, b, 1)

for range in font.smufl.ranges:
    color = get_random_color()
    for glyph in range:
        glyph.mark = color

The Range object provides range-specific glyph management as well as access to a SMuFL range’s name, description, start and end attributes.

The Range.glyphs attribute returns a tuple of Glyph instances belonging to the range, while Range.keys() returns a tuple of their canonical SMuFL names.

Note

Attempting to insert a glyph whose Unicode value is not within the range’s bounds will raise a ValueError.

Ranges are read-only by default, but may be made editable with the ranges.editable configuration setting. When enabled, custom ranges may be added using the Smufl.newRange() method:

>>> font.smufl.newRange(  
...     "myRange", 0xF500, 0xF50F, "A Range of custom glyphs."
... )

Glyph Classes

Another way to work with groups of glyphs in SMufoLib is by using SMuFL classes. When imported or set, the Smufl.classes attribute stores the class names associated with each glyph:

>>> glyph = font["uniE260"]  # accidentalFlat
>>> glyph.smufl.classes
('accidentals', 'accidentalsSagittalMixed',
'accidentalsStandard', 'combiningStaffPositions')

This information can be used to collect glyphs based on their combined class membership:

for glyph in font:
    classes = glyph.smufl.classes
    if "accidentalsStandard" in classes and "accidentalsSagittalMixed" in classes:
        ...

The Smufl.classMembers() method provides a convenient way to collect all glyph members of the specified class:

>>> glyph.smufl.classMembers("accidentalsStandard")
(<Glyph 'uniE260' ['accidentalFlat'] ('public.default') at ...>,
<Glyph 'uniE266' ['accidentalTripleFlat'] ('public.default') at ...>,
<Glyph 'uniE267' ['accidentalNaturalFlat'] ('public.default') at ...>)

By default class names are limited to those specified in the SMuFL specification. To allow custom class names, disable the classes.strict configuration setting.

Engraving Defaults

Engraving defaults are managed by their own appropriately named EngravingDefaults object, accessed with the Smufl.engravingDefaults attribute:

>>> font.smufl.engravingDefaults
<EngravingDefaults in font 'MyFont Regular'
path='/path/to/MyFont.ufo' auto=True at ...>

Each setting has its own attribute within this object:

>>> ed = font.smufl.engravingDefaults
>>> ed.stemThickness = 30
>>> ed.stemThickness
30

Engraving defaults are calculated automatically from corresponding glyphs by default – provided that these glyphs exist. As an example, the value for hairpinThickness is based on the shape of the glyph U+E53E (dynamicCrescendoHairpin). See ENGRAVING_DEFAULTS_MAPPING for a full list of attributes and their corresponding glyphs.

Override the automatic calculations by setting the attributes to a value other than None.

To turn the feature off entirely, disable engravingDefaults.auto. See Configuring SMufoLib for more information about how to customize SMufoLib’s behavior.

Engraving defaults are available in either font units or staff spaces. See Changing Measurement Units for more information.

Anchors

SMufoLib does not currently provide its own anchor object, but a SMuFL specific representation of a glyph’s anchors is available from the Smufl.anchors attribute:

>>> glyph = font["uniE240"]  # flag8thUp
>>> glyph.smufl.anchors
{'graceNoteSlashNE': (321, -199),
'graceNoteSlashSW': (-161, -614),
'stemUpNW': (0, -10)}

Anchor coordinates are available in either font units or staff spaces. See Changing Measurement Units for more information.

Anchors may be imported from another font’s metadata file using the importAnchors script. SMufoLib also provides the diagnostics script checkAnchors to keep track of missing or superfluous SMuFL glyph anchors in a font. See Running Scripts for more information.

Note

Only anchors with names specific to SMuFL are accessible through the Smufl object’s anchors attribute. See ANCHOR_NAMES for a full set of available SMuFL anchors.

Glyph Metrics and Dimensions

Similarly to anchors, the Smufl class also provides a SMuFL-specific dict representation of the glyph bounding box:

>>> glyph = font["uniE050"]  # gClef
>>> glyph.smufl.bBox
{'bBoxSW': (0, -634), 'bBoxNE': (648, 1167)}

Even the glyph advance width is available as Smufl.advanceWidth:

>>> glyph.smufl.advanceWidth = 648
>>> glyph.smufl.advanceWidth
648

It differs from the usual Glyph.width in optionally providing the value in staff spaces (see Changing Measurement Units).

Ligatures and Stylistic Alternates

Ligatures have their component glyphs readily available with the componentGlyphs attribute:

>>> ligature = font["uniE26A_uniE260_uniE26B"]
>>> ligature.smufl.componentGlyphs
(<Glyph 'uniE26A' ['accidentalParensLeft'] ('public.default') at ...>,
<Glyph 'uniE260' ['accidentalFlat'] ('public.default') at ...>,
<Glyph 'uniE26B' ['accidentalParensRight'] ('public.default') at ...>)

Alternately, components can be listed by their canonical SMuFL names with the componentNames attribute:

>>> ligature.smufl.componentNames
('accidentalParensLeft', 'accidentalFlat', 'accidentalParensRight')

The alternateGlyphs and alternateNames attribute similarly provide convenient access to a glyph’s stylistic alternates, by Glyph object and SMuFL name respectively.

A SMuFL-specific metadata representation of the same alternates can be retrieved with the alternates attribute:

>>> glyph = font["uniE050"]  # gClef
>>> glyph.smufl.alternates
({'codepoint': 'U+F472', 'name': 'gClefSmall'},)

The inverse base glyph is also accessible through the base attribute:

>>> alternate = font["uniE050.ss01"]
>>> alternate.smufl.base
<Glyph 'uniE050' ['gClef'] ('public.default') at ...>

The glyph name suffix is a common characteristic of different types of OpenType alternates and sets, and may therefore sometimes be necessary to isolate. This is what the suffix attribute is for:

>>> glyph = font["uniE050.ss01"]
>>> glyph.smufl.suffix
'ss01'

Important

The attributes in this section demand strict adherence to SMuFL’s glyph naming standards. See this note about glyph naming for details.

Status Indicators

The Smufl class includes a set of convenient boolean checks to determine a glyph’s membership status:

isLigature

Return True if glyph is ligature.

isMember

Return True if glyph is either recommended or optional.

isOptional

Return True if glyph is optional.

isRecommended

Return True if glyph is recommended.

isSalt

Return True if glyph is stylistic alternate.

isSet

Return True if glyph is stylistic set member.

For instance, checking if a glyph is within the accepted range for recommended glyphs in SMuFL is as easy as:

>>> if glyph.smufl.isRecommended:
...     ...

Changing Measurement Units

You can get or set engraving defaults, anchor coordinates, glyph bounds and advance widths in either font units or staff spaces – whatever suits your workflow. By default, all values are expressed in font units unless changed. To switch to staff spaces, set either EngravingDefaults.spaces or Smufl.spaces to True, e.g.:

>>> ed = font.smufl.engravingDefaults
>>> ed.spaces = True
>>> ed.stemThickness
0.12
>>> ed.stemThickness = 0.14
>>> ed.spaces = False
>>> ed.stemThickness
35

Note

  • Setting font.smufl.engravingDefaults.spaces = True is equivalent to setting font.smufl.spaces = True, so either one will affect all relevant attributes across the entire library.

  • This setting is stored in the font’s metadata and will persist when saving the font.

The Smufl class also provides methods to convert a given value between the different units of measurement. Use the toSpaces() method to convert a font units value to staff spaces, and the toUnits() to do the opposite:

>>> font.smufl.toSpaces(250)
1.0
>>> font.smufl.toUnits(1.0)
250

Important

Conversion to staff spaces depends on the font’s units-per-em (UPM) value. Make sure BaseInfo.unitsPerEm is set correctly for the conversion to work as expected, e.g.:

>>> font.info.unitsPerEm = 1000

Running Scripts

SMufoLib comes bundled with several useful scripts for building SMuFL metadata files, importing anchors, setting identification attributes and more.

Scripts may be run either directly from the command line or imported as regular python modules, passing in any arguments in the familiar manner to each platform.

As an example, check for missing or superfluous SMuFL anchors and mark discrepant glyphs by running the Check Anchors script with the check-anchors --mark flag directly from the command line:

$ check-anchors path/to/my/font.ufo --mark

Positional arguments and available options can be listed by running the help command on the script:

$ check-anchors --help

usage: check-anchors [-h] [-F FONTDATA] [-m] [-c COLOR COLOR COLOR COLOR] [-v] font

Check SMuFL glyph anchors against reference font metadata.

positional arguments:
  font                  path to UFO file

options:
  -h, --help            show this help message and exit
  -F FONTDATA, --font-data FONTDATA
                        path to font metadata file
  -m, --mark            apply defined color values to objects
  -c COLOR COLOR COLOR COLOR, --color COLOR COLOR COLOR COLOR
                        list of RGBA color values
  -v, --verbose         make output verbose

Alternatively, scripts can be imported as modules in Python:

>>> from bin.checkAnchors import checkAnchors
>>> checkAnchors(mark=True)  

This imports and executes the script’s program function, checkAnchors(), from the script module of the same name.

Making Metadata Requests

SMufoLib provides a request module to handle web requests and metadata file operations, facilitating access to updated SMuFL data. Most of this functionality is handled by the module’s Request class, which may be imported directly:

>>> from smufolib import Request

Standard Metadata Requests

The different metadata support files published under the SMuFL standard, as well as the metadata file for SMuFL’s reference font, Bravura, can be easily retrieved using the appropriately named Request class methods:

classes

Retrieve classes metadata from configured paths.

glyphnames

Retrieve glyphnames metadata from configured paths.

ranges

Retrieve ranges metadata from configured paths.

font

Retrieve font metadata from configured paths.

By default, these methods return a parsed Python dict. Retrieve a raw str response instead by setting decode=False:

>>> text = Request.classes(decode=False)

Paths and Fallbacks

Request can handle both URL and filesystem paths. Pass the path as the first argument:

>>> file = Request("path/to/file.json")  
>>> file = Request("https://path/to/file.json")  

You can also combine a remote URL with a local fallback file. This enables automatic fallback to a local copy if the remote request fails due to a connection error:

>>> file = Request("https://path/to/file.json", "path/to/file.json")  

Note

A fallback will only be attempted if a URLError is raised. If the primary path points to a local file and it fails, the error will be raised immediately.

Raw Output

Similarly to the well known HTTP library Requests <https://requests.readthedocs.io/en/latest/>_, SMufoLib’s Request object provides two properties for accessing raw response data:

  • Use the text attribute to get a decoded str:

    >>> data = Request("path/to/file.json").text  
    
  • Use the content attribute to get the raw bytes content:

    >>> data = Request("path/to/file.json").content  
    

Unless an encoding is explicitly specified, text responses will be decoded using UTF-8.

Parsing JSON Files

If the file is a JSON file, use the built-in json() method to parse it:

>>> data = Request("https://path/to/file.json").json()  

Writing JSON Files

The request module also provides a helper function to simplify the logic concerned with writing JSON data to a file. Using the writeJson() function this is as simple as:

>>> from smufolib.request import writeJson  
>>> jsonDict = {"font": "MyFont"}  
>>> writeJson("path/to/file.json", jsonDict)  

Building Command Line Interfaces

The cli module provides a flexible and developer-friendly framework, based on Python’s argparse module, for building command-line tools that operate on SMuFL-based font data and metadata. It is designed to streamline the development of scripts by offering consistent argument definitions, reusable parsing logic, and integration with the rest of the smufolib ecosystem.

By using the commonParser() utility and the pre-configured CLI_ARGUMENTS, you can easily construct robust and consistent parsers for your own scripts.

See the CLI Framework section of the API documentation for a complete list of available arguments and their default flags.

Features

  • A shared set of standardized CLI arguments covering common SMuFL workflows.

  • commonParser() utility to quickly construct a parser with selected arguments.

  • Support for custom help messages and default values.

  • Compatibility with extended help formatters for improved --help output.

  • Type-safe conversions for inputs like JSON strings, RGBA colors, or font file paths.

Creating A Parser

To create a simple parser using only predefined arguments:

from smufolib import commonParser

parser = commonParser(
   "font", "clear", includeOptionals=False,
   description="My SMuFL utility", addHelp=True
   )

Note

commonParser() automatically converts argument names from camelCase to kebab-case (e.g., includeOptionals becomes --include-optionals) to maintain consistency with common command-line interfaces.

Combining Parsers

If you want to define your own additional custom arguments, you can combine commonParser() with a separate argparse.ArgumentParser object by passing the function output as a list to the parents parameter of the class:

import argparse
from smufolib import commonParser

args = commonParser("font", clear=True, addHelp=False)
parser = argparse.ArgumentParser(
    parents=[args],
    description="showcase commonParser"
)
parser.add_argument(
    "-m", "--my-argument",
    action="store_true",
    help="do something",
    dest="myArgument"
)

Important

When cobining parsers, the addHelp argument must be sett to False, otherwise the parser will fail (see the parents section of the argparse.ArgumentParser documentation).

To avoid conflicts between standard and custom arguments, you can modify the short flag definitions for each argument in the cli.shortFlags section of smufolib.cfg.

Creating Help Formatters

The CLI framework also supports custom help formatting by combining the different help formatters available in the argparse module:

Use the createHelpFormatter() function to combine the formatters you want when creating your parser:

import argparse
from smufolib import cli

formatter = cli.createHelpFormatter(
    ("RawTextHelpFormatter", "ArgumentDefaultsHelpFormatter")
)
parser = argparse.ArgumentParser(
    formatter_class=formatter,
    description="Process SMuFL metadata"
)

Using the Utility Modules

SMufoLib includes a whole host of utility functions, spread across several modules. The sections below summarize some of the most useful features for external use.

Conversion

The converters module provides helper functions for converting between different measurement formats, Unicode codepoints, and naming styles. Functions include:

convertMeasurement

Convert between units of measurement based on UPM size.

toDecimal

Convert formatted unicode or uni name to decimal codepoint.

toUniHex

Convert decimal codepoint or uni-name to formatted Unicode hex.

toUniName

Convert decimal codepoint or Unicode notation string to uni-name.

toNumber

Convert numeric string to number.

toIntIfWhole

Convert float without a fractional part to integer.

toKebab

Convert camelCase to kebab-case.

Errors and Warnings

The error module provides functions to generate error messages, check types, and suggest corrections for invalid values. It includes a dictionary of ERROR_TEMPLATES to ensure streamlined and consistent error reporting. Functions include:

generateErrorMessage

Generate an error message from a template and keyword arguments.

generateTypeError

Generate a TypeError message.

validateType

Validate if a value matches any of the valid types.

suggestValue

Validate value and suggests a valid close match.

Contours and Measuring

The rulers module provides functions to extract glyph contours, segments and points and calculate glyph geometry used in engraving analysis. Functions include:

Contour Tools

getGlyphContours

Return all contours in glyph, including component references.

getGlyphSegments

Return all segments in glyph, matching given types.

getGlyphPoints

Return all points in glyph, matching given types.

getParentSegment

Get the segment which the given point belongs to.

combineBounds

Combine a list of bounds into one bounding box.

Rulers

glyphBoundsHeight

Return height of the glyph's bounding box.

glyphBoundsWidth

Return width of the glyph's bounding box.

glyphBoundsXMinAbs

Return absolute value of glyph's xMin bound.

xDistanceStemToDot

Measure horizontal distance between a stem and a dot contour.

xDistanceBetweenContours

Measure horizontal distance between two adjacent contours in a glyph.

yDistanceBetweenContours

Measure vertical distance between two adjacent contours in a glyph.

xStrokeWidthAtOrigin

Measure horizontal distance between aligned points closest to origin.

yStrokeWidthAtMinimum

Measure vertical distance between aligned low-points in the glyph.

wedgeArmStrokeWidth

Measure thickness of arm in a wedge-shaped glyph.

Boolean Checks

areAlligned

Check if the specified points are aligned on axis.

hasHorizontalOffCurve

Check if the given point has a predominantly horizontal off-curve.

hasVerticalOffCurve

Check if the given point has a predominantly vertical off-curve.

Footnotes