Conventions

These are conventions that are used throughout the UFO.

Allowed Content Types

UFOs may contain only plain files or directories. Symlinks are not allowed anywhere, as they increase the amount of edge cases implementations have to handle without clear benefit, while complicating data exchange across different platforms.

XML Property Lists

XML property lists are used throughout this specification. A DTD is available.

It is recommended, but not required, that dictionaries in property lists be written in ascending order based on the keys of the dictionary. This makes UFO usage more convenient in revision control systems, among other things.

Coordinate System

Coordinates and transformations in the UFO are relative to the origin (x=0 and y=0), the positive x-axis points towards the right and the positive y-axis points up, unless otherwise noted.

Data Types

These types are used throughout the UFO specification.

While efforts have been made to define how many of the values in various UFO files relate to binary files such as OpenType, it is the responsibility of authoring tools to ensure the UFO values convert to said binary values appropriately. For example, in OpenType many fields require unsigned short integers, but a UFO may contain values for these fields that are greater than 65,535.

string

A string is specified with one or more characters encoded with the encoding scheme defined for the XML file that the string belongs to. The range of characters allowed in a given string field in the UFO may be limited. In these cases the specification defines the available subset.

integer

An integer is specified with an optional sign character (“+” or “-“) followed by one or more digits “0” to “9”. There is not limit to the number of integer digits. If a sign character is not present, the number is non-negative.

non-negative integer

An integer greater than or equal to zero. See above for integer notation.

float

A float is specified with an optional sign character (“+” or “-“) followed by zero or more digits “0” to “9” followed by a period “.” followed by one or more digits “0” to “9”. There is no limit to the number of digits before or after the decimal. If a sign character is not present, the number is non-negative. Only normal and zero float types are allowed; NaN, infinite, and subnormal floats are disallowed.

Authoring tools should not write floats that can be represented losslessly as integers unless the specification requires a float.

non-negative float

A float greater than or equal to zero. See above for float notation.

list

An collection of items. The items are ordered, however the order is insignificant unless otherwise stated for a particular list in this specification.

dictionary

A dictionary is an unordered associative array mapping keys to values. In the XML property lists used throughout the UFO, the keys must be strings. Values may be any type, though the options are defined throughout this specification.

color definition

A color definition is defined as a string containing a comma-separated sequence of four integers or floats between 0 and 1. White space characters are allowed around the numerical values. The values in the string define the red, green, blue and alpha components of the color. The color is always specified in the sRGB color space.

Examples

string red component green component blue component alpha component
1,0,0,1 1.0 0 0 1.0
0,.5,0,.5 0 0.5 0 0.5

control characters

Any character with a Unicode value in the ranges of U+0000—U+001F (C0 controls), U+007F (delete), and U+0080—U+009F (C1 controls).

Reverse Domain Naming Schemes

In several places in the UFO the reverse domain naming system is recommended for creating unique keys and ids. To make a reverse domain, reverse the relevant Internet domain. For example, if the Internet domain is unifiedfontobject.org, the reverse domain name would be org.unifiedfontobject. Further extensions to make the string unique may be added. For example, org.unifiedfontobject.MySpecialTool.

The name public.*, where * represents an arbitrary string of one or more characters, is reserved for use by data structures that are part of the UFO specification.

Common User Name to File Name Algorithm

There is no standard user name to file name conversion. The algorithm below is an example of a common conversion process. It has been designed to avoid name clashes and work with common file systems. It applies the following rules:

  1. Filenames must be unique.
  2. Filenames must be case insensitive.
  3. Filenames may use any character that can be represented by UTF-8 except the ones that are reserved across a variety of popular filesystems.
  4. Filenames must be no longer than 255 characters.
  5. Filenames, regardless of extension and case, must not match any of the Windows reserved file names.
  6. Filenames must be valid Unicode strings.

Examples

