try:
from itertools import zip_longest as zip_longest
except ImportError:
from itertools import izip_longest as zip_longest
import collections
import os
from copy import deepcopy
from fontParts.base.errors import FontPartsError
from fontParts.base.base import (
BaseObject,
TransformationMixin,
InterpolationMixin,
SelectionMixin,
dynamicProperty,
interpolate,
FuzzyNumber
)
from fontParts.base import normalizers
from fontParts.base.compatibility import GlyphCompatibilityReporter
from fontParts.base.color import Color
from fontParts.base.deprecated import DeprecatedGlyph, RemovedGlyph
[docs]class BaseGlyph(BaseObject,
TransformationMixin,
InterpolationMixin,
SelectionMixin,
DeprecatedGlyph,
RemovedGlyph
):
"""
A glyph object. This object will almost always
be created by retrieving it from a font object.
"""
copyAttributes = (
"name",
"unicodes",
"width",
"height",
"note",
"markColor",
"lib"
)
def _reprContents(self):
contents = [
"'%s'" % self.name,
]
if self.layer is not None:
contents.append("('%s')" % self.layer.name)
return contents
[docs] def copy(self):
"""
Copy this glyph's data into a new glyph object.
This new glyph object will not belong to a font.
>>> copiedGlyph = glyph.copy()
This will copy:
- name
- unicodes
- width
- height
- note
- markColor
- lib
- contours
- components
- anchors
- guidelines
- image
"""
return super(BaseGlyph, self).copy()
def copyData(self, source):
super(BaseGlyph, self).copyData(source)
for contour in source.contours:
self.appendContour(contour)
for component in source.components:
self.appendComponent(component=component)
for anchor in source.anchors:
self.appendAnchor(anchor=anchor)
for guideline in source.guidelines:
self.appendGuideline(guideline=guideline)
sourceImage = source.image
if sourceImage.data is not None:
selfImage = self.addImage(data=sourceImage.data)
selfImage.transformation = sourceImage.transformation
selfImage.color = sourceImage.color
# -------
# Parents
# -------
# Layer
_layer = None
layer = dynamicProperty(
"layer",
"""
The glyph's parent layer.
>>> layer = glyph.layer
"""
)
def _get_layer(self):
if self._layer is None:
return None
return self._layer
def _set_layer(self, layer):
self._layer = layer
# Font
font = dynamicProperty(
"font",
"""
The glyph's parent font.
>>> font = glyph.font
"""
)
def _get_font(self):
if self._layer is None:
return None
return self.layer.font
# --------------
# Identification
# --------------
# Name
name = dynamicProperty(
"base_name",
"""
The glyph's name. This will be a :ref:`type-string`.
>>> glyph.name
"A"
>>> glyph.name = "A.alt"
"""
)
def _get_base_name(self):
value = self._get_name()
if value is not None:
value = normalizers.normalizeGlyphName(value)
return value
def _set_base_name(self, value):
if value == self.name:
return
value = normalizers.normalizeGlyphName(value)
layer = self.layer
if layer is not None and value in layer:
raise ValueError("A glyph with the name '%s' already exists."
% value)
self._set_name(value)
[docs] def _get_name(self):
"""
Get the name of the glyph.
This must return a unicode string.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def _set_name(self, value):
"""
Set the name of the glyph.
This will be a unicode string.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# Unicodes
unicodes = dynamicProperty(
"base_unicodes",
"""
The glyph's unicode values in order from most to least important.
>>> glyph.unicodes
(65,)
>>> glyph.unicodes = [65, 66]
>>> glyph.unicodes = []
The values in the returned tuple will be :ref:`type-int`.
When setting you may use a list of :ref:`type-int` or
:ref:`type-hex` values.
"""
)
def _get_base_unicodes(self):
value = self._get_unicodes()
value = normalizers.normalizeGlyphUnicodes(value)
return value
def _set_base_unicodes(self, value):
value = list(value)
value = normalizers.normalizeGlyphUnicodes(value)
self._set_unicodes(value)
[docs] def _get_unicodes(self):
"""
Get the unicodes assigned to the glyph.
This must return a tuple of zero or more integers.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def _set_unicodes(self, value):
"""
Assign the unicodes to the glyph.
This will be a list of zero or more integers.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
unicode = dynamicProperty(
"base_unicode",
"""
The glyph's primary unicode value.
>>> glyph.unicode
65
>>> glyph.unicode = None
This is equivalent to ``glyph.unicodes[0]``. Setting a
``glyph.unicode`` value will reset ``glyph.unicodes`` to a tuple
containing that value or an empty tuple if ``value`` is ``None``.
>>> glyph.unicodes
(65, 67)
>>> glyph.unicode = 65
>>> glyph.unicodes
(65,)
>>> glyph.unicode = None
>>> glyph.unicodes
()
The returned value will be an :ref:`type-int` or ``None``.
When setting you may send :ref:`type-int` or :ref:`type-hex`
values or ``None``.
"""
)
def _get_base_unicode(self):
value = self._get_unicode()
if value is not None:
value = normalizers.normalizeGlyphUnicode(value)
return value
def _set_base_unicode(self, value):
if value is not None:
value = normalizers.normalizeGlyphUnicode(value)
self._set_unicode(value)
else:
self._set_unicodes(())
[docs] def _get_unicode(self):
"""
Get the primary unicode assigned to the glyph.
This must return an integer or None.
Subclasses may override this method.
"""
values = self.unicodes
if values:
return values[0]
return None
[docs] def _set_unicode(self, value):
"""
Assign the primary unicode to the glyph.
This will be an integer or None.
Subclasses may override this method.
"""
if value is None:
self.unicodes = []
else:
self.unicodes = [value]
[docs] def autoUnicodes(self):
"""
Use heuristics to set the Unicode values in the glyph.
>>> glyph.autoUnicodes()
Environments will define their own heuristics for
automatically determining values.
"""
self._autoUnicodes()
[docs] def _autoUnicodes(self):
"""
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# -------
# Metrics
# -------
# horizontal
width = dynamicProperty(
"base_width",
"""
The glyph's width.
>>> glyph.width
500
>>> glyph.width = 200
The value will be a :ref:`type-int-float`.
"""
)
def _get_base_width(self):
value = self._get_width()
value = normalizers.normalizeGlyphWidth(value)
return value
def _set_base_width(self, value):
value = normalizers.normalizeGlyphWidth(value)
self._set_width(value)
[docs] def _get_width(self):
"""
This must return an int or float.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def _set_width(self, value):
"""
value will be an int or float.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
leftMargin = dynamicProperty(
"base_leftMargin",
"""
The glyph's left margin.
>>> glyph.leftMargin
35
>>> glyph.leftMargin = 45
The value will be a :ref:`type-int-float`
or `None` if the glyph has no outlines.
"""
)
def _get_base_leftMargin(self):
value = self._get_leftMargin()
value = normalizers.normalizeGlyphLeftMargin(value)
return value
def _set_base_leftMargin(self, value):
value = normalizers.normalizeGlyphLeftMargin(value)
self._set_leftMargin(value)
[docs] def _get_leftMargin(self):
"""
This must return an int or float.
If the glyph has no outlines, this must return `None`.
Subclasses may override this method.
"""
bounds = self.bounds
if bounds is None:
return None
xMin, yMin, xMax, yMax = bounds
return xMin
[docs] def _set_leftMargin(self, value):
"""
value will be an int or float.
Subclasses may override this method.
"""
diff = value - self.leftMargin
self.moveBy((diff, 0))
self.width += diff
rightMargin = dynamicProperty(
"base_rightMargin",
"""
The glyph's right margin.
>>> glyph.rightMargin
35
>>> glyph.rightMargin = 45
The value will be a :ref:`type-int-float`
or `None` if the glyph has no outlines.
"""
)
def _get_base_rightMargin(self):
value = self._get_rightMargin()
value = normalizers.normalizeGlyphRightMargin(value)
return value
def _set_base_rightMargin(self, value):
value = normalizers.normalizeGlyphRightMargin(value)
self._set_rightMargin(value)
[docs] def _get_rightMargin(self):
"""
This must return an int or float.
If the glyph has no outlines, this must return `None`.
Subclasses may override this method.
"""
bounds = self.bounds
if bounds is None:
return None
xMin, yMin, xMax, yMax = bounds
return self.width - xMax
[docs] def _set_rightMargin(self, value):
"""
value will be an int or float.
Subclasses may override this method.
"""
bounds = self.bounds
if bounds is None:
self.width = value
else:
xMin, yMin, xMax, yMax = bounds
self.width = xMax + value
# vertical
height = dynamicProperty(
"base_height",
"""
The glyph's height.
>>> glyph.height
500
>>> glyph.height = 200
The value will be a :ref:`type-int-float`.
"""
)
def _get_base_height(self):
value = self._get_height()
value = normalizers.normalizeGlyphHeight(value)
return value
def _set_base_height(self, value):
value = normalizers.normalizeGlyphHeight(value)
self._set_height(value)
[docs] def _get_height(self):
"""
This must return an int or float.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def _set_height(self, value):
"""
value will be an int or float.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
bottomMargin = dynamicProperty(
"base_bottomMargin",
"""
The glyph's bottom margin.
>>> glyph.bottomMargin
35
>>> glyph.bottomMargin = 45
The value will be a :ref:`type-int-float`
or `None` if the glyph has no outlines.
"""
)
def _get_base_bottomMargin(self):
value = self._get_bottomMargin()
value = normalizers.normalizeGlyphBottomMargin(value)
return value
def _set_base_bottomMargin(self, value):
value = normalizers.normalizeGlyphBottomMargin(value)
self._set_bottomMargin(value)
[docs] def _get_bottomMargin(self):
"""
This must return an int or float.
If the glyph has no outlines, this must return `None`.
Subclasses may override this method.
"""
bounds = self.bounds
if bounds is None:
return None
xMin, yMin, xMax, yMax = bounds
return yMin
[docs] def _set_bottomMargin(self, value):
"""
value will be an int or float.
Subclasses may override this method.
"""
diff = value - self.bottomMargin
self.moveBy((0, diff))
self.height += diff
topMargin = dynamicProperty(
"base_topMargin",
"""
The glyph's top margin.
>>> glyph.topMargin
35
>>> glyph.topMargin = 45
The value will be a :ref:`type-int-float`
or `None` if the glyph has no outlines.
"""
)
def _get_base_topMargin(self):
value = self._get_topMargin()
value = normalizers.normalizeGlyphTopMargin(value)
return value
def _set_base_topMargin(self, value):
value = normalizers.normalizeGlyphTopMargin(value)
self._set_topMargin(value)
[docs] def _get_topMargin(self):
"""
This must return an int or float.
If the glyph has no outlines, this must return `None`.
Subclasses may override this method.
"""
bounds = self.bounds
if bounds is None:
return None
xMin, yMin, xMax, yMax = bounds
return self.height - yMax
[docs] def _set_topMargin(self, value):
"""
value will be an int or float.
Subclasses may override this method.
"""
bounds = self.bounds
if bounds is None:
self.height = value
else:
xMin, yMin, xMax, yMax = bounds
self.height = yMax + value
# ----
# Pens
# ----
[docs] def getPen(self):
"""
Return a :ref:`type-pen` object for adding outline data
to the glyph.
>>> pen = glyph.getPen()
"""
self.raiseNotImplementedError()
[docs] def getPointPen(self):
"""
Return a :ref:`type-pointpen` object for adding outline data
to the glyph.
>>> pointPen = glyph.getPointPen()
"""
self.raiseNotImplementedError()
[docs] def draw(self, pen, contours=True, components=True):
"""
Draw the glyph's outline data (contours and components) to
the given :ref:`type-pen`.
>>> glyph.draw(pen)
If ``contours`` is set to ``False``, the glyph's
contours will not be drawn.
>>> glyph.draw(pen, contours=False)
If ``components`` is set to ``False``, the glyph's
components will not be drawn.
>>> glyph.draw(pen, components=False)
"""
if contours:
for contour in self:
contour.draw(pen)
if components:
for component in self.components:
component.draw(pen)
[docs] def drawPoints(self, pen, contours=True, components=True):
"""
Draw the glyph's outline data (contours and components) to
the given :ref:`type-pointpen`.
>>> glyph.drawPoints(pointPen)
If ``contours`` is set to ``False``, the glyph's
contours will not be drawn.
>>> glyph.drawPoints(pointPen, contours=False)
If ``components`` is set to ``False``, the glyph's
components will not be drawn.
>>> glyph.drawPoints(pointPen, components=False)
"""
if contours:
for contour in self:
contour.drawPoints(pen)
if components:
for component in self.components:
component.drawPoints(pen)
# -----------------------------------------
# Contour, Component and Anchor Interaction
# -----------------------------------------
[docs] def clear(self, contours=True, components=True, anchors=True,
guidelines=True, image=True):
"""
Clear the glyph.
>>> glyph.clear()
This clears:
- contours
- components
- anchors
- guidelines
- image
It's possible to turn off the clearing of portions of
the glyph with the listed arguments.
>>> glyph.clear(guidelines=False)
"""
self._clear(contours=contours, components=components,
anchors=anchors, guidelines=guidelines, image=image)
[docs] def _clear(self, contours=True, components=True, anchors=True,
guidelines=True, image=True):
"""
Subclasses may override this method.
"""
if contours:
self.clearContours()
if components:
self.clearComponents()
if anchors:
self.clearAnchors()
if guidelines:
self.clearGuidelines()
if image:
self.clearImage()
[docs] def appendGlyph(self, other, offset=None):
"""
Append the data from ``other`` to new objects in this glyph.
>>> glyph.appendGlyph(otherGlyph)
This will append:
- contours
- components
- anchors
- guidelines
``offset`` indicates the x and y shift values that should
be applied to the appended data. It must be a :ref:`type-coordinate`
value or ``None``. If ``None`` is given, the offset will be ``(0, 0)``.
>>> glyph.appendGlyph(otherGlyph, (100, 0))
"""
if offset is None:
offset = (0, 0)
offset = normalizers.normalizeTransformationOffset(offset)
self._appendGlyph(other, offset)
[docs] def _appendGlyph(self, other, offset=None):
"""
Subclasses may override this method.
"""
other = other.copy()
if offset != (0, 0):
other.moveBy(offset)
for contour in other.contours:
self.appendContour(contour)
for component in other.components:
self.appendComponent(component=component)
for anchor in other.anchors:
self.appendAnchor(anchor=anchor)
for guideline in other.guidelines:
self.appendGuideline(guideline=guideline)
# Contours
def _setGlyphInContour(self, contour):
if contour.glyph is None:
contour.glyph = self
contours = dynamicProperty(
"contours",
"""
An :ref:`type-immutable-list` of all contours in the glyph.
>>> contours = glyph.contours
The list will contain :class:`BaseContour` objects.
"""
)
[docs] def _get_contours(self):
"""
Subclasses may override this method.
"""
return tuple([self[i] for i in range(len(self))])
[docs] def __len__(self):
"""
The number of contours in the glyph.
>>> len(glyph)
2
"""
return self._lenContours()
[docs] def _lenContours(self, **kwargs):
"""
This must return an integer.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def __iter__(self):
"""
Iterate through all contours in the glyph.
>>> for contour in glyph:
... contour.reverse()
"""
return self._iterContours()
[docs] def _iterContours(self, **kwargs):
"""
This must return an iterator that returns wrapped contours.
Subclasses may override this method.
"""
count = len(self)
index = 0
while count:
yield self[index]
count -= 1
index += 1
[docs] def __getitem__(self, index):
"""
Get the contour located at ``index`` from the glyph.
>>> contour = glyph[0]
The returned value will be a :class:`BaseContour` object.
"""
index = normalizers.normalizeIndex(index)
if index >= len(self):
raise ValueError("No contour located at index %d." % index)
contour = self._getContour(index)
self._setGlyphInContour(contour)
return contour
[docs] def _getContour(self, index, **kwargs):
"""
This must return a wrapped contour.
index will be a valid index.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def _getContourIndex(self, contour):
for i, other in enumerate(self.contours):
if contour == other:
return i
raise FontPartsError("The contour could not be found.")
[docs] def appendContour(self, contour, offset=None):
"""
Append a contour containing the same data as ``contour``
to this glyph.
>>> contour = glyph.appendContour(contour)
This will return a :class:`BaseContour` object representing
the new contour in the glyph. ``offset`` indicates the x and
y shift values that should be applied to the appended data.
It must be a :ref:`type-coordinate` value or ``None``. If
``None`` is given, the offset will be ``(0, 0)``.
>>> contour = glyph.appendContour(contour, (100, 0))
"""
contour = normalizers.normalizeContour(contour)
if offset is None:
offset = (0, 0)
offset = normalizers.normalizeTransformationOffset(offset)
return self._appendContour(contour, offset)
[docs] def _appendContour(self, contour, offset=None, **kwargs):
"""
contour will be an object with a drawPoints method.
offset will be a valid offset (x, y).
This must return the new contour.
Subclasses may override this method.
"""
pointPen = self.getPointPen()
if offset != (0, 0):
copy = contour.copy()
copy.moveBy(offset)
copy.drawPoints(pointPen)
else:
contour.drawPoints(pointPen)
return self[-1]
[docs] def removeContour(self, contour):
"""
Remove ``contour`` from the glyph.
>>> glyph.removeContour(contour)
``contour`` may be a :ref:`BaseContour` or an :ref:`type-int`
representing a contour index.
"""
if isinstance(contour, int):
index = contour
else:
index = self._getContourIndex(contour)
index = normalizers.normalizeIndex(index)
if index >= len(self):
raise ValueError("No contour located at index %d." % index)
self._removeContour(index)
[docs] def _removeContour(self, index, **kwargs):
"""
index will be a valid index.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def clearContours(self):
"""
Clear all contours in the glyph.
>>> glyph.clearContours()
"""
self._clearContours()
[docs] def _clearContours(self):
"""
Subclasses may override this method.
"""
for _ in range(len(self)):
self.removeContour(-1)
[docs] def removeOverlap(self):
"""
Perform a remove overlap operation on the contours.
>>> glyph.removeOverlap()
The behavior of this may vary across environments.
"""
self._removeOverlap()
[docs] def _removeOverlap(self):
"""
Subclasses must implement this method.
"""
self.raiseNotImplementedError()
# Components
def _setGlyphInComponent(self, component):
if component.glyph is None:
component.glyph = self
components = dynamicProperty(
"components",
"""
An :ref:`type-immutable-list` of all components in the glyph.
>>> components = glyph.components
The list will contain :class:`BaseComponent` objects.
"""
)
[docs] def _get_components(self):
"""
Subclasses may override this method.
"""
return tuple([self._getitem__components(i) for
i in range(self._len__components())])
def _len__components(self):
return self._lenComponents()
[docs] def _lenComponents(self, **kwargs):
"""
This must return an integer indicating
the number of components in the glyph.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def _getitem__components(self, index):
index = normalizers.normalizeIndex(index)
if index >= self._len__components():
raise ValueError("No component located at index %d." % index)
component = self._getComponent(index)
self._setGlyphInComponent(component)
return component
[docs] def _getComponent(self, index, **kwargs):
"""
This must return a wrapped component.
index will be a valid index.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def _getComponentIndex(self, component):
for i, other in enumerate(self.components):
if component == other:
return i
raise FontPartsError("The component could not be found.")
[docs] def appendComponent(self, baseGlyph=None, offset=None, scale=None, component=None):
"""
Append a component to this glyph.
>>> component = glyph.appendComponent("A")
This will return a :class:`BaseComponent` object representing
the new component in the glyph. ``offset`` indicates the x and
y shift values that should be applied to the appended component.
It must be a :ref:`type-coordinate` value or ``None``. If
``None`` is given, the offset will be ``(0, 0)``.
>>> component = glyph.appendComponent("A", offset=(10, 20))
``scale`` indicates the x and y scale values that should be
applied to the appended component. It must be a
:ref:`type-scale` value or ``None``. If ``None`` is given,
the scale will be ``(1.0, 1.0)``.
>>> component = glyph.appendComponent("A", scale=(1.0, 2.0))
``component`` may be a :class:`BaseComponent` object from which
attribute values will be copied. If ``baseGlyph``, ``offset``
or ``scale`` are specified as arguments, those values will be used
instead of the values in the given component object.
"""
identifier = None
sxy = 0
syx = 0
if component is not None:
component = normalizers.normalizeComponent(component)
if baseGlyph is None:
baseGlyph = component.baseGlyph
sx, sxy, syx, sy, ox, oy = component.transformation
if offset is None:
offset = (ox, oy)
if scale is None:
scale = (sx, sy)
if baseGlyph is None:
baseGlyph = component.baseGlyph
if component.identifier is not None:
existing = set([c.identifier for c in self.components if c.identifier is not None])
if component.identifier not in existing:
identifier = component.identifier
baseGlyph = normalizers.normalizeGlyphName(baseGlyph)
if self.name == baseGlyph:
raise FontPartsError(("A glyph cannot contain a component referencing itself."))
if offset is None:
offset = (0, 0)
if scale is None:
scale = (1, 1)
offset = normalizers.normalizeTransformationOffset(offset)
scale = normalizers.normalizeTransformationScale(scale)
ox, oy = offset
sx, sy = scale
transformation = (sx, sxy, syx, sy, ox, oy)
identifier = normalizers.normalizeIdentifier(identifier)
return self._appendComponent(baseGlyph, transformation=transformation, identifier=identifier)
[docs] def _appendComponent(self, baseGlyph, transformation=None, identifier=None, **kwargs):
"""
baseGlyph will be a valid glyph name.
The baseGlyph may or may not be in the layer.
offset will be a valid offset (x, y).
scale will be a valid scale (x, y).
identifier will be a valid, nonconflicting identifier.
This must return the new component.
Subclasses may override this method.
"""
pointPen = self.getPointPen()
pointPen.addComponent(baseGlyph, transformation=transformation, identifier=identifier)
return self.components[-1]
[docs] def removeComponent(self, component):
"""
Remove ``component`` from the glyph.
>>> glyph.removeComponent(component)
``component`` may be a :ref:`BaseComponent` or an
:ref:`type-int` representing a component index.
"""
if isinstance(component, int):
index = component
else:
index = self._getComponentIndex(component)
index = normalizers.normalizeIndex(index)
if index >= self._len__components():
raise ValueError("No component located at index %d." % index)
self._removeComponent(index)
[docs] def _removeComponent(self, index, **kwargs):
"""
index will be a valid index.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def clearComponents(self):
"""
Clear all components in the glyph.
>>> glyph.clearComponents()
"""
self._clearComponents()
[docs] def _clearComponents(self):
"""
Subclasses may override this method.
"""
for _ in range(self._len__components()):
self.removeComponent(-1)
[docs] def decompose(self):
"""
Decompose all components in the glyph to contours.
>>> glyph.decompose()
"""
self._decompose()
[docs] def _decompose(self):
"""
Subclasses may override this method.
"""
for component in self.components:
component.decompose()
# Anchors
def _setGlyphInAnchor(self, anchor):
if anchor.glyph is None:
anchor.glyph = self
anchors = dynamicProperty(
"anchors",
"""
An :ref:`type-immutable-list` of all anchors in the glyph.
>>> anchors = glyph.anchors
The list will contain :class:`BaseAnchor` objects.
"""
)
[docs] def _get_anchors(self):
"""
Subclasses may override this method.
"""
return tuple([self._getitem__anchors(i) for
i in range(self._len__anchors())])
def _len__anchors(self):
return self._lenAnchors()
[docs] def _lenAnchors(self, **kwargs):
"""
This must return an integer indicating
the number of anchors in the glyph.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def _getitem__anchors(self, index):
index = normalizers.normalizeIndex(index)
if index >= self._len__anchors():
raise ValueError("No anchor located at index %d." % index)
anchor = self._getAnchor(index)
self._setGlyphInAnchor(anchor)
return anchor
[docs] def _getAnchor(self, index, **kwargs):
"""
This must return a wrapped anchor.
index will be a valid index.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def _getAnchorIndex(self, anchor):
for i, other in enumerate(self.anchors):
if anchor == other:
return i
raise FontPartsError("The anchor could not be found.")
[docs] def appendAnchor(self, name=None, position=None, color=None, anchor=None):
"""
Append an anchor to this glyph.
>>> anchor = glyph.appendAnchor("top", (10, 20))
This will return a :class:`BaseAnchor` object representing
the new anchor in the glyph. ``name`` indicated the name to
be assigned to the anchor. It must be a :ref:`type-string`
or ``None``. ``position`` indicates the x and y location
to be applied to the anchor. It must be a
:ref:`type-coordinate` value. ``color`` indicates the color
to be applied to the anchor. It must be a :ref:`type-color`
or ``None``.
>>> anchor = glyph.appendAnchor("top", (10, 20), color=(1, 0, 0, 1))
``anchor`` may be a :class:`BaseAnchor` object from which
attribute values will be copied. If ``name``, ``position``
or ``color`` are specified as arguments, those values will
be used instead of the values in the given anchor object.
"""
identifier = None
if anchor is not None:
anchor = normalizers.normalizeAnchor(anchor)
if name is None:
name = anchor.name
if position is None:
position = anchor.position
if color is None:
color = anchor.color
if anchor.identifier is not None:
existing = set([a.identifier for a in self.anchors if a.identifier is not None])
if anchor.identifier not in existing:
identifier = anchor.identifier
name = normalizers.normalizeAnchorName(name)
position = normalizers.normalizeCoordinateTuple(position)
if color is not None:
color = normalizers.normalizeColor(color)
identifier = normalizers.normalizeIdentifier(identifier)
return self._appendAnchor(name, position=position, color=color, identifier=identifier)
[docs] def _appendAnchor(self, name, position=None, color=None, identifier=None, **kwargs):
"""
name will be a valid anchor name.
position will be a valid position (x, y).
color will be None or a valid color.
identifier will be a valid, nonconflicting identifier.
This must return the new anchor.
Subclasses may override this method.
"""
self.raiseNotImplementedError()
[docs] def removeAnchor(self, anchor):
"""
Remove ``anchor`` from the glyph.
>>> glyph.removeAnchor(anchor)
``anchor`` may be an :ref:`BaseAnchor` or an
:ref:`type-int` representing an anchor index.
"""
if isinstance(anchor, int):
index = anchor
else:
index = self._getAnchorIndex(anchor)
index = normalizers.normalizeIndex(index)
if index >= self._len__anchors():
raise ValueError("No anchor located at index %d." % index)
self._removeAnchor(index)
[docs] def _removeAnchor(self, index, **kwargs):
"""
index will be a valid index.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def clearAnchors(self):
"""
Clear all anchors in the glyph.
>>> glyph.clearAnchors()
"""
self._clearAnchors()
[docs] def _clearAnchors(self):
"""
Subclasses may override this method.
"""
for _ in range(self._len__anchors()):
self.removeAnchor(-1)
# ----------
# Guidelines
# ----------
def _setGlyphInGuideline(self, guideline):
if guideline.glyph is None:
guideline.glyph = self
guidelines = dynamicProperty(
"guidelines",
"""
An :ref:`type-immutable-list` of all guidelines in the glyph.
>>> guidelines = glyph.guidelines
The list will contain :class:`BaseGuideline` objects.
"""
)
[docs] def _get_guidelines(self):
"""
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 guidelines in the glyph.
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._setGlyphInGuideline(guideline)
return guideline
[docs] def _getGuideline(self, index, **kwargs):
"""
This must return a wrapped guideline.
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 guideline to this glyph.
>>> guideline = glyph.appendGuideline((100, 0), 90)
This will return a :class:`BaseGuideline` object representing
the new guideline in the glyph. ``position`` indicates the
x and y location to be used as the center point of the anchor.
It must be a :ref:`type-coordinate` value. ``angle`` indicates
the angle of the guideline, in degrees. This must be a
:ref:`type-int-float` between 0 and 360. ``name`` indicates
an name to be assigned to the guideline. It must be a
:ref:`type-string` or ``None``.
>>> guideline = glyph.appendGuideline((100, 0), 90, name="left")
``color`` indicates the color to be applied to the guideline.
It must be a :ref:`type-color` or ``None``.
>>> guideline = glyph.appendGuideline((100, 0), 90, color=(1, 0, 0, 1))
``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.glyph = self
return guideline
[docs] def _appendGuideline(self, position, angle, name=None, color=None, identifier=None, **kwargs):
"""
position will be a valid position (x, y).
angle will be a valid angle.
name will be a valid guideline name or None.
color will be a valid color or None .
identifier will be a valid, nonconflicting identifier.
This must return the new guideline.
Subclasses may override this method.
"""
self.raiseNotImplementedError()
[docs] def removeGuideline(self, guideline):
"""
Remove ``guideline`` from the glyph.
>>> glyph.removeGuideline(guideline)
``guideline`` may be a :ref:`BaseGuideline` or an
:ref:`type-int` representing an 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):
"""
index will be a valid index.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def clearGuidelines(self):
"""
Clear all guidelines in the glyph.
>>> glyph.clearGuidelines()
"""
self._clearGuidelines()
[docs] def _clearGuidelines(self):
"""
Subclasses may override this method.
"""
for _ in range(self._len__guidelines()):
self.removeGuideline(-1)
# ------------------
# Data Normalization
# ------------------
[docs] def round(self):
"""
Round coordinates to the nearest integer.
>>> glyph.round()
This applies to the following:
- width
- height
- contours
- components
- anchors
- guidelines
"""
self._round()
[docs] def _round(self):
"""
Subclasses may override this method.
"""
for contour in self.contours:
contour.round()
for component in self.components:
component.round()
for anchor in self.anchors:
anchor.round()
for guideline in self.guidelines:
guideline.round()
self.width = normalizers.normalizeVisualRounding(self.width)
self.height = normalizers.normalizeVisualRounding(self.height)
def correctDirection(self, trueType=False):
"""
Correct the winding direction of the contours following
the PostScript recommendations.
>>> glyph.correctDirection()
If ``trueType`` is ``True`` the TrueType recommendations
will be followed.
"""
self._correctDirection(trueType=trueType)
def _correctDirection(self, trueType=False, **kwargs):
"""
Subclasses may override this method.
"""
self.raiseNotImplementedError()
def autoContourOrder(self):
"""
Automatically order the contours based on heuristics.
>>> glyph.autoContourOrder()
The results of this may vary across environments.
"""
self._autoContourOrder()
def _autoContourOrder(self, **kwargs):
"""
Sorting is based on (in this order):
- the (negative) point count
- the (negative) segment count
- x value of the center of the contour rounded to a threshold
- y value of the center of the contour rounded to a threshold
(such threshold is calculated as the smallest contour width
or height in the glyph divided by two)
- the (negative) surface of the bounding box of the contour: width * height
the latter is a safety net for for instances like a very thin 'O' where the
x centers could be close enough to rely on the y for the sort which could
very well be the same for both contours. We use the _negative_ of the surface
to ensure that larger contours appear first, which seems more natural.
"""
tempContourList = []
contourList = []
xThreshold = None
yThreshold = None
for contour in self:
bounds = contour.bounds
if bounds is None:
continue
xMin, yMin, xMax, yMax = bounds
width = xMax - xMin
height = yMax - yMin
xC = 0.5 * (xMin + xMax)
yC = 0.5 * (yMin + yMax)
xTh = abs(width * 0.5)
yTh = abs(height * 0.5)
if xThreshold is None or xThreshold > xTh:
xThreshold = xTh
if yThreshold is None or yThreshold > yTh:
yThreshold = yTh
tempContourList.append((-len(contour.points), -len(contour.segments), xC, yC, -(width * height), contour))
for points, segments, x, y, surface, contour in tempContourList:
contourList.append((points, segments, FuzzyNumber(x, xThreshold), FuzzyNumber(y, yThreshold), surface, contour))
contourList.sort()
self.clearContours()
for points, segments, xO, yO, surface, contour in contourList:
self.appendContour(contour)
# --------------
# Transformation
# --------------
[docs] def scaleBy(self, value, origin=None, width=False, height=False):
"""
%s
**width** indicates if the glyph's width should be scaled.
**height** indicates if the glyph's height should be scaled.
The origin must not be specified when scaling the width or height.
"""
value = normalizers.normalizeTransformationScale(value)
if origin is None:
origin = (0, 0)
origin = normalizers.normalizeCoordinateTuple(origin)
if origin != (0, 0) and (width or height):
raise FontPartsError(("The origin must not be set when "
"scaling the width or height."))
super(BaseGlyph, self).scaleBy(value, origin=origin)
sX, sY = value
if width:
self._scaleWidthBy(sX)
if height:
self._scaleHeightBy(sY)
scaleBy.__doc__ %= TransformationMixin.scaleBy.__doc__
def _scaleWidthBy(self, value):
"""
Subclasses may override this method.
"""
self.width *= value
def _scaleHeightBy(self, value):
"""
Subclasses may override this method.
"""
self.height *= value
# --------------------
# Interpolation & Math
# --------------------
def toMathGlyph(self, scaleComponentTransform=True, strict=False):
"""
Returns the glyph as an object that follows the
`MathGlyph protocol <https://github.com/typesupply/fontMath>`_.
>>> mg = glyph.toMathGlyph()
**scaleComponentTransform** Enables the MathGlyph
`scaleComponentTransform` option.
**strict** Enables the MathGlyph `strict` option.
"""
return self._toMathGlyph(scaleComponentTransform=scaleComponentTransform, strict=strict)
def _toMathGlyph(self, scaleComponentTransform=True, strict=False):
"""
Subclasses may override this method.
"""
import fontMath
mathGlyph = fontMath.MathGlyph(
None,
scaleComponentTransform=scaleComponentTransform,
strict=strict
)
pen = mathGlyph.getPointPen()
self.drawPoints(pen)
for anchor in self.anchors:
d = dict(
x=anchor.x,
y=anchor.y,
name=anchor.name,
identifier=anchor.identifier,
color=anchor.color
)
mathGlyph.anchors.append(d)
for guideline in self.guidelines:
d = dict(
x=guideline.x,
y=guideline.y,
angle=guideline.angle,
name=guideline.name,
identifier=guideline.identifier,
color=guideline.color
)
mathGlyph.guidelines.append(d)
mathGlyph.lib = deepcopy(self.lib)
mathGlyph.name = self.name
mathGlyph.unicodes = self.unicodes
mathGlyph.width = self.width
mathGlyph.height = self.height
mathGlyph.note = self.note
return mathGlyph
def fromMathGlyph(self, mathGlyph, filterRedundantPoints=True):
"""
Replaces the contents of this glyph with the contents of ``mathGlyph``.
>>> glyph.fromMathGlyph(mg)
``mathGlyph`` must be an object following the
`MathGlyph protocol <https://github.com/typesupply/fontMath>`_.
**filterRedundantPoints** enables the MathGlyph `drawPoints`
`filterRedundantPoints` option.
"""
return self._fromMathGlyph(mathGlyph, toThisGlyph=True, filterRedundantPoints=filterRedundantPoints)
def _fromMathGlyph(self, mathGlyph, toThisGlyph=False, filterRedundantPoints=True):
# make the destination
if toThisGlyph:
copied = self
copied.clear()
else:
copyClass = self.copyClass
if copyClass is None:
copyClass = self.__class__
copied = copyClass()
# populate
pen = copied.getPointPen()
mathGlyph.drawPoints(pen, filterRedundantPoints=filterRedundantPoints)
for anchor in mathGlyph.anchors:
a = copied.appendAnchor(
name=anchor["name"],
position=(anchor["x"], anchor["y"]),
color=anchor["color"]
)
identifier = anchor.get("identifier")
if identifier is not None:
a._setIdentifier(identifier)
for guideline in mathGlyph.guidelines:
g = copied.appendGuideline(
position=(guideline["x"], guideline["y"]),
angle=guideline["angle"],
name=guideline["name"],
color=guideline["color"]
)
identifier = guideline.get("identifier")
if identifier is not None:
g._setIdentifier(identifier)
copied.lib.update(mathGlyph.lib)
if not toThisGlyph:
copied.name = mathGlyph.name
copied.unicodes = mathGlyph.unicodes
copied.width = mathGlyph.width
copied.height = mathGlyph.height
copied.note = mathGlyph.note
return copied
[docs] def __mul__(self, factor):
"""
Subclasses may override this method.
"""
mathGlyph = self._toMathGlyph()
result = mathGlyph * factor
copied = self._fromMathGlyph(result)
return copied
__rmul__ = __mul__
def __truediv__(self, factor):
"""
Subclasses may override this method.
"""
mathGlyph = self._toMathGlyph()
result = mathGlyph / factor
copied = self._fromMathGlyph(result)
return copied
# py2 support
__div__ = __truediv__
[docs] def __add__(self, other):
"""
Subclasses may override this method.
"""
selfMathGlyph = self._toMathGlyph()
otherMathGlyph = other._toMathGlyph()
result = selfMathGlyph + otherMathGlyph
copied = self._fromMathGlyph(result)
return copied
[docs] def __sub__(self, other):
"""
Subclasses may override this method.
"""
selfMathGlyph = self._toMathGlyph()
otherMathGlyph = other._toMathGlyph()
result = selfMathGlyph - otherMathGlyph
copied = self._fromMathGlyph(result)
return copied
[docs] def interpolate(self, factor, minGlyph, maxGlyph,
round=True, suppressError=True):
"""
Interpolate the contents of this glyph at location ``factor``
in a linear interpolation between ``minGlyph`` and ``maxGlyph``.
>>> glyph.interpolate(0.5, otherGlyph1, otherGlyph2)
``factor`` may be a :ref:`type-int-float` or a tuple containing
two :ref:`type-int-float` values representing x and y factors.
>>> glyph.interpolate((0.5, 1.0), otherGlyph1, otherGlyph2)
``minGlyph`` must be a :class:`BaseGlyph` and will be located at 0.0
in the interpolation range. ``maxGlyph`` must be a :class:`BaseGlyph`
and will be located at 1.0 in the interpolation range. If ``round``
is ``True``, the contents of the glyph will be rounded to integers
after the interpolation is performed.
>>> glyph.interpolate(0.5, otherGlyph1, otherGlyph2, round=True)
This method assumes that ``minGlyph`` and ``maxGlyph`` are completely
compatible with each other for interpolation. If not, any errors
encountered will raise a :class:`FontPartsError`. If ``suppressError``
is ``True``, no exception will be raised and errors will be silently
ignored.
"""
factor = normalizers.normalizeInterpolationFactor(factor)
if not isinstance(minGlyph, BaseGlyph):
raise TypeError(("Interpolation to an instance of %r can not be "
"performed from an instance of %r.")
% (self.__class__.__name__,
minGlyph.__class__.__name__))
if not isinstance(maxGlyph, BaseGlyph):
raise TypeError(("Interpolation to an instance of %r can not be "
"performed from an instance of %r.")
% (self.__class__.__name__,
maxGlyph.__class__.__name__))
round = normalizers.normalizeBoolean(round)
suppressError = normalizers.normalizeBoolean(suppressError)
self._interpolate(factor, minGlyph, maxGlyph,
round=round, suppressError=suppressError)
[docs] def _interpolate(self, factor, minGlyph, maxGlyph,
round=True, suppressError=True):
"""
Subclasses may override this method.
"""
from fontMath.mathFunctions import setRoundIntegerFunction
setRoundIntegerFunction(normalizers.normalizeVisualRounding)
minGlyph = minGlyph._toMathGlyph()
maxGlyph = maxGlyph._toMathGlyph()
try:
result = interpolate(minGlyph, maxGlyph, factor)
except IndexError:
result = None
if result is None and not suppressError:
raise FontPartsError(("Glyphs '%s' and '%s' could not be "
"interpolated.")
% (minGlyph.name, maxGlyph.name))
if result is not None:
if round:
result = result.round()
self._fromMathGlyph(result, toThisGlyph=True)
compatibilityReporterClass = GlyphCompatibilityReporter
@staticmethod
def _checkPairs(object1, object2, reporter, reporterObject):
compatibility = object1.isCompatible(object2)[1]
if compatibility.fatal or compatibility.warning:
if compatibility.fatal:
reporter.fatal = True
if compatibility.warning:
reporter.warning = True
reporterObject.append(compatibility)
[docs] def isCompatible(self, other):
"""
Evaluate the interpolation compatibility of this glyph
and ``other``.
>>> compatible, report = self.isCompatible(otherGlyph)
>>> compatible
False
This will return a :ref:`type-bool` indicating if this glyph is
compatible with ``other`` and a :class:`GlyphCompatibilityReporter`
containing a detailed report about compatibility errors.
"""
return super(BaseGlyph, self).isCompatible(other, BaseGlyph)
[docs] def _isCompatible(self, other, reporter):
"""
This is the environment implementation of
:meth:`BaseGlyph.isCompatible`.
Subclasses may override this method.
"""
glyph1 = self
glyph2 = other
# contour count
if len(self.contours) != len(glyph2.contours):
reporter.fatal = True
reporter.contourCountDifference = True
# contour pairs
for i in range(min(len(glyph1), len(glyph2))):
contour1 = glyph1[i]
contour2 = glyph2[i]
self._checkPairs(contour1, contour2, reporter, reporter.contours)
# component count
if len(glyph1.components) != len(glyph2.components):
reporter.fatal = True
reporter.componentCountDifference = True
# component check
component_diff = []
selfComponents = [component.baseGlyph for component in glyph1.components]
otherComponents = [component.baseGlyph for component in glyph2.components]
for index, (left, right) in enumerate(
zip_longest(selfComponents, otherComponents)
):
if left != right:
component_diff.append((index, left, right))
if component_diff:
reporter.warning = True
reporter.componentDifferences = component_diff
if not reporter.componentCountDifference and set(selfComponents) == set(
otherComponents
):
reporter.componentOrderDifference = True
selfComponents_counted_set = collections.Counter(selfComponents)
otherComponents_counted_set = collections.Counter(otherComponents)
missing_from_glyph1 = (
otherComponents_counted_set - selfComponents_counted_set
)
if missing_from_glyph1:
reporter.fatal = True
reporter.componentsMissingFromGlyph1 = sorted(
missing_from_glyph1.elements()
)
missing_from_glyph2 = (
selfComponents_counted_set - otherComponents_counted_set
)
if missing_from_glyph2:
reporter.fatal = True
reporter.componentsMissingFromGlyph2 = sorted(
missing_from_glyph2.elements()
)
# guideline count
if len(self.guidelines) != len(glyph2.guidelines):
reporter.warning = True
reporter.guidelineCountDifference = True
# guideline check
selfGuidelines = []
otherGuidelines = []
for source, names in ((self, selfGuidelines),
(other, otherGuidelines)):
for i, guideline in enumerate(source.guidelines):
names.append((guideline.name, i))
guidelines1 = set(selfGuidelines)
guidelines2 = set(otherGuidelines)
if len(guidelines1.difference(guidelines2)) != 0:
reporter.warning = True
reporter.guidelinesMissingFromGlyph2 = list(
guidelines1.difference(guidelines2))
if len(guidelines2.difference(guidelines1)) != 0:
reporter.warning = True
reporter.guidelinesMissingFromGlyph1 = list(
guidelines2.difference(guidelines1))
# anchor count
if len(self.anchors) != len(glyph2.anchors):
reporter.warning = True
reporter.anchorCountDifference = True
# anchor check
anchor_diff = []
selfAnchors = [anchor.name for anchor in glyph1.anchors]
otherAnchors = [anchor.name for anchor in glyph2.anchors]
for index, (left, right) in enumerate(zip_longest(selfAnchors, otherAnchors)):
if left != right:
anchor_diff.append((index, left, right))
if anchor_diff:
reporter.warning = True
reporter.anchorDifferences = anchor_diff
if not reporter.anchorCountDifference and set(selfAnchors) == set(
otherAnchors
):
reporter.anchorOrderDifference = True
selfAnchors_counted_set = collections.Counter(selfAnchors)
otherAnchors_counted_set = collections.Counter(otherAnchors)
missing_from_glyph1 = otherAnchors_counted_set - selfAnchors_counted_set
if missing_from_glyph1:
reporter.anchorsMissingFromGlyph1 = sorted(
missing_from_glyph1.elements()
)
missing_from_glyph2 = selfAnchors_counted_set - otherAnchors_counted_set
if missing_from_glyph2:
reporter.anchorsMissingFromGlyph2 = sorted(
missing_from_glyph2.elements()
)
# ------------
# Data Queries
# ------------
[docs] def pointInside(self, point):
"""
Determine if ``point`` is in the black or white of the glyph.
>>> glyph.pointInside((40, 65))
True
``point`` must be a :ref:`type-coordinate`.
"""
point = normalizers.normalizeCoordinateTuple(point)
return self._pointInside(point)
[docs] def _pointInside(self, point):
"""
Subclasses may override this method.
"""
from fontTools.pens.pointInsidePen import PointInsidePen
pen = PointInsidePen(glyphSet=None, testPoint=point, evenOdd=False)
self.draw(pen)
return pen.getResult()
bounds = dynamicProperty(
"bounds",
"""
The bounds of the glyph in the form
``(x minimum, y minimum, x maximum, y maximum)`` or,
in the case of empty glyphs ``None``.
>>> glyph.bounds
(10, 30, 765, 643)
"""
)
def _get_base_bounds(self):
value = self._get_bounds()
if value is not None:
value = normalizers.normalizeBoundingBox(value)
return value
[docs] def _get_bounds(self):
"""
Subclasses may override this method.
"""
from fontTools.pens.boundsPen import BoundsPen
pen = BoundsPen(self.layer)
self.draw(pen)
return pen.bounds
area = dynamicProperty(
"area",
"""
The area of the glyph as a :ref:`type-int-float` or,
in the case of empty glyphs ``None``.
>>> glyph.area
583
"""
)
def _get_base_area(self):
value = self._get_area()
if value is not None:
value = normalizers.normalizeArea(value)
return value
def _get_area(self):
"""
Subclasses may override this method.
"""
from fontTools.pens.areaPen import AreaPen
pen = AreaPen(self.layer)
self.draw(pen)
return abs(pen.value)
# -----------------
# Layer Interaction
# -----------------
layers = dynamicProperty(
"layers",
"""
Immutable tuple of the glyph's layers.
>>> glyphLayers = glyph.layers
This will return a tuple of all :ref:`type-glyph-layer` in the glyph.
"""
)
def _get_layers(self, **kwargs):
font = self.font
if font is None:
return tuple()
glyphs = []
for layer in font.layers:
if self.name in layer:
glyphs.append(layer[self.name])
return tuple(glyphs)
# get
[docs] def getLayer(self, name):
"""
Get the :ref:`type-glyph-layer` with ``name`` in this glyph.
>>> glyphLayer = glyph.getLayer("foreground")
"""
name = normalizers.normalizeLayerName(name)
return self._getLayer(name)
[docs] def _getLayer(self, name, **kwargs):
"""
name will be a string, but there may not be a
layer with a name matching the string. If not,
a ``ValueError`` must be raised.
Subclasses may override this method.
"""
for glyph in self.layers:
if glyph.layer.name == name:
return glyph
raise ValueError("No layer named '%s' in glyph '%s'."
% (name, self.name))
# new
[docs] def newLayer(self, name):
"""
Make a new layer with ``name`` in this glyph.
>>> glyphLayer = glyph.newLayer("background")
This will return the new :ref:`type-glyph-layer`.
If the layer already exists in this glyph, it
will be cleared.
"""
layerName = name
glyphName = self.name
layerName = normalizers.normalizeLayerName(layerName)
for glyph in self.layers:
if glyph.layer.name == layerName:
layer = glyph.layer
layer.removeGlyph(glyphName)
break
glyph = self._newLayer(name=layerName)
layer = self.font.getLayer(layerName)
# layer._setLayerInGlyph(glyph)
return glyph
[docs] def _newLayer(self, name, **kwargs):
"""
name will be a string representing a valid layer
name. The name will have been tested to make sure
that no layer in the glyph already has the name.
This must returned the new glyph.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# remove
[docs] def removeLayer(self, layer):
"""
Remove ``layer`` from this glyph.
>>> glyph.removeLayer("background")
Layer can be a :ref:`type-glyph-layer` or a :ref:`type-string`
representing a layer name.
"""
if isinstance(layer, BaseGlyph):
layer = layer.layer.name
layerName = layer
layerName = normalizers.normalizeLayerName(layerName)
if self._getLayer(layerName).layer.name == layerName:
self._removeLayer(layerName)
[docs] def _removeLayer(self, name, **kwargs):
"""
name will be a valid layer name. It will
represent an existing layer in the font.
Subclasses may override this method.
"""
self.raiseNotImplementedError()
# -----
# Image
# -----
image = dynamicProperty(
"base_image",
"The :class:`BaseImage` for the glyph."
)
def _get_base_image(self):
image = self._get_image()
if image.glyph is None:
image.glyph = self
return image
[docs] def _get_image(self):
"""
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def addImage(self, path=None, data=None, scale=None,
position=None, color=None):
"""
Set the image in the glyph. This will return the
assigned :class:`BaseImage`. The image data can be
defined via ``path`` to an image file:
>>> image = glyph.addImage(path="/path/to/my/image.png")
The image data can be defined with raw image data
via ``data``.
>>> image = glyph.addImage(data=someImageData)
If ``path`` and ``data`` are both provided, a
:class:`FontPartsError` will be raised. The supported
image formats will vary across environments. Refer
to :class:`BaseImage` for complete details.
``scale`` indicates the x and y scale values that should be
applied to the image. It must be a :ref:`type-scale` value
or ``None``.
>>> image = glyph.addImage(path="/p/t/image.png", scale=(0.5, 1.0))
``position`` indicates the x and y location of the lower left
point of the image.
>>> image = glyph.addImage(path="/p/t/image.png", position=(10, 20))
``color`` indicates the color to be applied to the image. It must
be a :ref:`type-color` or ``None``.
>>> image = glyph.addImage(path="/p/t/image.png", color=(1, 0, 0, 0.5))
"""
if path is not None and data is not None:
raise FontPartsError("Only path or data may be defined, not both.")
if scale is None:
scale = (1, 1)
if position is None:
position = (0, 0)
scale = normalizers.normalizeTransformationScale(scale)
position = normalizers.normalizeTransformationOffset(position)
if color is not None:
color = normalizers.normalizeColor(color)
sx, sy = scale
ox, oy = position
transformation = (sx, 0, 0, sy, ox, oy)
if path is not None:
if not os.path.exists(path):
raise IOError("No image located at '%s'." % path)
f = open(path, "rb")
data = f.read()
f.close()
self._addImage(data=data, transformation=transformation, color=color)
return self.image
[docs] def _addImage(self, data, transformation=None, color=None):
"""
data will be raw, unnormalized image data.
Each environment may have different possible
formats, so this is unspecified.
transformation will be a valid transformation matrix.
color will be a color tuple or None.
This must return an Image object. Assigning it
to the glyph will be handled by the base class.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def clearImage(self):
"""
Remove the image from the glyph.
>>> glyph.clearImage()
"""
if self.image is not None:
self._clearImage()
[docs] def _clearImage(self, **kwargs):
"""
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# ----------
# Mark color
# ----------
markColor = dynamicProperty(
"base_markColor",
"""
The glyph's mark color.
>>> glyph.markColor
(1, 0, 0, 0.5)
>>> glyph.markColor = None
The value may be a :ref:`type-color` or ``None``.
"""
)
def _get_base_markColor(self):
value = self._get_markColor()
if value is not None:
value = normalizers.normalizeColor(value)
value = Color(value)
return value
def _set_base_markColor(self, value):
if value is not None:
value = normalizers.normalizeColor(value)
self._set_markColor(value)
[docs] def _get_markColor(self):
"""
Return the mark color value as a color tuple or None.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def _set_markColor(self, value):
"""
value will be a color tuple or None.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# ----
# Note
# ----
note = dynamicProperty(
"base_note",
"""
The glyph's note.
>>> glyph.note
"P.B. said this looks 'awesome.'"
>>> glyph.note = "P.B. said this looks 'AWESOME.'"
The value may be a :ref:`type-string` or ``None``.
"""
)
def _get_base_note(self):
value = self._get_note()
if value is not None:
value = normalizers.normalizeGlyphNote(value)
return value
def _set_base_note(self, value):
if value is not None:
value = normalizers.normalizeGlyphNote(value)
self._set_note(value)
[docs] def _get_note(self):
"""
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs] def _set_note(self, value):
"""
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# ---
# Lib
# ---
lib = dynamicProperty(
"base_lib",
"""
The :class:`BaseLib` for the glyph.
>>> lib = glyph.lib
"""
)
def _get_base_lib(self):
lib = self._get_lib()
lib.glyph = self
return lib
[docs] def _get_lib(self):
"""
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# --------
# Temp Lib
# --------
tempLib = dynamicProperty(
"base_tempLib",
"""
The :class:`BaseLib` for the glyph.
>>> tempLib = glyph.tempLib
"""
)
def _get_base_tempLib(self):
lib = self._get_tempLib()
lib.glyph = self
return lib
def _get_tempLib(self):
"""
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# ---
# API
# ---
def isEmpty(self):
"""
This will return :ref:`type-bool` indicating if there are contours and/or
components in the glyph.
>>> glyph.isEmpty()
Note: This method only checks for the presence of contours and components.
Other attributes (guidelines, anchors, a lib, etc.) will not affect what
this method returns.
"""
if self.contours:
return False
if self.components:
return False
return True
def loadFromGLIF(self, glifData):
"""
Reads ``glifData``, in
`GLIF format <http://unifiedfontobject.org/versions/ufo3/glyphs/glif/>`_,
into this glyph.
>>> glyph.readGlyphFromString(xmlData)
"""
self._loadFromGLIF(glifData)
def _loadFromGLIF(self, glifData):
"""
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def dumpToGLIF(self, glyphFormatVersion=2):
"""
This will return the glyph's contents as a string in
`GLIF format <http://unifiedfontobject.org/versions/ufo3/glyphs/glif/>`_.
>>> xml = glyph.writeGlyphToString()
``glyphFormatVersion`` must be a :ref:`type-int` that defines
the preferred GLIF format version.
"""
glyphFormatVersion = normalizers.normalizeGlyphFormatVersion(
glyphFormatVersion)
return self._dumpToGLIF(glyphFormatVersion)
def _dumpToGLIF(self, glyphFormatVersion):
"""
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# ---------
# Selection
# ---------
# contours
selectedContours = dynamicProperty(
"base_selectedContours",
"""
An :ref:`type-immutable-list` of contours selected in the glyph.
>>> contours = glyph.selectedContours:
>>> glyph.selectedContours = otherContours
It is possible to use a list of :ref:`type-int` representing
contour indexes when setting the selected contours.
>>> glyph.selectedContours = [0, 2]
"""
)
def _get_base_selectedContours(self):
selected = tuple([normalizers.normalizeContour(contour) for
contour in self._get_selectedContours()])
return selected
def _get_selectedContours(self):
"""
Subclasses may override this method.
"""
return self._getSelectedSubObjects(self.contours)
def _set_base_selectedContours(self, value):
normalized = []
for i in value:
if isinstance(i, int):
i = normalizers.normalizeIndex(i)
else:
i = normalizers.normalizeContour(i)
normalized.append(i)
self._set_selectedContours(normalized)
def _set_selectedContours(self, value):
"""
Subclasses may override this method.
"""
return self._setSelectedSubObjects(self.contours, value)
# components
selectedComponents = dynamicProperty(
"base_selectedComponents",
"""
An :ref:`type-immutable-list` of components selected in the glyph.
>>> components = glyph.selectedComponents:
>>> glyph.selectedComponents = otherComponents
It is possible to use a list of :ref:`type-int` representing
component indexes when setting the selected components.
>>> glyph.selectedComponents = [0, 2]
"""
)
def _get_base_selectedComponents(self):
selected = tuple([normalizers.normalizeComponent(component) for
component in self._get_selectedComponents()])
return selected
def _get_selectedComponents(self):
"""
Subclasses may override this method.
"""
return self._getSelectedSubObjects(self.components)
def _set_base_selectedComponents(self, value):
normalized = []
for i in value:
if isinstance(i, int):
i = normalizers.normalizeIndex(i)
else:
i = normalizers.normalizeComponent(i)
normalized.append(i)
self._set_selectedComponents(normalized)
def _set_selectedComponents(self, value):
"""
Subclasses may override this method.
"""
return self._setSelectedSubObjects(self.components, value)
# anchors
selectedAnchors = dynamicProperty(
"base_selectedAnchors",
"""
An :ref:`type-immutable-list` of anchors selected in the glyph.
>>> anchors = glyph.selectedAnchors:
>>> glyph.selectedAnchors = otherAnchors
It is possible to use a list of :ref:`type-int` representing
anchor indexes when setting the selected anchors.
>>> glyph.selectedAnchors = [0, 2]
"""
)
def _get_base_selectedAnchors(self):
selected = tuple([normalizers.normalizeAnchor(anchor) for
anchor in self._get_selectedAnchors()])
return selected
def _get_selectedAnchors(self):
"""
Subclasses may override this method.
"""
return self._getSelectedSubObjects(self.anchors)
def _set_base_selectedAnchors(self, value):
normalized = []
for i in value:
if isinstance(i, int):
i = normalizers.normalizeIndex(i)
else:
i = normalizers.normalizeAnchor(i)
normalized.append(i)
self._set_selectedAnchors(normalized)
def _set_selectedAnchors(self, value):
"""
Subclasses may override this method.
"""
return self._setSelectedSubObjects(self.anchors, value)
# guidelines
selectedGuidelines = dynamicProperty(
"base_selectedGuidelines",
"""
An :ref:`type-immutable-list` of guidelines selected in the glyph.
>>> guidelines = glyph.selectedGuidelines:
>>> glyph.selectedGuidelines = otherGuidelines
It is possible to use a list of :ref:`type-int` representing
guidelines indexes when setting the selected guidelines.
>>> glyph.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)