Source code for fontParts.base.font

import os
from fontTools import ufoLib
from fontParts.base.errors import FontPartsError
from fontParts.base.base import dynamicProperty, InterpolationMixin
from fontParts.base.layer import _BaseGlyphVendor
from fontParts.base import normalizers
from fontParts.base.compatibility import FontCompatibilityReporter
from fontParts.base.deprecated import DeprecatedFont, RemovedFont


[docs]class BaseFont( _BaseGlyphVendor, InterpolationMixin, DeprecatedFont, RemovedFont ): """ A font object. This object is almost always created with one of the font functions in :ref:`fontparts-world`. """ def __init__(self, pathOrObject=None, showInterface=True): """ When constructing a font, the object can be created in a new file, from an existing file or from a native object. This is defined with the **pathOrObjectArgument**. If **pathOrObject** is a string, the string must represent an existing file. If **pathOrObject** is an instance of the environment's unwrapped native font object, wrap it with FontParts. If **pathOrObject** is None, create a new, empty font. If **showInterface** is ``False``, the font should be created without graphical interface. The default for **showInterface** is ``True``. """ super(BaseFont, self).__init__(pathOrObject=pathOrObject, showInterface=showInterface) def _reprContents(self): contents = [ "'%s %s'" % (self.info.familyName, self.info.styleName), ] if self.path is not None: contents.append("path=%r" % self.path) return contents # ---- # Copy # ---- copyAttributes = ( "info", "groups", "kerning", "features", "lib", "layerOrder", "defaultLayerName", "glyphOrder" )
[docs] def copy(self): """ Copy the font into a new font. :: >>> copiedFont = font.copy() This will copy: * info * groups * kerning * features * lib * layers * layerOrder * defaultLayerName * glyphOrder * guidelines """ return super(BaseFont, self).copy()
def copyData(self, source): """ Copy data from **source** into this font. Refer to :meth:`BaseFont.copy` for a list of values that will be copied. """ # set the default layer name self.defaultLayer.name = source.defaultLayerName for layerName in source.layerOrder: if layerName in self.layerOrder: layer = self.getLayer(layerName) else: layer = self.newLayer(layerName) layer.copyData(source.getLayer(layerName)) for guideline in self.guidelines: self.appendGuideline(guideline) super(BaseFont, self).copyData(source) # --------------- # File Operations # --------------- # Initialize
[docs] def _init(self, pathOrObject=None, showInterface=True, **kwargs): """ Initialize this object. This should wrap a native font object based on the values for **pathOrObject**: +--------------------+---------------------------------------------------+ | None | Create a new font. | +--------------------+---------------------------------------------------+ | string | Open the font file located at the given location. | +--------------------+---------------------------------------------------+ | native font object | Wrap the given object. | +--------------------+---------------------------------------------------+ If **showInterface** is ``False``, the font should be created without graphical interface. Subclasses must override this method. """ self.raiseNotImplementedError()
# path path = dynamicProperty( "base_path", """ The path to the file this object represents. :: >>> print font.path "/path/to/my/font.ufo" """ ) def _get_base_path(self): path = self._get_path() if path is not None: path = normalizers.normalizeFilePath(path) return path
[docs] def _get_path(self, **kwargs): """ This is the environment implementation of :attr:`BaseFont.path`. This must return a :ref:`type-string` defining the location of the file or ``None`` indicating that the font does not have a file representation. If the returned value is not ``None`` it will be normalized with :func:`normalizers.normalizeFilePath`. Subclasses must override this method. """ self.raiseNotImplementedError()
# save
[docs] def save(self, path=None, showProgress=False, formatVersion=None, fileStructure=None): """ Save the font to **path**. >>> font.save() >>> font.save("/path/to/my/font-2.ufo") If **path** is None, use the font's original location. The file type must be inferred from the file extension of the given path. If no file extension is given, the environment may fall back to the format of its choice. **showProgress** indicates if a progress indicator should be displayed during the operation. Environments may or may not implement this behavior. **formatVersion** indicates the format version that should be used for writing the given file type. For example, if 2 is given for formatVersion and the file type being written if UFO, the file is to be written in UFO 2 format. This value is not limited to UFO format versions. If no format version is given, the original format version of the file should be preserved. If there is no original format version it is implied that the format version is the latest version for the file type as supported by the environment. **fileStructure** indicates the file structure of the written ufo. The **fileStructure** can either be None, 'zip' or 'package', None will use the existing file strucure or the default one for unsaved font. 'package' is the default file structure and 'zip' will save the font to .ufoz. .. note:: Environments may define their own rules governing when a file should be saved into its original location and when it should not. For example, a font opened from a compiled OpenType font may not be written back into the original OpenType font. """ if path is None and self.path is None: raise IOError(("The font cannot be saved because no file " "location has been given.")) if path is not None: path = normalizers.normalizeFilePath(path) showProgress = bool(showProgress) if formatVersion is not None: formatVersion = normalizers.normalizeFileFormatVersion( formatVersion) if fileStructure is not None: fileStructure = normalizers.normalizeFileStructure(fileStructure) self._save(path=path, showProgress=showProgress, formatVersion=formatVersion, fileStructure=fileStructure)
[docs] def _save(self, path=None, showProgress=False, formatVersion=None, fileStructure=None, **kwargs): """ This is the environment implementation of :meth:`BaseFont.save`. **path** will be a :ref:`type-string` or ``None``. If **path** is not ``None``, the value will have been normalized with :func:`normalizers.normalizeFilePath`. **showProgress** will be a ``bool`` indicating if the environment should display a progress bar during the operation. Environments are not *required* to display a progress bar even if **showProgess** is ``True``. **formatVersion** will be :ref:`type-int-float` or ``None`` indicating the file format version to write the data into. It will have been normalized with :func:`normalizers.normalizeFileFormatVersion`. Subclasses must override this method. """ self.raiseNotImplementedError()
# close
[docs] def close(self, save=False): """ Close the font. >>> font.close() **save** is a boolean indicating if the font should be saved prior to closing. If **save** is ``True``, the :meth:`BaseFont.save` method will be called. The default is ``False``. """ if save: self.save() self._close()
[docs] def _close(self, **kwargs): """ This is the environment implementation of :meth:`BaseFont.close`. Subclasses must override this method. """ self.raiseNotImplementedError()
# generate @staticmethod def generateFormatToExtension(format, fallbackFormat): """ +--------------+--------------------------------------------------------------------+ | mactype1 | Mac Type 1 font (generates suitcase and LWFN file) | +--------------+--------------------------------------------------------------------+ | macttf | Mac TrueType font (generates suitcase) | +--------------+--------------------------------------------------------------------+ | macttdfont | Mac TrueType font (generates suitcase with resources in data fork) | +--------------+--------------------------------------------------------------------+ | otfcff | PS OpenType (CFF-based) font (OTF) | +--------------+--------------------------------------------------------------------+ | otfttf | PC TrueType/TT OpenType font (TTF) | +--------------+--------------------------------------------------------------------+ | pctype1 | PC Type 1 font (binary/PFB) | +--------------+--------------------------------------------------------------------+ | pcmm | PC MultipleMaster font (PFB) | +--------------+--------------------------------------------------------------------+ | pctype1ascii | PC Type 1 font (ASCII/PFA) | +--------------+--------------------------------------------------------------------+ | pcmmascii | PC MultipleMaster font (ASCII/PFA) | +--------------+--------------------------------------------------------------------+ | ufo1 | UFO format version 1 | +--------------+--------------------------------------------------------------------+ | ufo2 | UFO format version 2 | +--------------+--------------------------------------------------------------------+ | ufo3 | UFO format version 3 | +--------------+--------------------------------------------------------------------+ | unixascii | UNIX ASCII font (ASCII/PFA) | +--------------+--------------------------------------------------------------------+ """ formatToExtension = dict( # mactype1=None, macttf=".ttf", macttdfont=".dfont", otfcff=".otf", otfttf=".ttf", # pctype1=None, # pcmm=None, # pctype1ascii=None, # pcmmascii=None, ufo1=".ufo", ufo2=".ufo", ufo3=".ufo", unixascii=".pfa", ) return formatToExtension.get(format, fallbackFormat)
[docs] def generate(self, format, path=None, **environmentOptions): """ Generate the font to another format. >>> font.generate("otfcff") >>> font.generate("otfcff", "/path/to/my/font.otf") **format** defines the file format to output. Standard format identifiers can be found in :attr:`BaseFont.generateFormatToExtension`: Environments are not required to support all of these and environments may define their own format types. **path** defines the location where the new file should be created. If a file already exists at that location, it will be overwritten by the new file. If **path** defines a directory, the file will be output as the current file name, with the appropriate suffix for the format, into the given directory. If no **path** is given, the file will be output into the same directory as the source font with the file named with the current file name, with the appropriate suffix for the format. Environments may allow unique keyword arguments in this method. For example, if a tool allows decomposing components during a generate routine it may allow this: >>> font.generate("otfcff", "/p/f.otf", decompose=True) """ import warnings if format is None: raise ValueError("The format must be defined when generating.") elif not isinstance(format, str): raise TypeError("The format must be defined as a string.") env = {} for key, value in environmentOptions.items(): valid = self._isValidGenerateEnvironmentOption(key) if not valid: warnings.warn("The %s argument is not supported " "in this environment." % key, UserWarning) env[key] = value environmentOptions = env ext = self.generateFormatToExtension(format, "." + format) if path is None and self.path is None: raise IOError(("The file cannot be generated because an " "output path was not defined.")) elif path is None: path = os.path.splitext(self.path)[0] path += ext elif os.path.isdir(path): if self.path is None: raise IOError(("The file cannot be generated because " "the file does not have a path.")) fileName = os.path.basename(self.path) fileName += ext path = os.path.join(path, fileName) path = normalizers.normalizeFilePath(path) return self._generate( format=format, path=path, environmentOptions=environmentOptions )
@staticmethod def _isValidGenerateEnvironmentOption(name): """ Any unknown keyword arguments given to :meth:`BaseFont.generate` will be passed to this method. **name** will be the name used for the argument. Environments may evaluate if **name** is a supported option. If it is, they must return `True` if it is not, they must return `False`. Subclasses may override this method. """ return False
[docs] def _generate(self, format, path, environmentOptions, **kwargs): """ This is the environment implementation of :meth:`BaseFont.generate`. **format** will be a :ref:`type-string` defining the output format. Refer to the :meth:`BaseFont.generate` documentation for the standard format identifiers. If the value given for **format** is not supported by the environment, the environment must raise :exc:`FontPartsError`. **path** will be a :ref:`type-string` defining the location where the file should be created. It will have been normalized with :func:`normalizers.normalizeFilePath`. **environmentOptions** will be a dictionary of names validated with :meth:`BaseFont._isValidGenerateEnvironmentOption` nd the given values. These values will not have been passed through any normalization functions. Subclasses must override this method. """ self.raiseNotImplementedError()
# ----------- # Sub-Objects # ----------- # info info = dynamicProperty( "base_info", """ The font's :class:`BaseInfo` object. >>> font.info.familyName "My Family" """ ) def _get_base_info(self): info = self._get_info() info.font = self return info
[docs] def _get_info(self): """ This is the environment implementation of :attr:`BaseFont.info`. This must return an instance of a :class:`BaseInfo` subclass. Subclasses must override this method. """ self.raiseNotImplementedError()
# groups groups = dynamicProperty( "base_groups", """ The font's :class:`BaseGroups` object. >>> font.groups["myGroup"] ["A", "B", "C"] """ ) def _get_base_groups(self): groups = self._get_groups() groups.font = self return groups
[docs] def _get_groups(self): """ This is the environment implementation of :attr:`BaseFont.groups`. This must return an instance of a :class:`BaseGroups` subclass. Subclasses must override this method. """ self.raiseNotImplementedError()
# kerning kerning = dynamicProperty( "base_kerning", """ The font's :class:`BaseKerning` object. >>> font.kerning["A", "B"] -100 """ ) def _get_base_kerning(self): kerning = self._get_kerning() kerning.font = self return kerning
[docs] def _get_kerning(self): """ This is the environment implementation of :attr:`BaseFont.kerning`. This must return an instance of a :class:`BaseKerning` subclass. Subclasses must override this method. """ self.raiseNotImplementedError()
def getFlatKerning(self): """ Get the font's kerning as a flat dictionary. """ return self._getFlatKerning() def _getFlatKerning(self): """ This is the environment implementation of :meth:`BaseFont.getFlatKerning`. Subclasses may override this method. """ kernOrder = { (True, True): 0, # group group (True, False): 1, # group glyph (False, True): 2, # glyph group (False, False): 3, # glyph glyph } def kerningSortKeyFunc(pair): g1, g2 = pair g1grp = g1.startswith("public.kern1.") g2grp = g2.startswith("public.kern2.") return (kernOrder[g1grp, g2grp], pair) flatKerning = dict() kerning = self.kerning groups = self.groups for pair in sorted(self.kerning.keys(), key=kerningSortKeyFunc): kern = kerning[pair] (left, right) = pair if left.startswith("public.kern1."): left = groups.get(left, []) else: left = [left] if right.startswith("public.kern2."): right = groups.get(right, []) else: right = [right] for r in right: for l in left: flatKerning[(l, r)] = kern return flatKerning # features features = dynamicProperty( "base_features", """ The font's :class:`BaseFeatures` object. >>> font.features.text "include(features/substitutions.fea);" """ ) def _get_base_features(self): features = self._get_features() features.font = self return features
[docs] def _get_features(self): """ This is the environment implementation of :attr:`BaseFont.features`. This must return an instance of a :class:`BaseFeatures` subclass. Subclasses must override this method. """ self.raiseNotImplementedError()
# lib lib = dynamicProperty( "base_lib", """ The font's :class:`BaseLib` object. >>> font.lib["org.robofab.hello"] "world" """ ) def _get_base_lib(self): lib = self._get_lib() lib.font = self return lib
[docs] def _get_lib(self): """ This is the environment implementation of :attr:`BaseFont.lib`. This must return an instance of a :class:`BaseLib` subclass. Subclasses must override this method. """ self.raiseNotImplementedError()
# tempLib tempLib = dynamicProperty( "base_tempLib", """ The font's :class:`BaseLib` object. :: >>> font.tempLib["org.robofab.hello"] "world" """ ) def _get_base_tempLib(self): lib = self._get_tempLib() lib.font = self return lib def _get_tempLib(self): """ This is the environment implementation of :attr:`BaseLayer.tempLib`. This must return an instance of a :class:`BaseLib` subclass. """ self.raiseNotImplementedError() # ----------------- # Layer Interaction # ----------------- layers = dynamicProperty( "base_layers", """ The font's :class:`BaseLayer` objects. >>> for layer in font.layers: ... layer.name "My Layer 1" "My Layer 2" """ ) def _get_base_layers(self): layers = self._get_layers() for layer in layers: self._setFontInLayer(layer) return tuple(layers)
[docs] def _get_layers(self, **kwargs): """ This is the environment implementation of :attr:`BaseFont.layers`. This must return an :ref:`type-immutable-list` containing instances of :class:`BaseLayer` subclasses. The items in the list should be in the order defined by :attr:`BaseFont.layerOrder`. Subclasses must override this method. """ self.raiseNotImplementedError()
# order layerOrder = dynamicProperty( "base_layerOrder", """ A list of layer names indicating order of the layers in the font. >>> font.layerOrder = ["My Layer 2", "My Layer 1"] >>> font.layerOrder ["My Layer 2", "My Layer 1"] """ ) def _get_base_layerOrder(self): value = self._get_layerOrder() value = normalizers.normalizeLayerOrder(value, self) return list(value) def _set_base_layerOrder(self, value): value = normalizers.normalizeLayerOrder(value, self) self._set_layerOrder(value)
[docs] def _get_layerOrder(self, **kwargs): """ This is the environment implementation of :attr:`BaseFont.layerOrder`. This must return an :ref:`type-immutable-list` defining the order of the layers in the font. The contents of the list must be layer names as :ref:`type-string`. The list will be normalized with :func:`normalizers.normalizeLayerOrder`. Subclasses must override this method. """ self.raiseNotImplementedError()
[docs] def _set_layerOrder(self, value, **kwargs): """ This is the environment implementation of :attr:`BaseFont.layerOrder`. **value** will be a **list** of :ref:`type-string` representing layer names. The list will have been normalized with :func:`normalizers.normalizeLayerOrder`. Subclasses must override this method. """ self.raiseNotImplementedError()
# default layer def _setFontInLayer(self, layer): if layer.font is None: layer.font = self defaultLayerName = dynamicProperty( "base_defaultLayerName", """ The name of the font's default layer. >>> font.defaultLayerName = "My Layer 2" >>> font.defaultLayerName "My Layer 2" """ ) def _get_base_defaultLayerName(self): value = self._get_defaultLayerName() value = normalizers.normalizeDefaultLayerName(value, self) return value def _set_base_defaultLayerName(self, value): value = normalizers.normalizeDefaultLayerName(value, self) self._set_defaultLayerName(value) def _get_defaultLayerName(self): """ This is the environment implementation of :attr:`BaseFont.defaultLayerName`. Return the name of the default layer as a :ref:`type-string`. The name will be normalized with :func:`normalizers.normalizeDefaultLayerName`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_defaultLayerName(self, value, **kwargs): """ This is the environment implementation of :attr:`BaseFont.defaultLayerName`. **value** will be a :ref:`type-string`. It will have been normalized with :func:`normalizers.normalizeDefaultLayerName`. Subclasses must override this method. """ self.raiseNotImplementedError() defaultLayer = dynamicProperty( "base_defaultLayer", """ The font's default layer. >>> layer = font.defaultLayer >>> font.defaultLayer = otherLayer """ )
[docs] def _get_defaultLayer(self): layer = self._get_base_defaultLayer() layer = normalizers.normalizeLayer(layer) return layer
[docs] def _set_defaultLayer(self, layer): layer = normalizers.normalizeLayer(layer) self._set_base_defaultLayer(layer)
def _get_base_defaultLayer(self): """ This is the environment implementation of :attr:`BaseFont.defaultLayer`. Return the default layer as a :class:`BaseLayer` object. The layer will be normalized with :func:`normalizers.normalizeLayer`. Subclasses must override this method. """ name = self.defaultLayerName layer = self.getLayer(name) return layer def _set_base_defaultLayer(self, value): """ This is the environment implementation of :attr:`BaseFont.defaultLayer`. **value** will be a :class:`BaseLayer`. It will have been normalized with :func:`normalizers.normalizeLayer`. Subclasses must override this method. """ self.defaultLayerName = value.name # get
[docs] def getLayer(self, name): """ Get the :class:`BaseLayer` with **name**. >>> layer = font.getLayer("My Layer 2") """ name = normalizers.normalizeLayerName(name) if name not in self.layerOrder: raise ValueError("No layer with the name '%s' exists." % name) layer = self._getLayer(name) self._setFontInLayer(layer) return layer
[docs] def _getLayer(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseFont.getLayer`. **name** will be a :ref:`type-string`. It will have been normalized with :func:`normalizers.normalizeLayerName` and it will have been verified as an existing layer. This must return an instance of :class:`BaseLayer`. Subclasses may override this method. """ for layer in self.layers: if layer.name == name: return layer
# new
[docs] def newLayer(self, name, color=None): """ Make a new layer with **name** and **color**. **name** must be a :ref:`type-string` and **color** must be a :ref:`type-color` or ``None``. >>> layer = font.newLayer("My Layer 3") The will return the newly created :class:`BaseLayer`. """ name = normalizers.normalizeLayerName(name) if name in self.layerOrder: layer = self.getLayer(name) if color is not None: layer.color = color return layer if color is not None: color = normalizers.normalizeColor(color) layer = self._newLayer(name=name, color=color) self._setFontInLayer(layer) return layer
[docs] def _newLayer(self, name, color, **kwargs): """ This is the environment implementation of :meth:`BaseFont.newLayer`. **name** will be a :ref:`type-string` representing a valid layer name. The value will have been normalized with :func:`normalizers.normalizeLayerName` and **name** will not be the same as the name of an existing layer. **color** will be a :ref:`type-color` or ``None``. If the value is not ``None`` the value will have been normalized with :func:`normalizers.normalizeColor`. This must return an instance of a :class:`BaseLayer` subclass that represents the new layer. Subclasses must override this method. """ self.raiseNotImplementedError()
# remove
[docs] def removeLayer(self, name): """ Remove the layer with **name** from the font. >>> font.removeLayer("My Layer 3") """ name = normalizers.normalizeLayerName(name) if name not in self.layerOrder: raise ValueError("No layer with the name '%s' exists." % name) self._removeLayer(name)
[docs] def _removeLayer(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseFont.removeLayer`. **name** will be a :ref:`type-string` defining the name of an existing layer. The value will have been normalized with :func:`normalizers.normalizeLayerName`. Subclasses must override this method. """ self.raiseNotImplementedError()
# insert
[docs] def insertLayer(self, layer, name=None): """ Insert **layer** into the font. :: >>> layer = font.insertLayer(otherLayer, name="layer 2") This will not insert the layer directly. Rather, a new layer will be created and the data from **layer** will be copied to to the new layer. **name** indicates the name that should be assigned to the layer after insertion. If **name** is not given, the layer's original name must be used. If the layer does not have a name, an error must be raised. The data that will be inserted from **layer** is the same data as documented in :meth:`BaseLayer.copy`. """ if name is None: name = layer.name name = normalizers.normalizeLayerName(name) if name in self: self.removeLayer(name) return self._insertLayer(layer, name=name)
def _insertLayer(self, layer, name, **kwargs): """ This is the environment implementation of :meth:`BaseFont.insertLayer`. This must return an instance of a :class:`BaseLayer` subclass. **layer** will be a layer object with the attributes necessary for copying as defined in :meth:`BaseLayer.copy` An environment must not insert **layer** directly. Instead the data from **layer** should be copied to a new layer. **name** will be a :ref:`type-string` representing a glyph layer. It will have been normalized with :func:`normalizers.normalizeLayerName`. **name** will have been tested to make sure that no layer with the same name exists in the font. Subclasses may override this method. """ if name != layer.name and layer.name in self.layerOrder: layer = layer.copy() layer.name = name dest = self.newLayer(name) dest.copyData(layer) return dest # duplicate def duplicateLayer(self, layerName, newLayerName): """ Duplicate the layer with **layerName**, assign **newLayerName** to the new layer and insert the new layer into the font. :: >>> layer = font.duplicateLayer("layer 1", "layer 2") """ layerOrder = self.layerOrder layerName = normalizers.normalizeLayerName(layerName) if layerName not in layerOrder: raise ValueError("No layer with the name '%s' exists." % layerName) newLayerName = normalizers.normalizeLayerName(newLayerName) if newLayerName in layerOrder: raise ValueError("A layer with the name '%s' already exists." % newLayerName) newLayer = self._duplicateLayer(layerName, newLayerName) newLayer = normalizers.normalizeLayer(newLayer) return newLayer def _duplicateLayer(self, layerName, newLayerName): """ This is the environment implementation of :meth:`BaseFont.duplicateLayer`. **layerName** will be a :ref:`type-string` representing a valid layer name. The value will have been normalized with :func:`normalizers.normalizeLayerName` and **layerName** will be a layer that exists in the font. **newLayerName** will be a :ref:`type-string` representing a valid layer name. The value will have been normalized with :func:`normalizers.normalizeLayerName` and **newLayerName** will have been tested to make sure that no layer with the same name exists in the font. This must return an instance of a :class:`BaseLayer` subclass. Subclasses may override this method. """ newLayer = self.getLayer(layerName).copy() return self.insertLayer(newLayer, newLayerName) def swapLayerNames(self, layerName, otherLayerName): """ Assign **layerName** to the layer currently named **otherLayerName** and assign the name **otherLayerName** to the layer currently named **layerName**. >>> font.swapLayerNames("before drawing revisions", "after drawing revisions") """ layerOrder = self.layerOrder layerName = normalizers.normalizeLayerName(layerName) if layerName not in layerOrder: raise ValueError("No layer with the name '%s' exists." % layerName) otherLayerName = normalizers.normalizeLayerName(otherLayerName) if otherLayerName not in layerOrder: raise ValueError("No layer with the name '%s' exists." % otherLayerName) self._swapLayers(layerName, otherLayerName) def _swapLayers(self, layerName, otherLayerName): """ This is the environment implementation of :meth:`BaseFont.swapLayerNames`. **layerName** will be a :ref:`type-string` representing a valid layer name. The value will have been normalized with :func:`normalizers.normalizeLayerName` and **layerName** will be a layer that exists in the font. **otherLayerName** will be a :ref:`type-string` representing a valid layer name. The value will have been normalized with :func:`normalizers.normalizeLayerName` and **otherLayerName** will be a layer that exists in the font. Subclasses may override this method. """ import random layer1 = self.getLayer(layerName) layer2 = self.getLayer(otherLayerName) # make a temporary name and assign it to # the first layer to prevent two layers # from having the same name at once. layerOrder = self.layerOrder for _ in range(50): # shout out to PostScript unique IDs tempLayerName = str(random.randint(4000000, 4999999)) if tempLayerName not in layerOrder: break if tempLayerName in layerOrder: raise FontPartsError(("Couldn't find a temporary layer name " "after 50 tries. Sorry. Please try again.")) layer1.name = tempLayerName # now swap layer2.name = layerName layer1.name = otherLayerName # ----------------- # Glyph Interaction # ----------------- # base implementation overrides
[docs] def _getItem(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseFont.__getitem__`. **name** will be a :ref:`type-string` defining an existing glyph in the default layer. The value will have been normalized with :func:`normalizers.normalizeGlyphName`. Subclasses may override this method. """ layer = self.defaultLayer return layer[name]
[docs] def _keys(self, **kwargs): """ This is the environment implementation of :meth:`BaseFont.keys`. This must return an :ref:`type-immutable-list` of all glyph names in the default layer. Subclasses may override this method. """ layer = self.defaultLayer return layer.keys()
[docs] def _newGlyph(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseFont.newGlyph`. **name** will be a :ref:`type-string` representing a valid glyph name. The value will have been tested to make sure that an existing glyph in the default layer does not have an identical name. The value will have been normalized with :func:`normalizers.normalizeGlyphName`. This must return an instance of :class:`BaseGlyph` representing the new glyph. Subclasses may override this method. """ layer = self.defaultLayer # clear is False here because the base newFont # that has called this method will have already # handled the clearing as specified by the caller. return layer.newGlyph(name, clear=False)
[docs] def _removeGlyph(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseFont.removeGlyph`. **name** will be a :ref:`type-string` representing an existing glyph in the default layer. The value will have been normalized with :func:`normalizers.normalizeGlyphName`. Subclasses may override this method. """ layer = self.defaultLayer layer.removeGlyph(name)
def __setitem__(self, name, glyph): """ Insert **glyph** into the font. :: >>> glyph = font["A"] = otherGlyph This will not insert the glyph directly. Rather, a new glyph will be created and the data from **glyph** will be copied to the new glyph. **name** indicates the name that should be assigned to the glyph after insertion. The data that will be inserted from **glyph** is the same data as documented in :meth:`BaseGlyph.copy`. On a font level **font.glyphOrder** will be preserved if the **name** is already present. """ name = normalizers.normalizeGlyphName(name) if name in self: # clear the glyph here if the glyph exists dest = self._getItem(name) dest.clear() return self._insertGlyph(glyph, name=name, clear=False) # order glyphOrder = dynamicProperty( "base_glyphOrder", """ The preferred order of the glyphs in the font. >>> font.glyphOrder ["C", "B", "A"] >>> font.glyphOrder = ["A", "B", "C"] """ ) def _get_base_glyphOrder(self): value = self._get_glyphOrder() value = normalizers.normalizeGlyphOrder(value) return value def _set_base_glyphOrder(self, value): value = normalizers.normalizeGlyphOrder(value) self._set_glyphOrder(value)
[docs] def _get_glyphOrder(self): """ This is the environment implementation of :attr:`BaseFont.glyphOrder`. This must return an :ref:`type-immutable-list` containing glyph names representing the glyph order in the font. The value will be normalized with :func:`normalizers.normalizeGlyphOrder`. Subclasses must override this method. """ self.raiseNotImplementedError()
[docs] def _set_glyphOrder(self, value): """ This is the environment implementation of :attr:`BaseFont.glyphOrder`. **value** will be a list of :ref:`type-string`. It will have been normalized with :func:`normalizers.normalizeGlyphOrder`. Subclasses must override this method. """ self.raiseNotImplementedError()
# ----------------- # Global Operations # -----------------
[docs] def round(self): """ Round all approriate data to integers. >>> font.round() This is the equivalent of calling the round method on: * info * kerning * the default layer * font-level guidelines This applies only to the default layer. """ self._round()
[docs] def _round(self): """ This is the environment implementation of :meth:`BaseFont.round`. Subclasses may override this method. """ layer = self.defaultLayer layer.round() self.info.round() self.kerning.round() for guideline in self.guidelines: guideline.round()
[docs] def autoUnicodes(self): """ Use heuristics to set Unicode values in all glyphs. >>> font.autoUnicodes() Environments will define their own heuristics for automatically determining values. This applies only to the default layer. """ self._autoUnicodes()
[docs] def _autoUnicodes(self): """ This is the environment implementation of :meth:`BaseFont.autoUnicodes`. Subclasses may override this method. """ layer = self.defaultLayer layer.autoUnicodes()
# ---------- # Guidelines # ---------- def _setFontInGuideline(self, guideline): if guideline.font is None: guideline.font = self guidelines = dynamicProperty( "guidelines", """ An :ref:`type-immutable-list` of font-level :class:`BaseGuideline` objects. >>> for guideline in font.guidelines: ... guideline.angle 0 45 90 """ )
[docs] def _get_guidelines(self): """ This is the environment implementation of :attr:`BaseFont.guidelines`. This must return an :ref:`type-immutable-list` of :class:`BaseGuideline` objects. Subclasses may override this method. """ return tuple([self._getitem__guidelines(i) for i in range(self._len__guidelines())])
def _len__guidelines(self): return self._lenGuidelines()
[docs] def _lenGuidelines(self, **kwargs): """ This must return an integer indicating the number of font-level guidelines in the font. Subclasses must override this method. """ self.raiseNotImplementedError()
def _getitem__guidelines(self, index): index = normalizers.normalizeIndex(index) if index >= self._len__guidelines(): raise ValueError("No guideline located at index %d." % index) guideline = self._getGuideline(index) self._setFontInGuideline(guideline) return guideline
[docs] def _getGuideline(self, index, **kwargs): """ This must return a :class:`BaseGuideline` object. **index** will be a valid **index**. Subclasses must override this method. """ self.raiseNotImplementedError()
def _getGuidelineIndex(self, guideline): for i, other in enumerate(self.guidelines): if guideline == other: return i raise FontPartsError("The guideline could not be found.")
[docs] def appendGuideline(self, position=None, angle=None, name=None, color=None, guideline=None): """ Append a new guideline to the font. >>> guideline = font.appendGuideline((50, 0), 90) >>> guideline = font.appendGuideline((0, 540), 0, name="overshoot", >>> color=(0, 0, 0, 0.2)) **position** must be a :ref:`type-coordinate` indicating the position of the guideline. **angle** indicates the :ref:`type-angle` of the guideline. **name** indicates the name for the guideline. This must be a :ref:`type-string` or ``None``. **color** indicates the color for the guideline. This must be a :ref:`type-color` or ``None``. This will return the newly created :class:`BaseGuidline` object. ``guideline`` may be a :class:`BaseGuideline` object from which attribute values will be copied. If ``position``, ``angle``, ``name`` or ``color`` are specified as arguments, those values will be used instead of the values in the given guideline object. """ identifier = None if guideline is not None: guideline = normalizers.normalizeGuideline(guideline) if position is None: position = guideline.position if angle is None: angle = guideline.angle if name is None: name = guideline.name if color is None: color = guideline.color if guideline.identifier is not None: existing = set([g.identifier for g in self.guidelines if g.identifier is not None]) if guideline.identifier not in existing: identifier = guideline.identifier position = normalizers.normalizeCoordinateTuple(position) angle = normalizers.normalizeRotationAngle(angle) if name is not None: name = normalizers.normalizeGuidelineName(name) if color is not None: color = normalizers.normalizeColor(color) identifier = normalizers.normalizeIdentifier(identifier) guideline = self._appendGuideline(position, angle, name=name, color=color, identifier=identifier) guideline.font = self return guideline
[docs] def _appendGuideline(self, position, angle, name=None, color=None, identifier=None, **kwargs): """ This is the environment implementation of :meth:`BaseFont.appendGuideline`. **position** will be a valid :ref:`type-coordinate`. **angle** will be a valid angle. **name** will be a valid :ref:`type-string` or ``None``. **color** will be a valid :ref:`type-color` or ``None``. This must return the newly created :class:`BaseGuideline` object. Subclasses may override this method. """ self.raiseNotImplementedError()
[docs] def removeGuideline(self, guideline): """ Remove **guideline** from the font. >>> font.removeGuideline(guideline) >>> font.removeGuideline(2) **guideline** can be a guideline object or an integer representing the guideline index. """ if isinstance(guideline, int): index = guideline else: index = self._getGuidelineIndex(guideline) index = normalizers.normalizeIndex(index) if index >= self._len__guidelines(): raise ValueError("No guideline located at index %d." % index) self._removeGuideline(index)
[docs] def _removeGuideline(self, index, **kwargs): """ This is the environment implementation of :meth:`BaseFont.removeGuideline`. **index** will be a valid index. Subclasses must override this method. """ self.raiseNotImplementedError()
[docs] def clearGuidelines(self): """ Clear all guidelines. >>> font.clearGuidelines() """ self._clearGuidelines()
[docs] def _clearGuidelines(self): """ This is the environment implementation of :meth:`BaseFont.clearGuidelines`. Subclasses may override this method. """ for _ in range(self._len__guidelines()): self.removeGuideline(-1)
# ------------- # Interpolation # -------------
[docs] def interpolate(self, factor, minFont, maxFont, round=True, suppressError=True): """ Interpolate all possible data in the font. >>> font.interpolate(0.5, otherFont1, otherFont2) >>> font.interpolate((0.5, 2.0), otherFont1, otherFont2, round=False) The interpolation occurs on a 0 to 1.0 range where **minFont** is located at 0 and **maxFont** is located at 1.0. **factor** is the interpolation value. It may be less than 0 and greater than 1.0. It may be a :ref:`type-int-float` or a tuple of two :ref:`type-int-float`. If it is a tuple, the first number indicates the x factor and the second number indicates the y factor. **round** indicates if the result should be rounded to integers. **suppressError** indicates if incompatible data should be ignored or if an error should be raised when such incompatibilities are found. """ factor = normalizers.normalizeInterpolationFactor(factor) if not isinstance(minFont, BaseFont): raise TypeError(("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, minFont.__class__.__name__)) if not isinstance(maxFont, BaseFont): raise TypeError(("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, maxFont.__class__.__name__)) round = normalizers.normalizeBoolean(round) suppressError = normalizers.normalizeBoolean(suppressError) self._interpolate(factor, minFont, maxFont, round=round, suppressError=suppressError)
[docs] def _interpolate(self, factor, minFont, maxFont, round=True, suppressError=True): """ This is the environment implementation of :meth:`BaseFont.interpolate`. Subclasses may override this method. """ # layers for layerName in self.layerOrder: self.removeLayer(layerName) for layerName in minFont.layerOrder: if layerName not in maxFont.layerOrder: continue minLayer = minFont.getLayer(layerName) maxLayer = maxFont.getLayer(layerName) dstLayer = self.newLayer(layerName) dstLayer.interpolate(factor, minLayer, maxLayer, round=round, suppressError=suppressError) if self.layerOrder: if ufoLib.DEFAULT_LAYER_NAME in self.layerOrder: self.defaultLayer = self.getLayer(ufoLib.DEFAULT_LAYER_NAME) else: self.defaultLayer = self.getLayer(self.layerOrder[0]) # kerning and groups self.kerning.interpolate(factor, minFont.kerning, maxFont.kerning, round=round, suppressError=suppressError) # info self.info.interpolate(factor, minFont.info, maxFont.info, round=round, suppressError=suppressError)
compatibilityReporterClass = FontCompatibilityReporter
[docs] def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. >>> compatible, report = self.isCompatible(otherFont) >>> compatible False >>> report [Fatal] Glyph: "test1" + "test2" [Fatal] Glyph: "test1" contains 1 contours | "test2" contains 2 contours This will return a ``bool`` indicating if the font is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseFont, self).isCompatible(other, BaseFont)
[docs] def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseFont.isCompatible`. Subclasses may override this method. """ font1 = self font2 = other # incompatible guidelines guidelines1 = set(font1.guidelines) guidelines2 = set(font2.guidelines) if len(guidelines1) != len(guidelines2): reporter.warning = True reporter.guidelineCountDifference = True if len(guidelines1.difference(guidelines2)) != 0: reporter.warning = True reporter.guidelinesMissingFromFont2 = list( guidelines1.difference(guidelines2)) if len(guidelines2.difference(guidelines1)) != 0: reporter.warning = True reporter.guidelinesMissingInFont1 = list( guidelines2.difference(guidelines1)) # incompatible layers layers1 = set(font1.layerOrder) layers2 = set(font2.layerOrder) if len(layers1) != len(layers2): reporter.warning = True reporter.layerCountDifference = True if len(layers1.difference(layers2)) != 0: reporter.warning = True reporter.layersMissingFromFont2 = list(layers1.difference(layers2)) if len(layers2.difference(layers1)) != 0: reporter.warning = True reporter.layersMissingInFont1 = list(layers2.difference(layers1)) # test layers for layerName in sorted(layers1.intersection(layers2)): layer1 = font1.getLayer(layerName) layer2 = font2.getLayer(layerName) layerCompatibility = layer1.isCompatible(layer2)[1] if layerCompatibility.fatal or layerCompatibility.warning: if layerCompatibility.fatal: reporter.fatal = True if layerCompatibility.warning: reporter.warning = True reporter.layers.append(layerCompatibility)
# ------- # mapping # ------- def getReverseComponentMapping(self): """ Get a reversed map of component references in the font. { 'A' : ['Aacute', 'Aring'] 'acute' : ['Aacute'] 'ring' : ['Aring'] etc. } """ return self._getReverseComponentMapping() def _getReverseComponentMapping(self): """ This is the environment implementation of :meth:`BaseFont.getReverseComponentMapping`. Subclasses may override this method. """ layer = self.defaultLayer return layer.getReverseComponentMapping() def getCharacterMapping(self): """ Create a dictionary of unicode -> [glyphname, ...] mappings. All glyphs are loaded. Note that one glyph can have multiple unicode values, and a unicode value can have multiple glyphs pointing to it. """ return self._getCharacterMapping() def _getCharacterMapping(self): """ This is the environment implementation of :meth:`BaseFont.getCharacterMapping`. Subclasses may override this method. """ layer = self.defaultLayer return layer.getCharacterMapping() # --------- # Selection # --------- # layers selectedLayers = dynamicProperty( "base_selectedLayers", """ A list of layers selected in the layer. Getting selected layer objects: >>> for layer in layer.selectedLayers: ... layer.color = (1, 0, 0, 0.5) Setting selected layer objects: >>> layer.selectedLayers = someLayers """ ) def _get_base_selectedLayers(self): selected = tuple([normalizers.normalizeLayer(layer) for layer in self._get_selectedLayers()]) return selected def _get_selectedLayers(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.layers) def _set_base_selectedLayers(self, value): normalized = [normalizers.normalizeLayer(layer) for layer in value] self._set_selectedLayers(normalized) def _set_selectedLayers(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.layers, value) selectedLayerNames = dynamicProperty( "base_selectedLayerNames", """ A list of names of layers selected in the layer. Getting selected layer names: >>> for name in layer.selectedLayerNames: ... print(name) Setting selected layer names: >>> layer.selectedLayerNames = ["A", "B", "C"] """ ) def _get_base_selectedLayerNames(self): selected = tuple([normalizers.normalizeLayerName(name) for name in self._get_selectedLayerNames()]) return selected def _get_selectedLayerNames(self): """ Subclasses may override this method. """ selected = [layer.name for layer in self.selectedLayers] return selected def _set_base_selectedLayerNames(self, value): normalized = [normalizers.normalizeLayerName(name) for name in value] self._set_selectedLayerNames(normalized) def _set_selectedLayerNames(self, value): """ Subclasses may override this method. """ select = [self.layers(name) for name in value] self.selectedLayers = select # guidelines selectedGuidelines = dynamicProperty( "base_selectedGuidelines", """ A list of guidelines selected in the font. Getting selected guideline objects: >>> for guideline in font.selectedGuidelines: ... guideline.color = (1, 0, 0, 0.5) Setting selected guideline objects: >>> font.selectedGuidelines = someGuidelines Setting also supports guideline indexes: >>> font.selectedGuidelines = [0, 2] """ ) def _get_base_selectedGuidelines(self): selected = tuple([normalizers.normalizeGuideline(guideline) for guideline in self._get_selectedGuidelines()]) return selected def _get_selectedGuidelines(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.guidelines) def _set_base_selectedGuidelines(self, value): normalized = [] for i in value: if isinstance(i, int): i = normalizers.normalizeIndex(i) else: i = normalizers.normalizeGuideline(i) normalized.append(i) self._set_selectedGuidelines(normalized) def _set_selectedGuidelines(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.guidelines, value)