glyph name file name
a a
A A_
AE A_E_
Ae A_e
ae ae
aE aE_
a.alt a.alt
A.alt A_.alt
A.Alt A_.A_lt
A.aLt A_.aL_t
A.alT A_.alT_
T_H T__H_
T_h T__h
t_h t_h
F_F_I F__F__I_
f_f_i f_f_i
Aacute_V.swash A_acute_V_.swash
.notdef _notdef
con _con
CON C_O_N_
con.alt _con.alt
alt.con alt._con

Example implementation:

illegalCharacters = "\" * + / : < > ? [ \ ] | \0".split(" ")
illegalCharacters += [chr(i) for i in range(1, 32)]
illegalCharacters += [chr(0x7F)]
reservedFileNames = "CON PRN AUX CLOCK$ NUL A:-Z: COM1".lower().split(" ")
reservedFileNames += "LPT1 LPT2 LPT3 COM2 COM3 COM4".lower().split(" ")
maxFileNameLength = 255

def userNameToFileName(userName, existing=[], prefix="", suffix=""):
    """
    existing should be a case-insensitive list
    of all existing file names.

    >>> userNameToFileName(u"a")
    u'a'
    >>> userNameToFileName(u"A")
    u'A_'
    >>> userNameToFileName(u"AE")
    u'A_E_'
    >>> userNameToFileName(u"Ae")
    u'A_e'
    >>> userNameToFileName(u"ae")
    u'ae'
    >>> userNameToFileName(u"aE")
    u'aE_'
    >>> userNameToFileName(u"a.alt")
    u'a.alt'
    >>> userNameToFileName(u"A.alt")
    u'A_.alt'
    >>> userNameToFileName(u"A.Alt")
    u'A_.A_lt'
    >>> userNameToFileName(u"A.aLt")
    u'A_.aL_t'
    >>> userNameToFileName(u"A.alT")
    u'A_.alT_'
    >>> userNameToFileName(u"T_H")
    u'T__H_'
    >>> userNameToFileName(u"T_h")
    u'T__h'
    >>> userNameToFileName(u"t_h")
    u't_h'
    >>> userNameToFileName(u"F_F_I")
    u'F__F__I_'
    >>> userNameToFileName(u"f_f_i")
    u'f_f_i'
    >>> userNameToFileName(u"Aacute_V.swash")
    u'A_acute_V_.swash'
    >>> userNameToFileName(u".notdef")
    u'_notdef'
    >>> userNameToFileName(u"con")
    u'_con'
    >>> userNameToFileName(u"CON")
    u'C_O_N_'
    >>> userNameToFileName(u"con.alt")
    u'_con.alt'
    >>> userNameToFileName(u"alt.con")
    u'alt._con'
    """
    # the incoming name must be a unicode string
    assert isinstance(userName, unicode),
        "The value for userName must be a unicode string."
    # establish the prefix and suffix lengths
    prefixLength = len(prefix)
    suffixLength = len(suffix)
    # replace an initial period with an _
    # if no prefix is to be added
    if not prefix and userName[0] == ".":
        userName = "_" + userName[1:]
    # filter the user name
    filteredUserName = []
    for character in userName:
        # replace illegal characters with _
        if character in illegalCharacters:
            character = "_"
        # add _ to all non-lower characters
        elif character != character.lower():
            character += "_"
        filteredUserName.append(character)
    userName = "".join(filteredUserName)
    # clip to 255
    sliceLength = maxFileNameLength - prefixLength - suffixLength
    userName = userName[:sliceLength]
    # test for illegal files names
    parts = []
    for part in userName.split("."):
        if part.lower() in reservedFileNames:
            part = "_" + part
        parts.append(part)
    userName = ".".join(parts)
    # test for clash
    fullName = prefix + userName + suffix
    if fullName.lower() in existing:
        fullName = handleClash1(userName, existing, prefix, suffix)
    # finished
    return fullName

