from fontParts.base.base import (
BaseObject,
dynamicProperty,
interpolate,
reference
)
from fontParts.base import normalizers
from fontParts.base.errors import FontPartsError
from fontParts.base.deprecated import DeprecatedInfo, RemovedInfo
[docs]
class BaseInfo(BaseObject, DeprecatedInfo, RemovedInfo):
from fontTools.ufoLib import fontInfoAttributesVersion3
copyAttributes = set(fontInfoAttributesVersion3)
copyAttributes.remove("guidelines")
copyAttributes = tuple(copyAttributes)
def _reprContents(self):
contents = []
if self.font is not None:
contents.append("for font")
contents += self.font._reprContents()
return contents
# -------
# Parents
# -------
# Font
_font = None
font = dynamicProperty("font", "The info's parent font.")
def _get_font(self):
if self._font is None:
return None
return self._font()
def _set_font(self, font):
if self._font is not None and self._font != font:
raise AssertionError("font for info already set and is not same as font")
if font is not None:
font = reference(font)
self._font = font
# ----------
# Validation
# ----------
@staticmethod
def _validateFontInfoAttributeValue(attr, value):
from fontTools.ufoLib import validateFontInfoVersion3ValueForAttribute
valid = validateFontInfoVersion3ValueForAttribute(attr, value)
if not valid:
raise ValueError("Invalid value %s for attribute '%s'."
% (value, attr))
return value
# ----------
# Attributes
# ----------
# has
def __hasattr__(self, attr):
from fontTools.ufoLib import fontInfoAttributesVersion3
if attr in fontInfoAttributesVersion3:
return True
return super(BaseInfo, self).__hasattr__(attr)
# get
def __getattribute__(self, attr):
from fontTools.ufoLib import fontInfoAttributesVersion3
if attr != "guidelines" and attr in fontInfoAttributesVersion3:
value = self._getAttr(attr)
if value is not None:
value = self._validateFontInfoAttributeValue(attr, value)
return value
return super(BaseInfo, self).__getattribute__(attr)
[docs]
def _getAttr(self, attr):
"""
Subclasses may override this method.
If a subclass does not override this method,
it must implement '_get_attributeName' methods
for all Info methods.
"""
meth = "_get_%s" % attr
if not hasattr(self, meth):
raise AttributeError("No getter for attribute '%s'." % attr)
meth = getattr(self, meth)
value = meth()
return value
# set
def __setattr__(self, attr, value):
from fontTools.ufoLib import fontInfoAttributesVersion3
if attr != "guidelines" and attr in fontInfoAttributesVersion3:
if value is not None:
value = self._validateFontInfoAttributeValue(attr, value)
return self._setAttr(attr, value)
return super(BaseInfo, self).__setattr__(attr, value)
[docs]
def _setAttr(self, attr, value):
"""
Subclasses may override this method.
If a subclass does not override this method,
it must implement '_set_attributeName' methods
for all Info methods.
"""
meth = "_set_%s" % attr
if not hasattr(self, meth):
raise AttributeError("No setter for attribute '%s'." % attr)
meth = getattr(self, meth)
meth(value)
# -------------
# Normalization
# -------------
[docs]
def round(self):
"""
Round the following attributes to integers:
- unitsPerEm
- descender
- xHeight
- capHeight
- ascender
- openTypeHeadLowestRecPPEM
- openTypeHheaAscender
- openTypeHheaDescender
- openTypeHheaLineGap
- openTypeHheaCaretSlopeRise
- openTypeHheaCaretSlopeRun
- openTypeHheaCaretOffset
- openTypeOS2WidthClass
- openTypeOS2WeightClass
- openTypeOS2TypoAscender
- openTypeOS2TypoDescender
- openTypeOS2TypoLineGap
- openTypeOS2WinAscent
- openTypeOS2WinDescent
- openTypeOS2SubscriptXSize
- openTypeOS2SubscriptYSize
- openTypeOS2SubscriptXOffset
- openTypeOS2SubscriptYOffset
- openTypeOS2SuperscriptXSize
- openTypeOS2SuperscriptYSize
- openTypeOS2SuperscriptXOffset
- openTypeOS2SuperscriptYOffset
- openTypeOS2StrikeoutSize
- openTypeOS2StrikeoutPosition
- openTypeVheaVertTypoAscender
- openTypeVheaVertTypoDescender
- openTypeVheaVertTypoLineGap
- openTypeVheaCaretSlopeRise
- openTypeVheaCaretSlopeRun
- openTypeVheaCaretOffset
- postscriptSlantAngle
- postscriptUnderlineThickness
- postscriptUnderlinePosition
- postscriptBlueValues
- postscriptOtherBlues
- postscriptFamilyBlues
- postscriptFamilyOtherBlues
- postscriptStemSnapH
- postscriptStemSnapV
- postscriptBlueFuzz
- postscriptBlueShift
- postscriptDefaultWidthX
- postscriptNominalWidthX
"""
self._round()
[docs]
def _round(self, **kwargs):
"""
Subclasses may override this method.
"""
from fontMath.mathFunctions import setRoundIntegerFunction
setRoundIntegerFunction(normalizers.normalizeVisualRounding)
mathInfo = self._toMathInfo(guidelines=False)
mathInfo = mathInfo.round()
self._fromMathInfo(mathInfo, guidelines=False)
# --------
# Updating
# --------
[docs]
def update(self, other):
"""
Update this object with the values
from **otherInfo**.
"""
self._update(other)
def _update(self, other):
"""
Subclasses may override this method.
"""
from fontTools.ufoLib import fontInfoAttributesVersion3
for attr in fontInfoAttributesVersion3:
if attr == "guidelines":
continue
value = getattr(other, attr)
setattr(self, attr, value)
# -------------
# Interpolation
# -------------
def toMathInfo(self, guidelines=True):
"""
Returns the info as an object that follows the
`MathGlyph protocol <https://github.com/typesupply/fontMath>`_.
>>> mg = font.info.toMathInfo()
"""
return self._toMathInfo(guidelines=guidelines)
def fromMathInfo(self, mathInfo, guidelines=True):
"""
Replaces the contents of this info object with the contents of ``mathInfo``.
>>> font.fromMathInfo(mg)
``mathInfo`` must be an object following the
`MathInfo protocol <https://github.com/typesupply/fontMath>`_.
"""
return self._fromMathInfo(mathInfo, guidelines=guidelines)
def _toMathInfo(self, guidelines=True):
"""
Subclasses may override this method.
"""
import fontMath
# A little trickery is needed here because MathInfo
# handles font level guidelines. Those are not in this
# object so we temporarily fake them just enough for
# MathInfo and then move them back to the proper place.
self.guidelines = []
if guidelines:
for guideline in self.font.guidelines:
d = dict(
x=guideline.x,
y=guideline.y,
angle=guideline.angle,
name=guideline.name,
identifier=guideline.identifier,
color=guideline.color
)
self.guidelines.append(d)
info = fontMath.MathInfo(self)
del self.guidelines
return info
def _fromMathInfo(self, mathInfo, guidelines=True):
"""
Subclasses may override this method.
"""
self.guidelines = []
mathInfo.extractInfo(self)
font = self.font
if guidelines:
for guideline in self.guidelines:
font.appendGuideline(
position=(guideline["x"], guideline["y"]),
angle=guideline["angle"],
name=guideline["name"],
color=guideline["color"]
# XXX identifier is lost
)
del self.guidelines
[docs]
def interpolate(self, factor, minInfo, maxInfo, round=True, suppressError=True):
"""
Interpolate all pairs between minInfo and maxInfo.
The interpolation occurs on a 0 to 1.0 range where minInfo
is located at 0 and maxInfo 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 number (integer, float)
or a tuple of two numbers. 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(minInfo, BaseInfo):
raise TypeError(("Interpolation to an instance of %r can not be "
"performed from an instance of %r.") %
(self.__class__.__name__, minInfo.__class__.__name__))
if not isinstance(maxInfo, BaseInfo):
raise TypeError(("Interpolation to an instance of %r can not be "
"performed from an instance of %r.") %
(self.__class__.__name__, maxInfo.__class__.__name__))
round = normalizers.normalizeBoolean(round)
suppressError = normalizers.normalizeBoolean(suppressError)
self._interpolate(factor, minInfo, maxInfo,
round=round, suppressError=suppressError)
[docs]
def _interpolate(self, factor, minInfo, maxInfo, round=True, suppressError=True):
"""
Subclasses may override this method.
"""
from fontMath.mathFunctions import setRoundIntegerFunction
setRoundIntegerFunction(normalizers.normalizeVisualRounding)
minInfo = minInfo._toMathInfo()
maxInfo = maxInfo._toMathInfo()
result = interpolate(minInfo, maxInfo, factor)
if result is None and not suppressError:
raise FontPartsError(("Info from font '%s' and font '%s' could not be "
"interpolated.")
% (minInfo.font.name, maxInfo.font.name))
if round:
result = result.round()
self._fromMathInfo(result)