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:
Current working directory (
./smufolib.cfg)Home directory (
~/.smufolib.cfg) as returned byos.path.expanduser()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.cfgAdd this to your shell startup file (e.g.,
~/.zshrcor~/.bashrc) to make it persistent.On Windows, use the
setcommand: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 throughFont.smuflorGlyph.smufl.FontParts maintains consistent references to parent-level objects. As a result, font-specific
Smuflattributes 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 atuplecontaining all SMuFL ranges in the font.glyph.smufl.ranges– Returns atuplecontaining 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:
Return |
|
Return |
|
Return |
|
Return |
|
Return |
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 = Trueis equivalent to settingfont.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:
Retrieve |
|
Retrieve |
|
Retrieve |
|
Retrieve |
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
textattribute to get a decodedstr:>>> data = Request("path/to/file.json").text
Use the
contentattribute to get the rawbytescontent:>>> 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
--helpoutput.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:
Convert between units of measurement based on UPM size. |
|
Convert formatted unicode or uni name to decimal codepoint. |
|
Convert decimal codepoint or uni-name to formatted Unicode hex. |
|
Convert decimal codepoint or Unicode notation string to uni-name. |
|
Convert numeric string to number. |
|
Convert float without a fractional part to integer. |
|
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:
Generate an error message from a template and keyword arguments. |
|
Generate a |
|
Validate if a value matches any of the valid types. |
|
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
Return all contours in |
|
Return all segments in |
|
Return all points in |
|
Get the segment which the given point belongs to. |
|
Combine a list of bounds into one bounding box. |
Rulers
Return height of the glyph's bounding box. |
|
Return width of the glyph's bounding box. |
|
Return absolute value of glyph's xMin bound. |
|
Measure horizontal distance between a stem and a dot contour. |
|
Measure horizontal distance between two adjacent contours in a glyph. |
|
Measure vertical distance between two adjacent contours in a glyph. |
|
Measure horizontal distance between aligned points closest to origin. |
|
Measure vertical distance between aligned low-points in the glyph. |
|
Measure thickness of arm in a wedge-shaped glyph. |
Boolean Checks
Check if the specified |
|
Check if the given point has a predominantly horizontal off-curve. |
|
Check if the given point has a predominantly vertical off-curve. |