def handleClash1(userName, existing=[], prefix="", suffix=""):
    """
    existing should be a case-insensitive list
    of all existing file names.

    >>> prefix = ("0" * 5) + "."
    >>> suffix = "." + ("0" * 10)
    >>> existing = ["a" * 5]

    >>> e = list(existing)
    >>> handleClash1(userName="A" * 5, existing=e,
    ...     prefix=prefix, suffix=suffix)
    '00000.AAAAA000000000000001.0000000000'

    >>> e = list(existing)
    >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix)
    >>> handleClash1(userName="A" * 5, existing=e,
    ...     prefix=prefix, suffix=suffix)
    '00000.AAAAA000000000000002.0000000000'

    >>> e = list(existing)
    >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix)
    >>> handleClash1(userName="A" * 5, existing=e,
    ...     prefix=prefix, suffix=suffix)
    '00000.AAAAA000000000000001.0000000000'
    """
    # if the prefix length + user name length + suffix length + 15 is at
    # or past the maximum length, silce 15 characters off of the user name
    prefixLength = len(prefix)
    suffixLength = len(suffix)
    if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength:
        l = (prefixLength + len(userName) + suffixLength + 15)
        sliceLength = maxFileNameLength - l
        userName = userName[:sliceLength]
    finalName = None
    # try to add numbers to create a unique name
    counter = 1
    while finalName is None:
        name = userName + str(counter).zfill(15)
        fullName = prefix + name + suffix
        if fullName.lower() not in existing:
            finalName = fullName
            break
        else:
            counter += 1
        if counter >= 999999999999999:
            break
    # if there is a clash, go to the next fallback
    if finalName is None:
        finalName = handleClash2(existing, prefix, suffix)
    # finished
    return finalName

def handleClash2(existing=[], prefix="", suffix=""):
    """
    existing should be a case-insensitive list
    of all existing file names.

    >>> prefix = ("0" * 5) + "."
    >>> suffix = "." + ("0" * 10)
    >>> existing = [prefix + str(i) + suffix for i in range(100)]

    >>> e = list(existing)
    >>> handleClash2(existing=e, prefix=prefix, suffix=suffix)
    '00000.100.0000000000'

    >>> e = list(existing)
    >>> e.remove(prefix + "1" + suffix)
    >>> handleClash2(existing=e, prefix=prefix, suffix=suffix)
    '00000.1.0000000000'

    >>> e = list(existing)
    >>> e.remove(prefix + "2" + suffix)
    >>> handleClash2(existing=e, prefix=prefix, suffix=suffix)
    '00000.2.0000000000'
    """
    # calculate the longest possible string
    maxLength = maxFileNameLength - len(prefix) - len(suffix)
    maxValue = int("9" * maxLength)
    # try to find a number
    finalName = None
    counter = 1
    while finalName is None:
        fullName = prefix + str(counter) + suffix
        if fullName.lower() not in existing:
            finalName = fullName
            break
        else:
            counter += 1
        if counter >= maxValue:
            break
    # raise an error if nothing has been found
    if finalName is None:
        raise NameTranslationError("No unique name could be found.")
    # finished
    return finalName

Identifiers

Identifiers are optional attributes of several objects in the UFO. These identifiers are required to be unique within certain contexts as defined on a per object basis throughout this specification. Identifiers are specified as a string between one and 100 characters long. All characters must be in the printable ASCII range, 0x20 to 0x7E.

There is no standard identifier generation algorithm. Random strings, simple numbers, UUIDs, time stamps and more may be used. An example algorithm for generating identifiers that are random strings is detailed below.

Example Algorithm

This algorithm is designed to generate a unique identifier that is a unique string consisting of 10 characters from 0-9, A-Z and a-z. This allows a glyph to contain 1062 points that follow this identifier naming scheme.

Example implementation:

import random

characters = list("0123456789")
characters += list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
characters += list("abcdefghijklmnopqrstuvwxyz")
identifierLength = 10
identifierRange = range(identifierLength)

def makeRandomIdentifier(existing, recursionDepth=0):
    if recursionDepth >= 50:
        raise NotImplementedError,\
            "Failed 50 times in a row to create a unique id. Sorry."
    identifier = []
    for i in identifierRange:
        c = random.choice(characters)
        identifier.append(c)
    identifier = "".join(identifier)
    if identifier in existing:
        return makeRandomIdentifier(existing, recursionDepth+1)
    else:
        return identifier