class AnchorCompatibilityReporter(BaseCompatibilityReporter): objectName = "Anchor" def __init__(self, anchor1, anchor2): super(AnchorCompatibilityReporter, self).__init__(anchor1, anchor2) self.nameDifference = False anchor1 = dynamicProperty("object1") anchor1Name = dynamicProperty("object1Name") anchor2 = dynamicProperty("object2") anchor2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): anchor1 = self.anchor1 anchor2 = self.anchor2 report = [] if self.nameDifference: name1 = anchor1.name name2 = anchor2.name text = ("{anchor1Name} has name {name1} | " "{anchor2Name} has name {name2}").format( anchor1Name=self.anchor1Name, name1=name1, anchor2Name=self.anchor2Name, name2=name2) report.append(self.formatWarningString(text)) if report or showOK: report.insert(0, self.title) return "\n".join(report)
class GuidelineCompatibilityReporter(BaseCompatibilityReporter): objectName = "Guideline" def __init__(self, guideline1, guideline2): super(GuidelineCompatibilityReporter, self).__init__(guideline1, guideline2) self.nameDifference = False guideline1 = dynamicProperty("object1") guideline1Name = dynamicProperty("object1Name") guideline2 = dynamicProperty("object2") guideline2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): guideline1 = self.guideline1 guideline2 = self.guideline2 report = [] if self.nameDifference: name1 = guideline1.name name2 = guideline2.name text = ("{guideline1Name} has name {name1} | " "{guideline2Name} has name {name2}").format( guideline1Name=self.guideline1Name, name1=name1, guideline2Name=self.guideline2Name, name2=name2) report.append(self.formatWarningString(text)) if report or showOK: report.insert(0, self.title) return "\n".join(report)
class ComponentCompatibilityReporter(BaseCompatibilityReporter): objectName = "Component" def __init__(self, component1, component2): super(ComponentCompatibilityReporter, self).__init__(component1, component2) self.baseDifference = False component1 = dynamicProperty("object1") component1Name = dynamicProperty("object1Name") component2 = dynamicProperty("object2") component2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): component1 = self.component1 component2 = self.component2 report = [] if self.baseDifference: name1 = component1.baseName name2 = component2.baseName text = ("{component1Name} has base glyph {name1} | " "{component2Name} has base glyph {name2}").format( component1Name=self.component1Name, name1=name1, component2Name=self.component2Name, name2=name2) report.append(self.formatWarningString(text)) if report or showOK: report.insert(0, self.title) return "\n".join(report)
class SegmentCompatibilityReporter(BaseCompatibilityReporter): objectName = "Segment" def __init__(self, contour1, contour2): super(SegmentCompatibilityReporter, self).__init__(contour1, contour2) self.typeDifference = False segment1 = dynamicProperty("object1") segment1Name = dynamicProperty("object1Name") segment2 = dynamicProperty("object2") segment2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): segment1 = self.segment1 segment2 = self.segment2 report = [] if self.typeDifference: type1 = segment1.type type2 = segment2.type text = "{segment1Name} is {type1} | {segment2Name} is {type2}".format( segment1Name=self.segment1Name, type1=type1, segment2Name=self.segment2Name, type2=type2) report.append(self.formatFatalString(text)) if report or showOK: report.insert(0, self.title) return "\n".join(report)
class ContourCompatibilityReporter(BaseCompatibilityReporter): objectName = "Contour" def __init__(self, contour1, contour2): super(ContourCompatibilityReporter, self).__init__(contour1, contour2) self.openDifference = False self.directionDifference = False self.segmentCountDifference = False self.segments = [] contour1 = dynamicProperty("object1") contour1Name = dynamicProperty("object1Name") contour2 = dynamicProperty("object2") contour2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): contour1 = self.contour1 contour2 = self.contour2 report = [] if self.segmentCountDifference: text = self.reportCountDifference(subObjectName="segments", object1Name=self.contour1Name, object1Count=len(contour1), object2Name=self.contour2Name, object2Count=len(contour2)) report.append(self.formatFatalString(text)) if self.openDifference: state1 = state2 = "closed" if contour1.open: state1 = "open" if contour2.open: state2 = "open" text = "{contour1Name} is {state1} | {contour2Name} is {state2}".format( contour1Name=self.contour1Name, state1=state1, contour2Name=self.contour2Name, state2=state2) report.append(self.formatFatalString(text)) if self.directionDifference: state1 = state2 = "counter-clockwise" if contour1.clockwise: state1 = "clockwise" if contour2.clockwise: state2 = "clockwise" text = "{contour1Name} is {state1} | {contour2Name} is {state2}".format( contour1Name=self.contour1Name, state1=state1, contour2Name=self.contour2Name, state2=state2) report.append(self.formatFatalString(text)) report += self.reportSubObjects(self.segments, showOK=showOK, showWarnings=showWarnings) if report or showOK: report.insert(0, self.title) return "\n".join(report)
class LayerCompatibilityReporter(BaseCompatibilityReporter): objectName = "Layer" def __init__(self, layer1, layer2): super(LayerCompatibilityReporter, self).__init__(layer1, layer2) self.glyphCountDifference = False self.glyphsMissingFromLayer2 = [] self.glyphsMissingInLayer1 = [] self.glyphs = [] layer1 = dynamicProperty("object1") layer1Name = dynamicProperty("object1Name") layer2 = dynamicProperty("object2") layer2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): layer1 = self.layer1 layer2 = self.layer2 report = [] if self.glyphCountDifference: text = self.reportCountDifference(subObjectName="glyphs", object1Name=self.layer1Name, object1Count=len(layer1), object2Name=self.layer2Name, object2Count=len(layer2)) report.append(self.formatWarningString(text)) if len(self.glyphsMissingFromLayer2) != 0: for name in self.glyphsMissingFromLayer2: text = self.reportDifferences( object1Name=self.layer1Name, subObjectName="glyph", subObjectID=name, object2Name=self.layer2Name, ) report.append(self.formatWarningString(text)) if len(self.glyphsMissingInLayer1) != 0: for name in self.glyphsMissingInLayer1: text = self.reportDifferences( object1Name=self.layer2Name, subObjectName="glyph", subObjectID=name, object2Name=self.layer1Name, ) report.append(self.formatWarningString(text)) report += self.reportSubObjects(self.glyphs, showOK=showOK, showWarnings=showWarnings) if report or showOK: report.insert(0, self.title) return "\n".join(report)
class BaseImage(BaseObject, TransformationMixin, PointPositionMixin, SelectionMixin): copyAttributes = ( "transformation", "color", "data" ) def _reprContents(self): contents = [ "offset='({x}, {y})'".format(x=self.offset[0], y=self.offset[1]), ] if self.color: contents.append("color=%r" % str(self.color)) if self.glyph is not None: contents.append("in glyph") contents += self.glyph._reprContents() return contents def __bool__(self): if len(self.data) == 0 or self.data is None: return False else: return True __nonzero__ = __bool__ # ------- # Parents # ------- def getParent(self): """ This is a backwards compatibility method. """ return self.glyph # Glyph _glyph = None glyph = dynamicProperty("glyph", "The image's parent :class:`BaseGlyph`.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): assert self._glyph is None if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Layer layer = dynamicProperty("layer", "The image's parent :class:`BaseLayer`.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # Font font = dynamicProperty("font", "The image's parent :class:`BaseFont`.") def _get_font(self): if self._glyph is None: return None return self.glyph.font # ---------- # Attributes # ---------- # Transformation transformation = dynamicProperty( "base_transformation", """ The image's :ref:`type-transformation`. This defines the image's position, scale, and rotation. :: >>> image.transformation (1, 0, 0, 1, 0, 0) >>> image.transformation = (2, 0, 0, 2, 100, -50) """ ) def _get_base_transformation(self): value = self._get_transformation() value = normalizers.normalizeTransformationMatrix(value) return value def _set_base_transformation(self, value): value = normalizers.normalizeTransformationMatrix(value) self._set_transformation(value) def _get_transformation(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def _set_transformation(self, value): """ Subclasses must override this method. """ self.raiseNotImplementedError() offset = dynamicProperty( "base_offset", """ The image's offset. This is a shortcut to the offset values in :attr:`transformation`. This must be an iterable containing two :ref:`type-int-float` values defining the x and y values to offset the image by. :: >>> image.offset (0, 0) >>> image.offset = (100, -50) """ ) def _get_base_offset(self): value = self._get_offset() value = normalizers.normalizeTransformationOffset(value) return value def _set_base_offset(self, value): value = normalizers.normalizeTransformationOffset(value) self._set_offset(value) def _get_offset(self): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation return (ox, oy) def _set_offset(self, value): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation ox, oy = value self.transformation = (sx, sxy, syx, sy, ox, oy) scale = dynamicProperty( "base_scale", """ The image's scale. This is a shortcut to the scale values in :attr:`transformation`. This must be an iterable containing two :ref:`type-int-float` values defining the x and y values to scale the image by. :: >>> image.scale (1, 1) >>> image.scale = (2, 2) """ ) def _get_base_scale(self): value = self._get_scale() value = normalizers.normalizeTransformationScale(value) return value def _set_base_scale(self, value): value = normalizers.normalizeTransformationScale(value) self._set_scale(value) def _get_scale(self): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation return (sx, sy) def _set_scale(self, value): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation sx, sy = value self.transformation = (sx, sxy, syx, sy, ox, oy) # Color color = dynamicProperty( "base_color", """ The image's color. This will be a :ref:`type-color` or ``None``. :: >>> image.color None >>> image.color = (1, 0, 0, 0.5) """ ) def _get_base_color(self): value = self._get_color() if value is not None: value = normalizers.normalizeColor(value) value = Color(value) return value def _set_base_color(self, value): if value is not None: value = normalizers.normalizeColor(value) self._set_color(value) def _get_color(self): """ Return the color value as a color tuple or None. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_color(self, value): """ value will be a color tuple or None. Subclasses must override this method. """ self.raiseNotImplementedError() # Data data = dynamicProperty( "data", """ The image's raw byte data. The possible formats are defined by each environment. """ ) def _get_base_data(self): return self._get_data() def _set_base_data(self, value): self._set_data(value) def _get_data(self): """ This must return raw byte data. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_data(self, value): """ value will be raw byte data. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ Subclasses may override this method. """ t = transform.Transform(*matrix) transformation = t.transform(self.transformation) self.transformation = tuple(transformation) # ------------- # Normalization # ------------- def round(self): """ Round offset coordinates. """ self._round() def _round(self): """ Subclasses may override this method. """ x, y = self.offset x = normalizers.normalizeRounding(x) y = normalizers.normalizeRounding(y) self.offset = (x, y)
class BasePoint(BaseObject, TransformationMixin, PointPositionMixin, SelectionMixin, IdentifierMixin, DeprecatedPoint, RemovedPoint): """ A point object. This object is almost always created with :meth:`BaseContour.appendPoint`, the pen returned by :meth:`BaseGlyph.getPen` or the point pen returned by :meth:`BaseGLyph.getPointPen`. An orphan point can be created like this:: >>> point = RPoint() """ copyAttributes = ("type", "smooth", "x", "y", "name") def _reprContents(self): contents = [ "%s" % self.type, ("({x}, {y})".format(x=self.x, y=self.y)), ] if self.name is not None: contents.append("name='%s'" % self.name) if self.smooth: contents.append("smooth=%r" % self.smooth) return contents # ------- # Parents # ------- # Contour _contour = None contour = dynamicProperty("contour", "The point's parent :class:`BaseContour`.") def _get_contour(self): if self._contour is None: return None return self._contour() def _set_contour(self, contour): assert self._contour is None if contour is not None: contour = reference(contour) self._contour = contour # Glyph glyph = dynamicProperty("glyph", "The point's parent :class:`BaseGlyph`.") def _get_glyph(self): if self._contour is None: return None return self.contour.glyph # Layer layer = dynamicProperty("layer", "The point's parent :class:`BaseLayer`.") def _get_layer(self): if self._contour is None: return None return self.glyph.layer # Font font = dynamicProperty("font", "The point's parent :class:`BaseFont`.") def _get_font(self): if self._contour is None: return None return self.glyph.font # ---------- # Attributes # ---------- # type type = dynamicProperty( "base_type", """ The point type defined with a :ref:`type-string`. The possible types are: +----------+---------------------------------+ | move | An on-curve move to. | +----------+---------------------------------+ | line | An on-curve line to. | +----------+---------------------------------+ | curve | An on-curve cubic curve to. | +----------+---------------------------------+ | qcurve | An on-curve quadratic curve to. | +----------+---------------------------------+ | offcurve | An off-curve. | +----------+---------------------------------+ """) def _get_base_type(self): value = self._get_type() value = normalizers.normalizePointType(value) return value def _set_base_type(self, value): value = normalizers.normalizePointType(value) self._set_type(value) def _get_type(self): """ This is the environment implementation of :attr:`BasePoint.type`. This must return a :ref:`type-string` defining the point type. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_type(self, value): """ This is the environment implementation of :attr:`BasePoint.type`. **value** will be a :ref:`type-string` defining the point type. It will have been normalized with :func:`normalizers.normalizePointType`. Subclasses must override this method. """ self.raiseNotImplementedError() # smooth smooth = dynamicProperty( "base_smooth", """ A ``bool`` indicating if the point is smooth or not. :: >>> point.smooth False >>> point.smooth = True """) def _get_base_smooth(self): value = self._get_smooth() value = normalizers.normalizeBoolean(value) return value def _set_base_smooth(self, value): value = normalizers.normalizeBoolean(value) self._set_smooth(value) def _get_smooth(self): """ This is the environment implementation of :attr:`BasePoint.smooth`. This must return a ``bool`` indicating the smooth state. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_smooth(self, value): """ This is the environment implementation of :attr:`BasePoint.smooth`. **value** will be a ``bool`` indicating the smooth state. It will have been normalized with :func:`normalizers.normalizeBoolean`. Subclasses must override this method. """ self.raiseNotImplementedError() # x x = dynamicProperty( "base_x", """ The x coordinate of the point. It must be an :ref:`type-int-float`. :: >>> point.x 100 >>> point.x = 101 """) def _get_base_x(self): value = self._get_x() value = normalizers.normalizeX(value) return value def _set_base_x(self, value): value = normalizers.normalizeX(value) self._set_x(value) def _get_x(self): """ This is the environment implementation of :attr:`BasePoint.x`. This must return an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_x(self, value): """ This is the environment implementation of :attr:`BasePoint.x`. **value** will be an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() # y y = dynamicProperty( "base_y", """ The y coordinate of the point. It must be an :ref:`type-int-float`. :: >>> point.y 100 >>> point.y = 101 """) def _get_base_y(self): value = self._get_y() value = normalizers.normalizeY(value) return value def _set_base_y(self, value): value = normalizers.normalizeY(value) self._set_y(value) def _get_y(self): """ This is the environment implementation of :attr:`BasePoint.y`. This must return an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_y(self, value): """ This is the environment implementation of :attr:`BasePoint.y`. **value** will be an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Identification # -------------- # index index = dynamicProperty( "base_index", """ The index of the point within the ordered list of the parent glyph's point. This attribute is read only. :: >>> point.index 0 """) def _get_base_index(self): value = self._get_index() value = normalizers.normalizeIndex(value) return value def _get_index(self): """ Get the point's index. This must return an ``int``. Subclasses may override this method. """ contour = self.contour if contour is None: return None return contour.points.index(self) # name name = dynamicProperty( "base_name", """ The name of the point. This will be a :ref:`type-string` or ``None``. >>> point.name 'my point' >>> point.name = None """) def _get_base_name(self): value = self._get_name() if value is not None: value = normalizers.normalizePointName(value) return value def _set_base_name(self, value): if value is not None: value = normalizers.normalizePointName(value) self._set_name(value) def _get_name(self): """ This is the environment implementation of :attr:`BasePoint.name`. This must return a :ref:`type-string` or ``None``. The returned value will be normalized with :func:`normalizers.normalizePointName`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_name(self, value): """ This is the environment implementation of :attr:`BasePoint.name`. **value** will be a :ref:`type-string` or ``None``. It will have been normalized with :func:`normalizers.normalizePointName`. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ This is the environment implementation of :meth:`BasePoint.transformBy`. **matrix** will be a :ref:`type-transformation`. that has been normalized with :func:`normalizers.normalizeTransformationMatrix`. Subclasses may override this method. """ t = transform.Transform(*matrix) x, y = t.transformPoint((self.x, self.y)) self.x = x self.y = y # ------------- # Normalization # ------------- def round(self): """ Round the point's coordinate. >>> point.round() This applies to the following: * x * y """ self._round() def _round(self, **kwargs): """ This is the environment implementation of :meth:`BasePoint.round`. Subclasses may override this method. """ self.x = normalizers.normalizeRounding(self.x) self.y = normalizers.normalizeRounding(self.y)
class BaseInfo(BaseObject, DeprecatedInfo, RemovedInfo): from 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): assert self._font is None or self._font() == font if font is not None: font = reference(font) self._font = font # ---------- # Validation # ---------- def _validateFontInfoAttributeValue(self, attr, value): from 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 ufoLib import fontInfoAttributesVersion3 if attr in fontInfoAttributesVersion3: return True return super(BaseInfo, self).__hasattr__(attr) # get def __getattribute__(self, attr): from 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) 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 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) 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 # ------------- 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() def _round(self, **kwargs): """ Subclasses may override this method. """ mathInfo = self._toMathInfo(guidelines=False) mathInfo = mathInfo.round() self._fromMathInfo(mathInfo, guidelines=False) # ------------- # Interpolation # ------------- 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 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) def _interpolate(self, factor, minInfo, maxInfo, round=True, suppressError=True): """ Subclasses may override this method. """ minInfo = minInfo._toMathInfo() maxInfo = maxInfo._toMathInfo() result = interpolate(minInfo, maxInfo, factor) if round: result = result.round() self._fromMathInfo(result)
class BaseCompatibilityReporter(object): objectName = "Base" def __init__(self, obj1, obj2): self._object1 = obj1 self._object2 = obj2 # status fatal = False warning = False def _get_title(self): title = "{object1Name} + {object2Name}".format( object1Name=self.object1Name, object2Name=self.object2Name) if self.fatal: return self.formatFatalString(title) elif self.warning: return self.formatWarningString(title) else: return self.formatOKString(title) title = dynamicProperty("title") # objects object1 = dynamicProperty("object1") object1Name = dynamicProperty("object1Name") def _get_object1(self): return self._object1 def _get_object1Name(self): return self._getObjectName(self._object1) object2 = dynamicProperty("object2") object2Name = dynamicProperty("object2Name") def _get_object2(self): return self._object2 def _get_object2Name(self): return self._getObjectName(self._object2) @staticmethod def _getObjectName(obj): if hasattr(obj, "name") and obj.name is not None: return "\"%s\"" % obj.name elif hasattr(obj, "identifier") and obj.identifier is not None: return "\"%s\"" % obj.identifier elif hasattr(obj, "index"): return "[%s]" % obj.index else: return "<%s>" % id(obj) # Report def __repr__(self): return self.report() def report(self, showOK=False, showWarnings=False): raise NotImplementedError def formatFatalString(self, text): return "[Fatal] {objectName}: ".format( objectName=self.objectName) + text def formatWarningString(self, text): return "[Warning] {objectName}: ".format( objectName=self.objectName) + text def formatOKString(self, text): return "[OK] {objectName}: ".format(objectName=self.objectName) + text @staticmethod def reportSubObjects(reporters, showOK=True, showWarnings=True): report = [] for reporter in reporters: if showOK or reporter.fatal or (showWarnings and reporter.warning): report.append(repr(reporter)) return report @staticmethod def reportCountDifference(subObjectName, object1Name, object1Count, object2Name, object2Count): text = ( "{object1Name} contains {object1Count} {subObjectName} | " "{object2Name} contains {object2Count} {subObjectName}").format( subObjectName=subObjectName, object1Name=object1Name, object1Count=object1Count, object2Name=object2Name, object2Count=object2Count) return text @staticmethod def reportOrderDifference(subObjectName, object1Name, object1Order, object2Name, object2Order): text = ("{object1Name} has {subObjectName} ordered {object1Order} | " "{object2Name} has {object2Order}").format( subObjectName=subObjectName, object1Name=object1Name, object1Order=object1Order, object2Name=object2Name, object2Order=object2Order) return text @staticmethod def reportDifferences(object1Name, subObjectName, subObjectID, object2Name): text = ("{object1Name} contains {subObjectName} {subObjectID} " "not in {object2Name}").format( object1Name=object1Name, subObjectName=subObjectName, subObjectID=subObjectID, object2Name=object2Name, ) return text
class BaseLib(BaseDict, DeprecatedLib, RemovedLib): """ A Lib object. This object normally created as part of a :class:`BaseFont`. An orphan Lib object can be created like this:: >>> lib = RLib() This object behaves like a Python dictionary. Most of the dictionary functionality comes from :class:`BaseDict`, look at that object for the required environment implementation details. Lib uses :func:`normalizers.normalizeLibKey` to normalize the key of the ``dict``, and :func:`normalizers.normalizeLibValue` to normalize the value of the ``dict``. """ keyNormalizer = normalizers.normalizeLibKey valueNormalizer = normalizers.normalizeLibValue def _reprContents(self): contents = [] if self.glyph is not None: contents.append("in glyph") contents += self.glyph._reprContents() if self.font: contents.append("in font") contents += self.font._reprContents() return contents # ------- # Parents # ------- # Glyph _glyph = None glyph = dynamicProperty("glyph", "The lib's parent glyph.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): assert self._font is None assert self._glyph is None or self._glyph() == glyph if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Font _font = None font = dynamicProperty("font", "The lib's parent font.") def _get_font(self): if self._font is not None: return self._font() elif self._glyph is not None: return self.glyph.font return None def _set_font(self, font): assert self._font is None or self._font() == font assert self._glyph is None if font is not None: font = reference(font) self._font = font # Layer layer = dynamicProperty("layer", "The lib's parent layer.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # --------------------- # RoboFab Compatibility # --------------------- def remove(self, key): """ Removes a key from the Lib. **key** will be a :ref:`type-string` that is the key to be removed. This is a backwards compatibility method. """ del self[key] def asDict(self): """ Return the Lib as a ``dict``. This is a backwards compatibility method. """ d = {} for k, v in self.items(): d[k] = v return d # ------------------- # Inherited Functions # ------------------- def __contains__(self, key): """ Tests to see if a lib name is in the Lib. **key** will be a :ref:`type-string`. This returns a ``bool`` indicating if the **key** is in the Lib. :: >>> "public.glyphOrder" in font.lib True """ return super(BaseLib, self).__contains__(key) def __delitem__(self, key): """ Removes **key** from the Lib. **key** is a :ref:`type-string`.:: >>> del font.lib["public.glyphOrder"] """ super(BaseLib, self).__delitem__(key) def __getitem__(self, key): """ Returns the contents of the named lib. **key** is a :ref:`type-string`. The returned value will be a ``list`` of the lib contents.:: >>> font.lib["public.glyphOrder"] ["A", "B", "C"] It is important to understand that any changes to the returned lib contents will not be reflected in the Lib object. If one wants to make a change to the lib contents, one should do the following:: >>> lib = font.lib["public.glyphOrder"] >>> lib.remove("A") >>> font.lib["public.glyphOrder"] = lib """ return super(BaseLib, self).__getitem__(key) def __iter__(self): """ Iterates through the Lib, giving the key for each iteration. The order that the Lib will iterate though is not fixed nor is it ordered.:: >>> for key in font.lib: >>> print key "public.glyphOrder" "org.robofab.scripts.SomeData" "public.postscriptNames" """ return super(BaseLib, self).__iter__() def __len__(self): """ Returns the number of keys in Lib as an ``int``.:: >>> len(font.lib) 5 """ return super(BaseLib, self).__len__() def __setitem__(self, key, items): """ Sets the **key** to the list of **items**. **key** is the lib name as a :ref:`type-string` and **items** is a ``list`` of items as :ref:`type-string`. >>> font.lib["public.glyphOrder"] = ["A", "B", "C"] """ super(BaseLib, self).__setitem__(key, items) def clear(self): """ Removes all keys from Lib, resetting the Lib to an empty dictionary. :: >>> font.lib.clear() """ super(BaseLib, self).clear() def get(self, key, default=None): """ Returns the contents of the named key. **key** is a :ref:`type-string`, and the returned values will either be ``list`` of key contents or ``None`` if no key was found. :: >>> font.lib["public.glyphOrder"] ["A", "B", "C"] It is important to understand that any changes to the returned key contents will not be reflected in the Lib object. If one wants to make a change to the key contents, one should do the following:: >>> lib = font.lib["public.glyphOrder"] >>> lib.remove("A") >>> font.lib["public.glyphOrder"] = lib """ return super(BaseLib, self).get(key, default) def items(self): """ Returns a list of ``tuple`` of each key name and key items. Keys are :ref:`type-string` and key members are a ``list`` of :ref:`type-string`. The initial list will be unordered. >>> font.lib.items() [("public.glyphOrder", ["A", "B", "C"]), ("public.postscriptNames", {'be': 'uni0431', 'ze': 'uni0437'})] """ return super(BaseLib, self).items() def keys(self): """ Returns a ``list`` of all the key names in Lib. This list will be unordered.:: >>> font.lib.keys() ["public.glyphOrder", "org.robofab.scripts.SomeData", "public.postscriptNames"] """ return super(BaseLib, self).keys() def pop(self, key, default=None): """ Removes the **key** from the Lib and returns the ``list`` of key members. If no key is found, **default** is returned. **key** is a :ref:`type-string`. This must return either **default** or a ``list`` of items as :ref:`type-string`. >>> font.lib.pop("public.glyphOrder") ["A", "B", "C"] """ return super(BaseLib, self).pop(key, default) def update(self, otherLib): """ Updates the Lib based on **otherLib**. *otherLib** is a ``dict`` of keys. If a key from **otherLib** is in Lib the key members will be replaced by the key members from **otherLib**. If a key from **otherLib** is not in the Lib, it is added to the Lib. If Lib contain a key name that is not in *otherLib**, it is not changed. >>> font.lib.update(newLib) """ super(BaseLib, self).update(otherLib) def values(self): """ Returns a ``list`` of each named key's members. This will be a list of lists, the key members will be a ``list`` of :ref:`type-string`. The initial list will be unordered. >>> font.lib.items() [["A", "B", "C"], {'be': 'uni0431', 'ze': 'uni0437'}] """ return super(BaseLib, self).values()
class BaseBPoint(BaseObject, TransformationMixin, SelectionMixin, DeprecatedBPoint, IdentifierMixin, RemovedBPoint): def _reprContents(self): contents = [ "%s" % self.type, "anchor='({x}, {y})'".format(x=self.anchor[0], y=self.anchor[1]), ] return contents def _setPoint(self, point): if hasattr(self, "_point"): raise AssertionError("point for bPoint already set") self._point = point def __eq__(self, other): if hasattr(other, "_point"): return self._point == other._point return NotImplemented # this class should not be used in hashable # collections since it is dynamically generated. __hash__ = None # ------- # Parents # ------- # identifier def _get_identifier(self): """ Subclasses may override this method. """ return self._point.identifier def _getIdentifier(self): """ Subclasses may override this method. """ return self._point.getIdentifier() # Segment _segment = dynamicProperty("base_segment") def _get_base_segment(self): point = self._point for segment in self.contour.segments: if segment.onCurve == point: return segment _nextSegment = dynamicProperty("base_nextSegment") def _get_base_nextSegment(self): contour = self.contour if contour is None: return None segments = contour.segments segment = self._segment i = segments.index(segment) + 1 if i >= len(segments): i = i % len(segments) nextSegment = segments[i] return nextSegment # Contour _contour = None contour = dynamicProperty("contour", "The bPoint's parent contour.") def _get_contour(self): if self._contour is None: return None return self._contour() def _set_contour(self, contour): if self._contour is not None: raise AssertionError("contour for bPoint already set") if contour is not None: contour = reference(contour) self._contour = contour # Glyph glyph = dynamicProperty("glyph", "The bPoint's parent glyph.") def _get_glyph(self): if self._contour is None: return None return self.contour.glyph # Layer layer = dynamicProperty("layer", "The bPoint's parent layer.") def _get_layer(self): if self._contour is None: return None return self.glyph.layer # Font font = dynamicProperty("font", "The bPoint's parent font.") def _get_font(self): if self._contour is None: return None return self.glyph.font # ---------- # Attributes # ---------- # anchor anchor = dynamicProperty("base_anchor", "The anchor point.") def _get_base_anchor(self): value = self._get_anchor() value = normalizers.normalizeCoordinateTuple(value) return value def _set_base_anchor(self, value): value = normalizers.normalizeCoordinateTuple(value) self._set_anchor(value) def _get_anchor(self): """ Subclasses may override this method. """ point = self._point return (point.x, point.y) def _set_anchor(self, value): """ Subclasses may override this method. """ pX, pY = self.anchor x, y = value dX = x - pX dY = y - pY self.moveBy((dX, dY)) # bcp in bcpIn = dynamicProperty("base_bcpIn", "The incoming off curve.") def _get_base_bcpIn(self): value = self._get_bcpIn() value = normalizers.normalizeCoordinateTuple(value) return value def _set_base_bcpIn(self, value): value = normalizers.normalizeCoordinateTuple(value) self._set_bcpIn(value) def _get_bcpIn(self): """ Subclasses may override this method. """ segment = self._segment offCurves = segment.offCurve if offCurves: bcp = offCurves[-1] x, y = relativeBCPIn(self.anchor, (bcp.x, bcp.y)) else: x = y = 0 return (x, y) def _set_bcpIn(self, value): """ Subclasses may override this method. """ x, y = absoluteBCPIn(self.anchor, value) segment = self._segment if segment.type == "move" and value != (0, 0): raise FontPartsError(("Cannot set the bcpIn for the first " "point in an open contour.")) else: offCurves = segment.offCurve if offCurves: # if the two off curves are located at the anchor # coordinates we can switch to a line segment type. if value == (0, 0) and self.bcpOut == (0, 0): segment.type = "line" segment.smooth = False else: offCurves[-1].x = x offCurves[-1].y = y elif value != (0, 0): segment.type = "curve" offCurves = segment.offCurve offCurves[-1].x = x offCurves[-1].y = y # bcp out bcpOut = dynamicProperty("base_bcpOut", "The outgoing off curve.") def _get_base_bcpOut(self): value = self._get_bcpOut() value = normalizers.normalizeCoordinateTuple(value) return value def _set_base_bcpOut(self, value): value = normalizers.normalizeCoordinateTuple(value) self._set_bcpOut(value) def _get_bcpOut(self): """ Subclasses may override this method. """ nextSegment = self._nextSegment offCurves = nextSegment.offCurve if offCurves: bcp = offCurves[0] x, y = relativeBCPOut(self.anchor, (bcp.x, bcp.y)) else: x = y = 0 return (x, y) def _set_bcpOut(self, value): """ Subclasses may override this method. """ x, y = absoluteBCPOut(self.anchor, value) segment = self._segment nextSegment = self._nextSegment if nextSegment.type == "move" and value != (0, 0): raise FontPartsError(("Cannot set the bcpOut for the last " "point in an open contour.")) else: offCurves = nextSegment.offCurve if offCurves: # if the off curves are located at the anchor coordinates # we can switch to a "line" segment type if value == (0, 0) and self.bcpIn == (0, 0): segment.type = "line" segment.smooth = False else: offCurves[0].x = x offCurves[0].y = y elif value != (0, 0): nextSegment.type = "curve" offCurves = nextSegment.offCurve offCurves[0].x = x offCurves[0].y = y # type type = dynamicProperty("base_type", "The bPoint type.") def _get_base_type(self): value = self._get_type() value = normalizers.normalizeBPointType(value) return value def _set_base_type(self, value): value = normalizers.normalizeBPointType(value) self._set_type(value) def _get_type(self): """ Subclasses may override this method. """ point = self._point typ = point.type if typ == "curve" and point.smooth: bType = "curve" elif typ in ("move", "line", "curve"): bType = "corner" else: raise FontPartsError( "A %s point can not be converted to a bPoint." % typ) return bType def _set_type(self, value): """ Subclasses may override this method. """ point = self._point # convert corner to curve if value == "curve" and point.type == "line": # This needs to insert off curves without # generating unnecessary points in the # following segment. The segment object # implements this logic, so delegate the # change to the corresponding segment. segment = self._segment segment.type = "curve" segment.smooth = True # convert curve to corner elif value == "corner" and point.type == "curve": point.smooth = False # -------------- # Identification # -------------- index = dynamicProperty("index", ("The index of the bPoint within the ordered " "list of the parent contour's bPoints. None " "if the bPoint does not belong to a contour.")) def _get_base_index(self): if self.contour is None: return None value = self._get_index() value = normalizers.normalizeIndex(value) return value def _get_index(self): """ Subclasses may override this method. """ contour = self.contour value = contour.bPoints.index(self) return value # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ Subclasses may override this method. """ anchor = self.anchor bcpIn = absoluteBCPIn(anchor, self.bcpIn) bcpOut = absoluteBCPOut(anchor, self.bcpOut) points = [bcpIn, anchor, bcpOut] t = transform.Transform(*matrix) bcpIn, anchor, bcpOut = t.transformPoints(points) x, y = anchor self._point.x = x self._point.y = y self.bcpIn = relativeBCPIn(anchor, bcpIn) self.bcpOut = relativeBCPOut(anchor, bcpOut) # ---- # Misc # ---- def round(self): """ Round coordinates. """ x, y = self.anchor self.anchor = (normalizers.normalizeRounding(x), normalizers.normalizeRounding(y)) x, y = self.bcpIn self.bcpIn = (normalizers.normalizeRounding(x), normalizers.normalizeRounding(y)) x, y = self.bcpOut self.bcpOut = (normalizers.normalizeRounding(x), normalizers.normalizeRounding(y))
class _BaseGlyphVendor(BaseObject, SelectionMixin, DeprecatedLayer, RemovedLayer): """ This class exists to provide common glyph interaction code to BaseFont and BaseLayer. It should not be directly subclassed. """ # ----------------- # Glyph Interaction # ----------------- def _setLayerInGlyph(self, glyph): if glyph.layer is None: if isinstance(self, BaseLayer): layer = self else: layer = self.defaultLayer glyph.layer = layer def __len__(self): """ An ``int`` representing number of glyphs in the layer. :: >>> len(layer) 256 """ return self._len() def _len(self, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.__len__` and :meth:`BaseFont.__len__` This must return an ``int`` indicating the number of glyphs in the layer. Subclasses may override this method. """ return len(self.keys()) def __iter__(self): """ Iterate through the :class:`BaseGlyph` objects in the layer. :: >>> for glyph in layer: ... glyph.name "A" "B" "C" """ return self._iter() def _iter(self, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.__iter__` and :meth:`BaseFont.__iter__` This must return an iterator that returns instances of a :class:`BaseGlyph` subclass. Subclasses may override this method. """ for name in self.keys(): yield self[name] def __getitem__(self, name): """ Get the :class:`BaseGlyph` with name from the layer. :: >>> glyph = layer["A"] """ name = normalizers.normalizeGlyphName(name) if name not in self: raise ValueError("No glyph named '%s'." % name) glyph = self._getItem(name) self._setLayerInGlyph(glyph) return glyph def _getItem(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.__getitem__` and :meth:`BaseFont.__getitem__` This must return an instance of a :class:`BaseGlyph` subclass. **name** will be a :ref:`type-string` representing a name of a glyph that is in the layer. It will have been normalized with :func:`normalizers.normalizeGlyphName`. Subclasses must override this method. """ self.raiseNotImplementedError() def __setitem__(self, name, glyph): """ Set the :class:`BaseGlyph` with name into the layer. :: >>> layer["A"] = glyph """ self.insertGlyph(glyph, name) def keys(self): """ Get a list of all glyphs in the layer. :: >>> layer.keys() ["B", "C", "A"] The order of the glyphs is undefined. """ return self._keys() def _keys(self, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.keys` and :meth:`BaseFont.keys` This must return an :ref:`type-immutable-list` of the names representing all glyphs in the layer. The order is not defined. Subclasses must override this method. """ self.raiseNotImplementedError() def __contains__(self, name): """ Test if the layer contains a glyph with **name**. :: >>> "A" in layer True """ name = normalizers.normalizeGlyphName(name) return self._contains(name) def _contains(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.__contains__` and :meth:`BaseFont.__contains__` This must return ``bool`` indicating if the layer has a glyph with the defined name. **name** will be a :ref-type-string` representing a glyph name. It will have been normalized with :func:`normalizers.normalizeGlyphName`. Subclasses may override this method. """ return name in self.keys() def newGlyph(self, name, clear=True): """ Make a new glyph with **name** in the layer. :: >>> glyph = layer.newGlyph("A") The newly created :class:`BaseGlyph` will be returned. If the glyph exists in the layer and clear is set to ``False``, the existing glyph will be returned, otherwise the default behavior is to clear the exisiting glyph. """ name = normalizers.normalizeGlyphName(name) if name not in self: glyph = self._newGlyph(name) elif clear: self.removeGlyph(name) glyph = self._newGlyph(name) else: glyph = self._getItem(name) self._setLayerInGlyph(glyph) return glyph def _newGlyph(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.newGlyph` and :meth:`BaseFont.newGlyph` This must return an instance of a :class:`BaseGlyph` subclass. **name** will be a :ref:`type-string` representing a glyph name. It will have been normalized with :func:`normalizers.normalizeGlyphName`. The name will have been tested to make sure that no glyph with the same name exists in the layer. Subclasses must override this method. """ self.raiseNotImplementedError() def removeGlyph(self, name): """ Remove the glyph with name from the layer. :: >>> layer.removeGlyph("A") """ name = normalizers.normalizeGlyphName(name) if name not in self: raise ValueError("No glyph with the name '%s' exists." % name) self._removeGlyph(name) def _removeGlyph(self, name, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.removeGlyph` and :meth:`BaseFont.removeGlyph`. **name** will be a :ref:`type-string` representing a glyph name of a glyph that is in the layer. It will have been normalized with :func:`normalizers.normalizeGlyphName`. The newly created :class:`BaseGlyph` must be returned. Subclasses must override this method. """ self.raiseNotImplementedError() def insertGlyph(self, glyph, name=None): """ Insert **glyph** into the layer. :: >>> glyph = layer.insertGlyph(otherGlyph, name="A") This does not necessarily insert the glyph directly. In many cases, the environment will create a new glyph and copy the data from **glyph** to the new glyph. **name** indicates the name that should be assigned to the glyph after insertion. If **name** is not given, the glyph's original name must be used. If the glyph does not have a name, an error must be raised. The data that will be inserted from **glyph** is the same data as documented in :meth:`BaseGlyph.copy`. """ if name is None: name = glyph.name name = normalizers.normalizeGlyphName(name) if name in self: self.removeGlyph(name) return self._insertGlyph(glyph, name=name) def _insertGlyph(self, glyph, name, **kwargs): """ This is the environment implementation of :meth:`BaseLayer.insertGlyph` and :meth:`BaseFont.insertGlyph`. This must return an instance of a :class:`BaseGlyph` subclass. **glyph** will be a glyph object with the attributes necessary for copying as defined in :meth:`BaseGlyph.copy` An environment may choose to not insert **glyph** directly, opting to copy the data from **glyph** into a new glyph instead. **name** will be a :ref:`type-string` representing a glyph name. It will have been normalized with :func:`normalizers.normalizeGlyphName`. **name** will have been tested to make sure that no glyph with the same name exists in the layer. Subclasses may override this method. """ dest = self.newGlyph(name) dest.copyData(glyph) return dest # --------- # Selection # --------- selectedGlyphs = dynamicProperty( "base_selectedGlyphs", """ A list of glyphs selected in the layer. Getting selected glyph objects: >>> for glyph in layer.selectedGlyphs: ... glyph.markColor = (1, 0, 0, 0.5) Setting selected glyph objects: >>> layer.selectedGlyphs = someGlyphs """) def _get_base_selectedGlyphs(self): selected = tuple([ normalizers.normalizeGlyph(glyph) for glyph in self._get_selectedGlyphs() ]) return selected def _get_selectedGlyphs(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self) def _set_base_selectedGlyphs(self, value): normalized = [normalizers.normalizeGlyph(glyph) for glyph in value] self._set_selectedGlyphs(normalized) def _set_selectedGlyphs(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self, value) selectedGlyphNames = dynamicProperty( "base_selectedGlyphNames", """ A list of names of glyphs selected in the layer. Getting selected glyph names: >>> for name in layer.selectedGlyphNames: ... print(name) Setting selected glyph names: >>> layer.selectedGlyphNames = ["A", "B", "C"] """) def _get_base_selectedGlyphNames(self): selected = tuple([ normalizers.normalizeGlyphName(name) for name in self._get_selectedGlyphNames() ]) return selected def _get_selectedGlyphNames(self): """ Subclasses may override this method. """ selected = [glyph.name for glyph in self.selectedGlyphs] return selected def _set_base_selectedGlyphNames(self, value): normalized = [normalizers.normalizeGlyphName(name) for name in value] self._set_selectedGlyphNames(normalized) def _set_selectedGlyphNames(self, value): """ Subclasses may override this method. """ select = [self[name] for name in value] self.selectedGlyphs = select # -------------------- # Legacy Compatibility # -------------------- has_key = __contains__
class GlyphCompatibilityReporter(BaseCompatibilityReporter): objectName = "Glyph" def __init__(self, glyph1, glyph2): super(GlyphCompatibilityReporter, self).__init__(glyph1, glyph2) self.contourCountDifference = False self.componentCountDifference = False self.guidelineCountDifference = False self.anchorDifferences = [] self.anchorCountDifference = False self.anchorOrderDifference = False self.anchorsMissingFromGlyph1 = [] self.anchorsMissingFromGlyph2 = [] self.componentDifferences = [] self.componentOrderDifference = False self.componentsMissingFromGlyph1 = [] self.componentsMissingFromGlyph2 = [] self.guidelinesMissingFromGlyph1 = [] self.guidelinesMissingFromGlyph2 = [] self.contours = [] glyph1 = dynamicProperty("object1") glyph1Name = dynamicProperty("object1Name") glyph2 = dynamicProperty("object2") glyph2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): glyph1 = self.glyph1 glyph2 = self.glyph2 report = [] # Contour test if self.contourCountDifference: text = self.reportCountDifference(subObjectName="contours", object1Name=self.glyph1Name, object1Count=len(glyph1), object2Name=self.glyph2Name, object2Count=len(glyph2)) report.append(self.formatFatalString(text)) report += self.reportSubObjects(self.contours, showOK=showOK, showWarnings=showWarnings) # Component test if self.componentCountDifference: text = self.reportCountDifference( subObjectName="components", object1Name=self.glyph1Name, object1Count=len(glyph1.components), object2Name=self.glyph2Name, object2Count=len(glyph2.components)) report.append(self.formatFatalString(text)) elif self.componentOrderDifference: text = self.reportOrderDifference( subObjectName="components", object1Name=self.glyph1Name, object1Order=[c.baseGlyph for c in glyph1.components], object2Name=self.glyph2Name, object2Order=[c.baseGlyph for c in glyph2.components]) report.append(self.formatWarningString(text)) for name in self.componentsMissingFromGlyph2: text = self.reportDifferences( object1Name=self.glyph1Name, subObjectName="component", subObjectID=name, object2Name=self.glyph2Name, ) report.append(self.formatWarningString(text)) for name in self.componentsMissingFromGlyph1: text = self.reportDifferences( object1Name=self.glyph2Name, subObjectName="component", subObjectID=name, object2Name=self.glyph1Name, ) report.append(self.formatWarningString(text)) # Anchor test if self.anchorCountDifference: text = self.reportCountDifference(subObjectName="anchors", object1Name=self.glyph1Name, object1Count=len(glyph1.anchors), object2Name=self.glyph2Name, object2Count=len(glyph2.anchors)) report.append(self.formatWarningString(text)) elif self.anchorOrderDifference: text = self.reportOrderDifference( subObjectName="anchors", object1Name=self.glyph1Name, object1Order=[a.name for a in glyph1.anchors], object2Name=self.glyph2Name, object2Order=[a.name for a in glyph2.anchors]) report.append(self.formatWarningString(text)) for name in self.anchorsMissingFromGlyph2: text = self.reportDifferences( object1Name=self.glyph1Name, subObjectName="anchor", subObjectID=name, object2Name=self.glyph2Name, ) report.append(self.formatWarningString(text)) for name in self.anchorsMissingFromGlyph1: text = self.reportDifferences( object1Name=self.glyph2Name, subObjectName="anchor", subObjectID=name, object2Name=self.glyph1Name, ) report.append(self.formatWarningString(text)) # Guideline test if self.guidelineCountDifference: text = self.reportCountDifference( subObjectName="guidelines", object1Name=self.glyph1Name, object1Count=len(glyph1.guidelines), object2Name=self.glyph2Name, object2Count=len(glyph2.guidelines)) report.append(self.formatWarningString(text)) for name in self.guidelinesMissingFromGlyph2: text = self.reportDifferences( object1Name=self.glyph1Name, subObjectName="guideline", subObjectID=name, object2Name=self.glyph2Name, ) report.append(self.formatWarningString(text)) for name in self.guidelinesMissingFromGlyph1: text = self.reportDifferences( object1Name=self.glyph2Name, subObjectName="guideline", subObjectID=name, object2Name=self.glyph1Name, ) report.append(self.formatWarningString(text)) if report or showOK: report.insert(0, self.title) return "\n".join(report)
class FontCompatibilityReporter(BaseCompatibilityReporter): objectName = "Font" def __init__(self, font1, font2): super(FontCompatibilityReporter, self).__init__(font1, font2) self.guidelineCountDifference = False self.layerCountDifference = False self.guidelinesMissingFromFont2 = [] self.guidelinesMissingInFont1 = [] self.layersMissingFromFont2 = [] self.layersMissingInFont1 = [] self.layers = [] font1 = dynamicProperty("object1") font1Name = dynamicProperty("object1Name") font2 = dynamicProperty("object2") font2Name = dynamicProperty("object2Name") def report(self, showOK=True, showWarnings=True): font1 = self.font1 font2 = self.font2 report = [] if self.guidelineCountDifference: text = self.reportCountDifference( subObjectName="guidelines", object1Name=self.font1Name, object1Count=len(font1.guidelines), object2Name=self.font2Name, object2Count=len(font2.guidelines)) report.append(self.formatWarningString(text)) for name in self.guidelinesMissingFromFont2: text = self.reportDifferences( object1Name=self.font1Name, subObjectName="guideline", subObjectID=name, object2Name=self.font2Name, ) report.append(self.formatWarningString(text)) for name in self.guidelinesMissingInFont1: text = self.reportDifferences( object1Name=self.font2Name, subObjectName="guideline", subObjectID=name, object2Name=self.font1Name, ) report.append(self.formatWarningString(text)) if self.layerCountDifference: text = self.reportCountDifference( subObjectName="layers", object1Name=self.font1Name, object1Count=len(font1.layerOrder), object2Name=self.font2Name, object2Count=len(font2.layerOrder)) report.append(self.formatWarningString(text)) for name in self.layersMissingFromFont2: text = self.reportDifferences( object1Name=self.font1Name, subObjectName="layer", subObjectID=name, object2Name=self.font2Name, ) report.append(self.formatWarningString(text)) for name in self.layersMissingInFont1: text = self.reportDifferences( object1Name=self.font2Name, subObjectName="layer", subObjectID=name, object2Name=self.font1Name, ) report.append(self.formatWarningString(text)) report += self.reportSubObjects(self.layers, showOK=showOK, showWarnings=showWarnings) if report or showOK: report.insert(0, self.title) return "\n".join(report)
class BaseFeatures(BaseObject, DeprecatedFeatures, RemovedFeatures): copyAttributes = ("text", ) 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 features' parent :class:`BaseFont`.") 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 features already set and is not same as font") if font is not None: font = reference(font) self._font = font # ---- # Text # ---- text = dynamicProperty( "base_text", """ The `.fea formated <http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html>`_ text representing the features. It must be a :ref:`type-string`. """) def _get_base_text(self): value = self._get_text() if value is not None: value = normalizers.normalizeFeatureText(value) return value def _set_base_text(self, value): if value is not None: value = normalizers.normalizeFeatureText(value) self._set_text(value) def _get_text(self): """ This is the environment implementation of :attr:`BaseFeatures.text`. This must return a :ref:`type-string`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_text(self, value): """ This is the environment implementation of :attr:`BaseFeatures.text`. **value** will be a :ref:`type-string`. Subclasses must override this method. """ self.raiseNotImplementedError()
class BaseGroups(BaseDict, DeprecatedGroups, RemovedGroups): """ A Groups object. This object normally created as part of a :class:`BaseFont`. An orphan Groups object can be created like this:: >>> groups = RGroups() This object behaves like a Python dictionary. Most of the dictionary functionality comes from :class:`BaseDict`, look at that object for the required environment implementation details. Groups uses :func:`normalizers.normalizeGroupKey` to normalize the key of the ``dict``, and :func:`normalizers.normalizeGroupValue` to normalize the value of the ``dict``. """ keyNormalizer = normalizers.normalizeGroupKey valueNormalizer = normalizers.normalizeGroupValue 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 Groups' parent :class:`BaseFont`.") def _get_font(self): if self._font is None: return None return self._font() def _set_font(self, font): assert self._font is None or self._font() == font if font is not None: font = reference(font) self._font = font # --------- # Searching # --------- def findGlyph(self, glyphName): """ Returns a ``list`` of the group or groups associated with **glyphName**. **glyphName** will be an :ref:`type-string`. If no group is found to contain **glyphName** an empty ``list`` will be returned. :: >>> font.groups.findGlyph("A") ["A_accented"] """ glyphName = normalizers.normalizeGlyphName(glyphName) groupNames = self._findGlyph(glyphName) groupNames = [ self.keyNormalizer.__func__(groupName) for groupName in groupNames ] return groupNames def _findGlyph(self, glyphName): """ This is the environment implementation of :meth:`BaseGroups.findGlyph`. **glyphName** will be an :ref:`type-string`. Subclasses may override this method. """ found = [] for key, groupList in self.items(): if glyphName in groupList: found.append(key) return found # -------------- # Kerning Groups # -------------- side1KerningGroups = dynamicProperty( "base_side1KerningGroups", """ All groups marked as potential side 1 kerning members. >>> side1Groups = groups.side1KerningGroups The value will be a :ref:`dict` with :ref:`string` keys representing group names and :ref:`tuple` contaning glyph names. """) def get_base_side1KerningGroups(self): kerningGroups = self._get_side1KerningGroups() normalized = {} for name, members in kerningGroups.items(): name = normalizers.normalizeGroupKey(name) members = normalizers.normalizeGroupValue(members) return normalized def _get_base_side1KerningGroups(self): """ Subclasses may override this method. """ found = {} for name, contents in self.items(): if name.startswith("public.kern1."): found[name] = contents return found side2KerningGroups = dynamicProperty( "base_side2KerningGroups", """ All groups marked as potential side 1 kerning members. >>> side2Groups = groups.side2KerningGroups The value will be a :ref:`dict` with :ref:`string` keys representing group names and :ref:`tuple` contaning glyph names. """) def _get_base_side2KerningGroups(self): kerningGroups = self._get_side2KerningGroups() normalized = {} for name, members in kerningGroups.items(): name = normalizers.normalizeGroupKey(name) members = normalizers.normalizeGroupValue(members) return normalized def get_base_side2KerningGroups(self): """ Subclasses may override this method. """ found = {} for name, contents in self.items(): if name.startswith("public.kern2."): found[name] = contents return found # --------------------- # RoboFab Compatibility # --------------------- def remove(self, groupName): """ Removes a group from the Groups. **groupName** will be a :ref:`type-string` that is the group name to be removed. This is a backwards compatibility method. """ del self[groupName] def asDict(self): """ Return the Groups as a ``dict``. This is a backwards compatibility method. """ d = {} for k, v in self.items(): d[k] = v return d # ------------------- # Inherited Functions # ------------------- def __contains__(self, groupName): """ Tests to see if a group name is in the Groups. **groupName** will be a :ref:`type-string`. This returns a ``bool`` indicating if the **groupName** is in the Groups. :: >>> "myGroup" in font.groups True """ return super(BaseGroups, self).__contains__(groupName) def __delitem__(self, groupName): """ Removes **groupName** from the Groups. **groupName** is a :ref:`type-string`.:: >>> del font.groups["myGroup"] """ super(BaseGroups, self).__delitem__(groupName) def __getitem__(self, groupName): """ Returns the contents of the named group. **groupName** is a :ref:`type-string`. The returned value will be a ``list`` of the group contents.:: >>> font.groups["myGroup"] ["A", "B", "C"] It is important to understand that any changes to the returned group contents will not be reflected in the Groups object. If one wants to make a change to the group contents, one should do the following:: >>> group = font.groups["myGroup"] >>> group.remove("A") >>> font.groups["myGroup"] = group """ return super(BaseGroups, self).__getitem__(groupName) def __iter__(self): """ Iterates through the Groups, giving the key for each iteration. The order that the Groups will iterate though is not fixed nor is it ordered.:: >>> for groupName in font.groups: >>> print groupName "myGroup" "myGroup3" "myGroup2" """ return super(BaseGroups, self).__iter__() def __len__(self): """ Returns the number of groups in Groups as an ``int``.:: >>> len(font.groups) 5 """ return super(BaseGroups, self).__len__() def __setitem__(self, groupName, glyphNames): """ Sets the **groupName** to the list of **glyphNames**. **groupName** is the group name as a :ref:`type-string` and **glyphNames** is a ``list`` of glyph names as :ref:`type-string`. >>> font.groups["myGroup"] = ["A", "B", "C"] """ super(BaseGroups, self).__setitem__(groupName, glyphNames) def clear(self): """ Removes all group information from Groups, resetting the Groups to an empty dictionary. :: >>> font.groups.clear() """ super(BaseGroups, self).clear() def get(self, groupName, default=None): """ Returns the contents of the named group. **groupName** is a :ref:`type-string`, and the returned values will either be ``list`` of group contents or ``None`` if no group was found. :: >>> font.groups["myGroup"] ["A", "B", "C"] It is important to understand that any changes to the returned group contents will not be reflected in the Groups object. If one wants to make a change to the group contents, one should do the following:: >>> group = font.groups["myGroup"] >>> group.remove("A") >>> font.groups["myGroup"] = group """ return super(BaseGroups, self).get(groupName, default) def items(self): """ Returns a list of ``tuple`` of each group name and group members. Group names are :ref:`type-string` and group members are a ``list`` of :ref:`type-string`. The initial list will be unordered. >>> font.groups.items() [("myGroup", ["A", "B", "C"]), ("myGroup2", ["D", "E", "F"])] """ return super(BaseGroups, self).items() def keys(self): """ Returns a ``list`` of all the group names in Groups. This list will be unordered.:: >>> font.groups.keys() ["myGroup4", "myGroup1", "myGroup5"] """ return super(BaseGroups, self).keys() def pop(self, groupName, default=None): """ Removes the **groupName** from the Groups and returns the ``list`` of group members. If no group is found, **default** is returned. **groupName** is a :ref:`type-string`. This must return either **default** or a ``list`` of glyph names as :ref:`type-string`. >>> font.groups.pop("myGroup") ["A", "B", "C"] """ return super(BaseGroups, self).pop(groupName, default) def update(self, otherGroups): """ Updates the Groups based on **otherGroups**. *otherGroups** is a ``dict`` of groups information. If a group from **otherGroups** is in Groups, the group members will be replaced by the group members from **otherGroups**. If a group from **otherGroups** is not in the Groups, it is added to the Groups. If Groups contain a group name that is not in *otherGroups**, it is not changed. >>> font.groups.update(newGroups) """ super(BaseGroups, self).update(otherGroups) def values(self): """ Returns a ``list`` of each named group's members. This will be a list of lists, the group members will be a ``list`` of :ref:`type-string`. The initial list will be unordered. >>> font.groups.items() [["A", "B", "C"], ["D", "E", "F"]] """ return super(BaseGroups, self).values()
class BaseComponent(BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, IdentifierMixin, DeprecatedComponent, RemovedComponent): copyAttributes = ("baseGlyph", "transformation") def _reprContents(self): contents = [ "baseGlyph='%s'" % self.baseGlyph, "offset='({x}, {y})'".format(x=self.offset[0], y=self.offset[1]), ] if self.glyph is not None: contents.append("in glyph") contents += self.glyph._reprContents() return contents # ------- # Parents # ------- # Glyph _glyph = None glyph = dynamicProperty("glyph", "The component's parent glyph.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): if self._glyph is not None: raise AssertionError("glyph for component already set") if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Layer layer = dynamicProperty("layer", "The component's parent layer.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # Font font = dynamicProperty("font", "The component's parent font.") def _get_font(self): if self._glyph is None: return None return self.glyph.font # ---------- # Attributes # ---------- # baseGlyph baseGlyph = dynamicProperty("base_baseGlyph", "The glyph the component references.") def _get_base_baseGlyph(self): value = self._get_baseGlyph() # if the component does not belong to a layer, # it is allowed to have None as its baseGlyph if value is None and self.layer is None: pass else: value = normalizers.normalizeGlyphName(value) return value def _set_base_baseGlyph(self, value): value = normalizers.normalizeGlyphName(value) self._set_baseGlyph(value) def _get_baseGlyph(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def _set_baseGlyph(self, value): """ Subclasses must override this method. """ self.raiseNotImplementedError() # transformation transformation = dynamicProperty("base_transformation", "The component's transformation matrix.") def _get_base_transformation(self): value = self._get_transformation() value = normalizers.normalizeTransformationMatrix(value) return value def _set_base_transformation(self, value): value = normalizers.normalizeTransformationMatrix(value) self._set_transformation(value) def _get_transformation(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def _set_transformation(self, value): """ Subclasses must override this method. """ self.raiseNotImplementedError() # offset offset = dynamicProperty("base_offset", "The component's offset.") def _get_base_offset(self): value = self._get_offset() value = normalizers.normalizeTransformationOffset(value) return value def _set_base_offset(self, value): value = normalizers.normalizeTransformationOffset(value) self._set_offset(value) def _get_offset(self): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation return ox, oy def _set_offset(self, value): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation ox, oy = value self.transformation = sx, sxy, syx, sy, ox, oy # scale scale = dynamicProperty("base_scale", "The component's scale.") def _get_base_scale(self): value = self._get_scale() value = normalizers.normalizeComponentScale(value) return value def _set_base_scale(self, value): value = normalizers.normalizeComponentScale(value) self._set_scale(value) def _get_scale(self): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation return sx, sy def _set_scale(self, value): """ Subclasses may override this method. """ sx, sxy, syx, sy, ox, oy = self.transformation sx, sy = value self.transformation = sx, sxy, syx, sy, ox, oy # -------------- # Identification # -------------- # index index = dynamicProperty("base_index", ("The index of the component within the " "ordered list of the parent glyph's components.")) def _get_base_index(self): glyph = self.glyph if glyph is None: return None value = self._get_index() value = normalizers.normalizeIndex(value) return value def _set_base_index(self, value): glyph = self.glyph if glyph is None: raise FontPartsError("The component does not belong to a glyph.") value = normalizers.normalizeIndex(value) componentCount = len(glyph.components) if value < 0: value = -(value % componentCount) if value >= componentCount: value = componentCount self._set_index(value) def _get_index(self): """ Subclasses may override this method. """ glyph = self.glyph return glyph.components.index(self) def _set_index(self, value): """ Subclasses must override this method. """ self.raiseNotImplementedError() # ---- # Pens # ---- def draw(self, pen): """ Draw the component with the given Pen. """ self._draw(pen) def _draw(self, pen, **kwargs): """ Subclasses may override this method. """ from fontTools.ufoLib.pointPen import PointToSegmentPen adapter = PointToSegmentPen(pen) self.drawPoints(adapter) def drawPoints(self, pen): """ Draw the contour with the given PointPen. """ self._drawPoints(pen) def _drawPoints(self, pen, **kwargs): """ Subclasses may override this method. """ # The try: ... except TypeError: ... # handles backwards compatibility with # point pens that have not been upgraded # to point pen protocol 2. try: pen.addComponent(self.baseGlyph, self.transformation, identifier=self.identifier, **kwargs) except TypeError: pen.addComponent(self.baseGlyph, self.transformation, **kwargs) # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ Subclasses may override this method. """ t = transform.Transform(*matrix) transformation = t.transform(self.transformation) self.transformation = tuple(transformation) # ------------- # Normalization # ------------- def round(self): """ Round offset coordinates. """ self._round() def _round(self): """ Subclasses may override this method. """ x, y = self.offset x = normalizers.normalizeVisualRounding(x) y = normalizers.normalizeVisualRounding(y) self.offset = (x, y) def decompose(self): """ Decompose the component. """ glyph = self.glyph if glyph is None: raise FontPartsError("The component does not belong to a glyph.") self._decompose() def _decompose(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() # ------------- # Interpolation # ------------- compatibilityReporterClass = ComponentCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. :: >>> compatible, report = self.isCompatible(otherComponent) >>> compatible True >>> compatible [Warning] Component: "A" + "B" [Warning] Component: "A" has name A | "B" has name B This will return a ``bool`` indicating if the component is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseComponent, self).isCompatible(other, BaseComponent) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseComponent.isCompatible`. Subclasses may override this method. """ component1 = self component2 = other # base glyphs if component1.baseName != component2.baseName: reporter.baseDifference = True reporter.warning = True # ------------ # Data Queries # ------------ def pointInside(self, point): """ Determine if point is in the black or white of the component. point must be an (x, y) tuple. """ point = normalizers.normalizeCoordinateTuple(point) return self._pointInside(point) def _pointInside(self, point): """ Subclasses may override this method. """ from fontTools.pens.pointInsidePen import PointInsidePen pen = PointInsidePen(glyphSet=self.layer, testPoint=point, evenOdd=False) self.draw(pen) return pen.getResult() bounds = dynamicProperty("base_bounds", ("The bounds of the component: " "(xMin, yMin, xMax, yMax) or None.")) def _get_base_bounds(self): value = self._get_bounds() if value is not None: value = normalizers.normalizeBoundingBox(value) return value 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
class BaseLayer(_BaseGlyphVendor, InterpolationMixin): def _reprContents(self): contents = [ "'%s'" % self.name, ] if self.color: contents.append("color=%r" % str(self.color)) return contents # ---- # Copy # ---- copyAttributes = ("name", "color", "lib") def copy(self): """ Copy the layer into a new layer that does not belong to a font. :: >>> copiedLayer = layer.copy() This will copy: * name * color * lib * glyphs """ return super(BaseLayer, self).copy() def copyData(self, source): """ Copy data from **source** into this layer. Refer to :meth:`BaseLayer.copy` for a list of values that will be copied. """ super(BaseLayer, self).copyData(source) for name in source.keys(): glyph = self.newGlyph(name) glyph.copyData(source[name]) # ------- # Parents # ------- # Font _font = None font = dynamicProperty( "font", """ The layer's parent :class:`BaseFont`. :: >>> font = layer.font """) def _get_font(self): if self._font is None: return None return self._font() def _set_font(self, font): assert self._font is None if font is not None: font = reference(font) self._font = font # -------------- # Identification # -------------- # name name = dynamicProperty( "base_name", """ The name of the layer. :: >>> layer.name "foreground" >>> layer.name = "top" """) def _get_base_name(self): value = self._get_name() if value is not None: value = normalizers.normalizeLayerName(value) return value def _set_base_name(self, value): if value == self.name: return value = normalizers.normalizeLayerName(value) existing = self.font.layerOrder if value in existing: raise ValueError("A layer with the name '%s' already exists." % value) self._set_name(value) def _get_name(self): """ This is the environment implementation of :attr:`BaseLayer.name`. This must return a :ref:`type-string` defining the name of the layer. If the layer is the default layer, the returned value must be ``None``. It will be normalized with :func:`normalizers.normalizeLayerName`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_name(self, value, **kwargs): """ This is the environment implementation of :attr:`BaseLayer.name`. **value** will be a :ref:`type-string` defining the name of the layer. It will have been normalized with :func:`normalizers.normalizeLayerName`. No layer with the same name will exist. Subclasses must override this method. """ self.raiseNotImplementedError() # color color = dynamicProperty( "base_color", """ The layer's color. :: >>> layer.color None >>> layer.color = (1, 0, 0, 0.5) """) def _get_base_color(self): value = self._get_color() if value is not None: value = normalizers.normalizeColor(value) value = Color(value) return value def _set_base_color(self, value): if value is not None: value = normalizers.normalizeColor(value) self._set_color(value) def _get_color(self): """ This is the environment implementation of :attr:`BaseLayer.color`. This must return a :ref:`type-color` defining the color assigned to the layer. If the layer does not have an assigned color, the returned value must be ``None``. It will be normalized with :func:`normalizers.normalizeColor`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_color(self, value, **kwargs): """ This is the environment implementation of :attr:`BaseLayer.color`. **value** will be a :ref:`type-color` or ``None`` defining the color to assign to the layer. It will have been normalized with :func:`normalizers.normalizeColor`. Subclasses must override this method. """ self.raiseNotImplementedError() # ----------- # Sub-Objects # ----------- # lib lib = dynamicProperty( "lib", """ The layer's :class:`BaseLib` object. :: >>> layer.lib["org.robofab.hello"] "world" """) def _get_base_lib(self): lib = self._get_lib() lib.font = self return lib def _get_lib(self): """ This is the environment implementation of :attr:`BaseLayer.lib`. This must return an instance of a :class:`BaseLib` subclass. """ self.raiseNotImplementedError() # ----------------- # Global Operations # ----------------- def round(self): """ Round all approriate data to integers. :: >>> layer.round() This is the equivalent of calling the round method on: * all glyphs in the layer """ self._round() def _round(self): """ This is the environment implementation of :meth:`BaseLayer.round`. Subclasses may override this method. """ for glyph in self: glyph.round() def autoUnicodes(self): """ Use heuristics to set Unicode values in all glyphs. :: >>> layer.autoUnicodes() Environments will define their own heuristics for automatically determining values. """ self._autoUnicodes() def _autoUnicodes(self): """ This is the environment implementation of :meth:`BaseLayer.autoUnicodes`. Subclasses may override this method. """ for glyph in self: glyph.autoUnicodes() # ------------- # Interpolation # ------------- def interpolate(self, factor, minLayer, maxLayer, round=True, suppressError=True): """ Interpolate all possible data in the layer. :: >>> layer.interpolate(0.5, otherLayer1, otherLayer2) >>> layer.interpolate((0.5, 2.0), otherLayer1, otherLayer2, round=False) The interpolation occurs on a 0 to 1.0 range where **minLayer** is located at 0 and **maxLayer** 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(minLayer, BaseLayer): raise TypeError( ("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, minLayer.__class__.__name__)) if not isinstance(maxLayer, BaseLayer): raise TypeError( ("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, maxLayer.__class__.__name__)) round = normalizers.normalizeBoolean(round) suppressError = normalizers.normalizeBoolean(suppressError) self._interpolate(factor, minLayer, maxLayer, round=round, suppressError=suppressError) def _interpolate(self, factor, minLayer, maxLayer, round=True, suppressError=True): """ This is the environment implementation of :meth:`BaseLayer.interpolate`. Subclasses may override this method. """ for glyphName in self.keys(): del self[glyphName] for glyphName in minLayer.keys(): if glyphName not in maxLayer: continue minGlyph = minLayer[glyphName] maxGlyph = maxLayer[glyphName] dstGlyph = self.newGlyph(glyphName) dstGlyph.interpolate(factor, minGlyph, maxGlyph, round=round, suppressError=suppressError) compatibilityReporterClass = LayerCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. :: >>> compat, report = self.isCompatible(otherLayer) >>> compat False >>> report A - [Fatal] The glyphs do not contain the same number of contours. This will return a ``bool`` indicating if the layer is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseLayer, self).isCompatible(other, BaseLayer) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseLayer.isCompatible`. Subclasses may override this method. """ layer1 = self layer2 = other # incompatible number of glyphs glyphs1 = set(layer1.keys()) glyphs2 = set(layer2.keys()) if len(glyphs1) != len(glyphs2): reporter.glyphCountDifference = True reporter.warning = True if len(glyphs1.difference(glyphs2)) != 0: reporter.warning = True reporter.glyphsMissingFromLayer2 = list( glyphs1.difference(glyphs2)) if len(glyphs2.difference(glyphs1)) != 0: reporter.warning = True reporter.glyphsMissingInLayer1 = list(glyphs2.difference(glyphs1)) # test glyphs for glyphName in sorted(glyphs1.intersection(glyphs2)): glyph1 = layer1[glyphName] glyph2 = layer2[glyphName] glyphCompatibility = glyph1.isCompatible(glyph2)[1] if glyphCompatibility.fatal or glyphCompatibility.warning: if glyphCompatibility.fatal: reporter.fatal = True if glyphCompatibility.warning: reporter.warning = True reporter.glyphs.append(glyphCompatibility) # ------- # mapping # ------- def getReverseComponentMapping(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._getReverseComponentMapping() def _getReverseComponentMapping(self): """ This is the environment implementation of :meth:`BaseFont.getReverseComponentMapping`. Subclasses may override this method. """ self.raiseNotImplementedError() def getCharacterMapping(self): """ Get a reversed map of component references in the font. { 'A' : ['Aacute', 'Aring'] 'acute' : ['Aacute'] 'ring' : ['Aring'] etc. } """ return self._getCharacterMapping() def _getCharacterMapping(self): """ This is the environment implementation of :meth:`BaseFont.getCharacterMapping`. Subclasses may override this method. """ self.raiseNotImplementedError()
class BaseKerning(BaseDict, DeprecatedKerning, RemovedKerning): """ A Kerning object. This object normally created as part of a :class:`BaseFont`. An orphan Kerning object can be created like this:: >>> groups = RKerning() This object behaves like a Python dictionary. Most of the dictionary functionality comes from :class:`BaseDict`, look at that object for the required environment implementation details. Kerning uses :func:`normalizers.normalizeKerningKey` to normalize the key of the ``dict``, and :func:`normalizers.normalizeKerningValue` to normalize the the value of the ``dict``. """ keyNormalizer = normalizers.normalizeKerningKey valueNormalizer = normalizers.normalizeKerningValue 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 Kerning's parent :class:`BaseFont`.") def _get_font(self): if self._font is None: return None return self._font() def _set_font(self, font): assert self._font is None or self._font() == font if font is not None: font = reference(font) self._font = font # -------------- # Transformation # -------------- def scaleBy(self, factor): """ Scales all kerning values by **factor**. **factor** will be an :ref:`type-int-float`, ``tuple`` or ``list``. The first value of the **factor** will be used to scale the kerning values. >>> myKerning.scaleBy(2) >>> myKerning.scaleBy((2,3)) """ factor = normalizers.normalizeTransformationScale(factor) self._scale(factor) def _scale(self, factor): """ This is the environment implementation of :meth:`BaseKerning.scaleBy`. **factor** will be a ``tuple``. Subclasses may override this method. """ factor = factor[0] for k, v in self.items(): v *= factor self[k] = v # ------------- # Normalization # ------------- def round(self, multiple=1): """ Rounds the kerning values to increments of **multiple**, which will be an ``int``. The default behavior is to round to increments of 1. """ if not isinstance(multiple, int): raise TypeError("The round multiple must be an int not %s." % multiple.__class__.__name__) self._round(multiple) def _round(self, multiple=1): """ This is the environment implementation of :meth:`BaseKerning.round`. **multiple** will be an ``int``. Subclasses may override this method. """ for pair, value in self.items(): value = int(normalizers.normalizeRounding( value / float(multiple))) * multiple self[pair] = value # ------------- # Interpolation # ------------- def interpolate(self, factor, minKerning, maxKerning, round=True, suppressError=True): """ Interpolates all pairs between two :class:`BaseKerning` objects: **minKerning** and **maxKerning**. The interpolation occurs on a 0 to 1.0 range where **minKerning** is located at 0 and **maxKerning** is located at 1.0. The kerning data is replaced by the interpolated kerning. * **factor** is the interpolation value. It may be less than 0 and greater than 1.0. It may be an :ref:`type-int-float`, ``tuple`` or ``list``. If it is a ``tuple`` or ``list``, the first number indicates the x factor and the second number indicates the y factor. * **round** is a ``bool`` indicating if the result should be rounded to ``int``\s. The default behavior is to round interpolated kerning. * **suppressError** is a ``bool`` indicating if incompatible data should be ignored or if an error should be raised when such incompatibilities are found. The default behavior is to ignore incompatible data. >>> myKerning.interpolate(kerningOne, kerningTwo) """ factor = normalizers.normalizeInterpolationFactor(factor) if not isinstance(minKerning, BaseKerning): raise TypeError( ("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, minKerning.__class__.__name__)) if not isinstance(maxKerning, BaseKerning): raise TypeError( ("Interpolation to an instance of %r can not be " "performed from an instance of %r.") % (self.__class__.__name__, maxKerning.__class__.__name__)) round = normalizers.normalizeBoolean(round) suppressError = normalizers.normalizeBoolean(suppressError) self._interpolate(factor, minKerning, maxKerning, round=round, suppressError=suppressError) def _interpolate(self, factor, minKerning, maxKerning, round=True, suppressError=True): """ This is the environment implementation of :meth:`BaseKerning.interpolate`. * **factor** will be an :ref:`type-int-float`, ``tuple`` or ``list``. * **minKerning** will be a :class:`BaseKerning` object. * **maxKerning** will be a :class:`BaseKerning` object. * **round** will be a ``bool`` indicating if the interpolated kerning should be rounded. * **suppressError** will be a ``bool`` indicating if incompatible data should be ignored. Subclasses may override this method. """ import fontMath minKerning = fontMath.MathKerning(kerning=minKerning, groups=minKerning.font.groups) maxKerning = fontMath.MathKerning(kerning=maxKerning, groups=maxKerning.font.groups) result = interpolate(minKerning, maxKerning, factor) if round: result.round() self.clear() result.extractKerning(self.font) # --------------------- # RoboFab Compatibility # --------------------- def remove(self, pair): """ Removes a pair from the Kerning. **pair** will be a ``tuple`` of two :ref:`type-string`\s. This is a backwards compatibility method. """ del self[pair] def asDict(self, returnIntegers=True): """ Return the Kerning as a ``dict``. This is a backwards compatibility method. """ d = {} for k, v in self.items(): d[k] = v if not returnIntegers else normalizers.normalizeRounding( v) return d # ------------------- # Inherited Functions # ------------------- def __contains__(self, pair): """ Tests to see if a pair is in the Kerning. **pair** will be a ``tuple`` of two :ref:`type-string`\s. This returns a ``bool`` indicating if the **pair** is in the Kerning. :: >>> ("A", "V") in font.kerning True """ return super(BaseKerning, self).__contains__(pair) def __delitem__(self, pair): """ Removes **pair** from the Kerning. **pair** is a ``tuple`` of two :ref:`type-string`\s.:: >>> del font.kerning[("A","V")] """ super(BaseKerning, self).__delitem__(pair) def __getitem__(self, pair): """ Returns the kerning value of the pair. **pair** is a ``tuple`` of two :ref:`type-string`\s. The returned value will be a :ref:`type-int-float`.:: >>> font.kerning[("A", "V")] -15 It is important to understand that any changes to the returned value will not be reflected in the Kerning object. If one wants to make a change to the value, one should do the following:: >>> value = font.kerning[("A", "V")] >>> value += 10 >>> font.kerning[("A", "V")] = value """ return super(BaseKerning, self).__getitem__(pair) def __iter__(self): """ Iterates through the Kerning, giving the pair for each iteration. The order that the Kerning will iterate though is not fixed nor is it ordered.:: >>> for pair in font.kerning: >>> print pair ("A", "Y") ("A", "V") ("A", "W") """ return super(BaseKerning, self).__iter__() def __len__(self): """ Returns the number of pairs in Kerning as an ``int``.:: >>> len(font.kerning) 5 """ return super(BaseKerning, self).__len__() def __setitem__(self, pair, value): """ Sets the **pair** to the list of **value**. **pair** is the pair as a ``tuple`` of two :ref:`type-string`\s and **value** is a :ref:`type-int-float`. >>> font.kerning[("A", "V")] = -20 >>> font.kerning[("A", "W")] = -10.5 """ super(BaseKerning, self).__setitem__(pair, value) def clear(self): """ Removes all information from Kerning, resetting the Kerning to an empty dictionary. :: >>> font.kerning.clear() """ super(BaseKerning, self).clear() def get(self, pair, default=None): """ Returns the value for the kerning pair. **pair** is a ``tuple`` of two :ref:`type-string`\s, and the returned values will either be :ref:`type-int-float` or ``None`` if no pair was found. :: >>> font.kerning[("A", "V")] -25 It is important to understand that any changes to the returned value will not be reflected in the Kerning object. If one wants to make a change to the value, one should do the following:: >>> value = font.kerning[("A", "V")] >>> value += 10 >>> font.kerning[("A", "V")] = value """ return super(BaseKerning, self).get(pair, default) def find(self, pair, default=None): """ Returns the value for the kerning pair. **pair** is a ``tuple`` of two :ref:`type-string`\s, and the returned values will either be :ref:`type-int-float` or ``None`` if no pair was found. :: >>> font.kerning[("A", "V")] -25 """ pair = normalizers.normalizeKerningKey(pair) value = self._find(pair, default) if value != default: value = normalizers.normalizeKerningValue(value) return value def _find(self, pair, default=None): """ This is the environment implementation of :attr:`BaseKerning.find`. This must return an :ref:`type-int-float` or `default`. """ from ufoLib.kerning import lookupKerningValue font = self.font groups = font.groups return lookupKerningValue(pair, self, groups, fallback=default) def items(self): """ Returns a list of ``tuple``\s of each pair and value. Pairs are a ``tuple`` of two :ref:`type-string`\s and values are :ref:`type-int-float`. The initial list will be unordered. >>> font.kerning.items() [(("A", "V"), -30), (("A", "W"), -10)] """ return super(BaseKerning, self).items() def keys(self): """ Returns a ``list`` of all the pairs in Kerning. This list will be unordered.:: >>> font.kerning.keys() [("A", "Y"), ("A", "V"), ("A", "W")] """ return super(BaseKerning, self).keys() def pop(self, pair, default=None): """ Removes the **pair** from the Kerning and returns the value as an ``int``. If no pair is found, **default** is returned. **pair** is a ``tuple`` of two :ref:`type-string`\s. This must return either **default** or a :ref:`type-int-float`. >>> font.kerning.pop(("A", "V")) -20 >>> font.kerning.pop(("A", "W")) -10.5 """ return super(BaseKerning, self).pop(pair, default) def update(self, otherKerning): """ Updates the Kerning based on **otherKerning**. **otherKerning** is a ``dict`` of kerning information. If a pair from **otherKerning** is in Kerning, the pair value will be replaced by the value from **otherKerning**. If a pair from **otherKerning** is not in the Kerning, it is added to the pairs. If Kerning contains a pair that is not in **otherKerning**, it is not changed. >>> font.kerning.update(newKerning) """ super(BaseKerning, self).update(otherKerning) def values(self): """ Returns a ``list`` of each pair's values, the values will be :ref:`type-int-float`\s. The list will be unordered. >>> font.kerning.items() [-20, -15, 5, 3.5] """ return super(BaseKerning, self).values()
class BaseGuideline(BaseObject, TransformationMixin, DeprecatedGuideline, RemovedGuideline, PointPositionMixin, InterpolationMixin, SelectionMixin): """ A guideline object. This object is almost always created with :meth:`BaseGlyph.appendGuideline`. An orphan guideline can be created like this:: >>> guideline = RGuideline() """ copyAttributes = ("x", "y", "angle", "name", "color") def _reprContents(self): contents = [] if self.name is not None: contents.append("'%s'" % self.name) if self.layer is not None: contents.append("('%s')" % self.layer.name) return contents # ------- # Parents # ------- def getParent(self): """ Return the guideline's parent :class:`fontParts.base.BaseGlyph`. This is a backwards compatibility method. """ glyph = self.glyph if glyph is not None: return glyph return self.font # Glyph _glyph = None glyph = dynamicProperty("glyph", "The guideline's parent :class:`BaseGlyph`.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): assert self._font is None assert self._glyph is None if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Layer layer = dynamicProperty("layer", "The guideline's parent :class:`BaseLayer`.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # Font _font = None font = dynamicProperty("font", "The guideline's parent :class:`BaseFont`.") def _get_font(self): if self._font is not None: return self._font() elif self._glyph is not None: return self.glyph.font return None def _set_font(self, font): assert self._font is None assert self._glyph is None if font is not None: font = reference(font) self._font = font # -------- # Position # -------- # x x = dynamicProperty( "base_x", """ The x coordinate of the guideline. It must be an :ref:`type-int-float`. :: >>> guideline.x 100 >>> guideline.x = 101 """) def _get_base_x(self): value = self._get_x() if value is None: return 0 value = normalizers.normalizeX(value) return value def _set_base_x(self, value): if value is None: value = 0 else: value = normalizers.normalizeX(value) self._set_x(value) def _get_x(self): """ This is the environment implementation of :attr:`BaseGuideline.x`. This must return an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_x(self, value): """ This is the environment implementation of :attr:`BaseGuideline.x`. **value** will be an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() # y y = dynamicProperty( "base_y", """ The y coordinate of the guideline. It must be an :ref:`type-int-float`. :: >>> guideline.y 100 >>> guideline.y = 101 """) def _get_base_y(self): value = self._get_y() if value is None: return 0 value = normalizers.normalizeY(value) return value def _set_base_y(self, value): if value is None: value = 0 else: value = normalizers.normalizeY(value) self._set_y(value) def _get_y(self): """ This is the environment implementation of :attr:`BaseGuideline.y`. This must return an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_y(self, value): """ This is the environment implementation of :attr:`BaseGuideline.y`. **value** will be an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() # angle angle = dynamicProperty( "base_angle", """ The angle of the guideline. It must be an :ref:`type-angle`. Please check how :func:`normalizers.normalizeGuidelineAngle` handles the angle. There is a special case, when angle is ``None``. If so, when x and y are not 0, the angle will be 0. If x is 0 but y is not, the angle will be 0. If y is 0 and x is not, the angle will be 90. If both x and y are 0, the angle will be 0. :: >>> guideline.angle 45.0 >>> guideline.angle = 90 """) def _get_base_angle(self): value = self._get_angle() if value is None: if self._get_x() != 0 and self._get_y() != 0: value = 0 elif self._get_x() != 0 and self._get_y() == 0: value = 90 elif self._get_x() == 0 and self._get_y() != 0: value = 0 else: value = 0 value = normalizers.normalizeGuidelineAngle(value) return value def _set_base_angle(self, value): if value is None: if self._get_x() != 0 and self._get_y() != 0: value = 0 elif self._get_x() != 0 and self._get_y() == 0: value = 90 elif self._get_x() == 0 and self._get_y() != 0: value = 0 else: value = 0 value = normalizers.normalizeGuidelineAngle(value) self._set_angle(value) def _get_angle(self): """ This is the environment implementation of :attr:`BaseGuideline.angle`. This must return an :ref:`type-angle`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_angle(self, value): """ This is the environment implementation of :attr:`BaseGuideline.angle`. **value** will be an :ref:`type-angle`. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Identification # -------------- # index index = dynamicProperty( "base_index", """ The index of the guideline within the ordered list of the parent glyph's guidelines. This attribute is read only. :: >>> guideline.index 0 """) def _get_base_index(self): value = self._get_index() value = normalizers.normalizeIndex(value) return value def _get_index(self): """ Get the guideline's index. This must return an ``int``. Subclasses may override this method. """ glyph = self.glyph if glyph is not None: parent = glyph else: parent = self.font if parent is None: return None return parent.guidelines.index(self) # name name = dynamicProperty( "base_name", """ The name of the guideline. This will be a :ref:`type-string` or ``None``. >>> guideline.name 'my guideline' >>> guideline.name = None """) def _get_base_name(self): value = self._get_name() if value is not None: value = normalizers.normalizeGuidelineName(value) return value def _set_base_name(self, value): if value is not None: value = normalizers.normalizeGuidelineName(value) self._set_value(value) def _get_name(self): """ This is the environment implementation of :attr:`BaseGuideline.name`. This must return a :ref:`type-string` or ``None``. The returned value will be normalized with :func:`normalizers.normalizeGuidelineName`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_name(self, value): """ This is the environment implementation of :attr:`BaseGuideline.name`. **value** will be a :ref:`type-string` or ``None``. It will have been normalized with :func:`normalizers.normalizeGuidelineName`. Subclasses must override this method. """ self.raiseNotImplementedError() # identifier identifier = dynamicProperty( "base_identifier", """ The unique identifier for the guideline. This value will be an :ref:`type-identifier` or `None`. This attribute is read only. :: >>> guideline.identifier 'ILHGJlygfds' To request an identifier if it does not exist use `guideline.getIdentifier()` """) def _get_base_identifier(self): value = self._get_identifier() value = normalizers.normalizeIdentifier(value) return value def _get_identifier(self): """ This is the environment implementation of :attr:`BaseGuideline.identifier`. This must return an :ref:`type-identifier`. If the native guideline does not have an identifier assigned, one should be assigned and returned. Subclasses must override this method. """ self.raiseNotImplementedError() def getIdentifier(self): """ Create a new, unique identifier for and assign it to the guideline. If the guideline already has an identifier, the existing one should be returned. """ return self._getIdentifier() def _getIdentifier(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def _setIdentifier(self, value): """ This method is used internally to force a specific identifier onto an object in certain situations. Subclasses that allow setting an identifier to a specific value may override this method. """ pass # color color = dynamicProperty( "base_color", """" The guideline's color. This will be a :ref:`type-color` or ``None``. :: >>> guideline.color None >>> guideline.color = (1, 0, 0, 0.5) """) def _get_base_color(self): value = self._get_color() if value is not None: value = normalizers.normalizeColor(value) value = Color(value) return value def _set_base_color(self, value): if value is not None: value = normalizers.normalizeColor(value) self._set_color(value) def _get_color(self): """ This is the environment implementation of :attr:`BaseGuideline.color`. This must return a :ref:`type-color` or ``None``. The returned value will be normalized with :func:`normalizers.normalizeColor`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_color(self, value): """ This is the environment implementation of :attr:`BaseGuideline.color`. **value** will be a :ref:`type-color` or ``None``. It will have been normalized with :func:`normalizers.normalizeColor`. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ This is the environment implementation of :meth:`BaseGuideline.transformBy`. **matrix** will be a :ref:`type-transformation`. that has been normalized with :func:`normalizers.normalizeTransformationMatrix`. Subclasses may override this method. """ t = transform.Transform(*matrix) # coordinates x, y = t.transformPoint((self.x, self.y)) self.x = x self.y = y # angle angle = math.radians(self.angle) dx = math.cos(angle) dy = math.sin(angle) tdx, tdy = t.transformPoint((dx, dy)) ta = math.atan2(tdy, tdx) self.angle = math.degrees(ta) # ------------- # Interpolation # ------------- compatibilityReporterClass = GuidelineCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. :: >>> compatible, report = self.isCompatible(otherGuideline) >>> compatible True >>> compatible [Warning] Guideline: "xheight" + "cap_height" [Warning] Guideline: "xheight" has name xheight | "cap_height" has name cap_height This will return a ``bool`` indicating if the guideline is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseGuideline, self).isCompatible(other, BaseGuideline) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseGuideline.isCompatible`. Subclasses may override this method. """ guideline1 = self guideline2 = other # guideline names if guideline1.name != guideline2.name: reporter.nameDifference = True reporter.warning = True # ------------- # Normalization # ------------- def round(self): """ Round the guideline's coordinate. >>> guideline.round() This applies to the following: * x * y It does not apply to * angle """ self._round() def _round(self, **kwargs): """ This is the environment implementation of :meth:`BaseGuideline.round`. Subclasses may override this method. """ self.x = normalizers.normalizeRounding(self.x) self.y = normalizers.normalizeRounding(self.y)
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 def __hash__(self): """ Allow font object to be used as a key in a dictionary. Subclasses may override this method. """ return id(self.naked()) # ---- # Copy # ---- copyAttributes = ("info", "groups", "kerning", "features", "lib", "layerOrder", "defaultLayerName", "glyphOrder") 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. """ 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 sourceGuideline in self.guidelines: selfGuideline = self.appendGuideline((0, 0), 0) selfGuideline.copyData(sourceGuideline) super(BaseFont, self).copyData(source) # --------------- # File Operations # --------------- # Initialize 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 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 def save(self, path=None, showProgress=False, formatVersion=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. .. 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) self._save(path=path, showProgress=showProgress, formatVersion=formatVersion) def _save(self, path=None, showProgress=False, formatVersion=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 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() def _close(self, **kwargs): """ This is the environment implementation of :meth:`BaseFont.close`. Subclasses must override this method. """ self.raiseNotImplementedError() # generate def generateFormatToExtension(self, 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) def generate(self, format, path=None): """ Generate the font to another format. >>> font.generate("otfcff") >>> font.generate("otfcff", "/path/to/my/font.otf") **format** defines the file format to output. These are the standard format identifiers: %s 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. """ if format is None: raise ValueError("The format must be defined when generating.") elif not isinstance(format, basestring): raise TypeError("The format must be defined as a string.") 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) generate.__doc__ %= generateFormatToExtension.__doc__ def _generate(self, format, path, **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`. 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 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 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 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() # 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 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 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() # ----------------- # 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) 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) 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() 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 """) def _get_defaultLayer(self): layer = self._get_base_defaultLayer() layer = normalizers.normalizeLayer(layer) return layer def _set_defaultLayer(self): 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 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 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 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 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 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) 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() # ----------------- # Glyph Interaction # ----------------- # base implementation overrides 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] 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() 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) 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) # 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) 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() 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 # ----------------- 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() 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() 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() 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 """) 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() 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.normalizeGuidelineIndex(index) if index >= self._len__guidelines(): raise ValueError("No guideline located at index %d." % index) guideline = self._getGuideline(index) self._setFontInGuideline(guideline) return guideline 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.") def appendGuideline(self, position, angle, name=None, color=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. """ position = normalizers.normalizeCoordinateTuple(position) angle = normalizers.normalizeGuidelineAngle(angle) if name is not None: name = normalizers.normalizeGuidelineName(name) if color is not None: color = normalizers.normalizeColor(color) return self._appendGuideline(position, angle, name=name, color=color) def _appendGuideline(self, position, angle, name=None, color=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() 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.normalizeGuidelineIndex(index) if index >= self._len__guidelines(): raise ValueError("No guideline located at index %d." % index) self._removeGuideline(index) 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() def clearGuidelines(self): """ Clear all guidelines. >>> font.clearGuidelines() """ self._clearGuidelines() 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 # ------------- 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) 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) # 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 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) 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): """ 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._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): """ Get a reversed map of component references in the font. { 'A' : ['Aacute', 'Aring'] 'acute' : ['Aacute'] 'ring' : ['Aring'] etc. } """ 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.normalizeGuidelineIndex(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)
class BaseSegment( BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, DeprecatedSegment, RemovedSegment ): def _setPoints(self, points): assert not hasattr(self, "_points") self._points = points def _reprContents(self): contents = [ "%s" % self.type, ] if self.index is not None: contents.append("index='%r'" % self.index) return contents # ------- # Parents # ------- # Contour _contour = None contour = dynamicProperty("contour", "The segment's parent contour.") def _get_contour(self): if self._contour is None: return None return self._contour() def _set_contour(self, contour): assert self._contour is None if contour is not None: contour = reference(contour) self._contour = contour # Glyph glyph = dynamicProperty("glyph", "The segment's parent glyph.") def _get_glyph(self): if self._contour is None: return None return self.contour.glyph # Layer layer = dynamicProperty("layer", "The segment's parent layer.") def _get_layer(self): if self._contour is None: return None return self.glyph.layer # Font font = dynamicProperty("font", "The segment's parent font.") def _get_font(self): if self._contour is None: return None return self.glyph.font # -------- # Equality # -------- def __eq__(self, other): """ The :meth:`BaseObject.__eq__` method can't be used here because the :class:`BaseContour` implementation contructs segment objects without assigning an underlying ``naked`` object. Therefore, comparisons will always fail. This method overrides the base method and compares the :class:`BasePoint` contained by the segment. Subclasses may override this method. """ if isinstance(other, self.__class__): return self.points == other.points return NotImplemented # -------------- # Identification # -------------- index = dynamicProperty("base_index", ("The index of the segment within the ordered " "list of the parent contour's segments.") ) def _get_base_index(self): if self.contour is None: return None value = self._get_index() value = normalizers.normalizeIndex(value) return value def _get_index(self): """ Subclasses may override this method. """ contour = self.contour value = contour.segments.index(self) return value # ---------- # Attributes # ---------- type = dynamicProperty("base_type", ("The segment type. The possible types are " "move, line, curve, qcurve.") ) def _get_base_type(self): value = self._get_type() value = normalizers.normalizeSegmentType(value) return value def _set_base_type(self, value): value = normalizers.normalizeSegmentType(value) self._set_type(value) def _get_type(self): """ Subclasses may override this method. """ value = self.onCurve.type return value def _set_type(self, newType): """ Subclasses may override this method. """ oldType = self.type if oldType == newType: return contour = self.contour if contour is None: raise FontPartsError("The segment does not belong to a contour.") # converting line <-> move if newType in ("move", "line") and oldType in ("move", "line"): pass # converting to a move or line elif newType not in ("curve", "qcurve"): offCurves = self.offCurve for point in offCurves: contour.removePoint(point) # converting a line/move to a curve/qcurve else: segments = contour.segments i = segments.index(self) prev = segments[i - 1].onCurve on = self.onCurve x = on.x y = on.y points = contour.points i = points.index(on) contour.insertPoint(i, (x, y), "offcurve") off2 = contour.points[i] contour.insertPoint(i, (prev.x, prev.y), "offcurve") off1 = contour.points[i] del self._points self._setPoints((off1, off2, on)) self.onCurve.type = newType smooth = dynamicProperty("base_smooth", ("Boolean indicating if the segment is " "smooth or not.") ) def _get_base_smooth(self): value = self._get_smooth() value = normalizers.normalizeBoolean(value) return value def _set_base_smooth(self, value): value = normalizers.normalizeBoolean(value) self._set_smooth(value) def _get_smooth(self): """ Subclasses may override this method. """ return self.onCurve.smooth def _set_smooth(self, value): """ Subclasses may override this method. """ self.onCurve.smooth = value # ------ # Points # ------ def __getitem__(self, index): return self._getItem(index) def _getItem(self, index): """ Subclasses may override this method. """ return self.points[index] def __iter__(self): return self._iterPoints() def _iterPoints(self, **kwargs): """ Subclasses may override this method. """ points = self.points count = len(points) index = 0 while count: yield points[index] count -= 1 index += 1 def __len__(self): return self._len() def _len(self, **kwargs): """ Subclasses may override this method. """ return len(self.points) points = dynamicProperty("base_points", "A list of points in the segment.") def _get_base_points(self): return tuple(self._get_points()) def _get_points(self): """ Subclasses may override this method. """ if not hasattr(self, "_points"): return tuple() return tuple(self._points) onCurve = dynamicProperty("base_onCurve", "The on curve point in the segment.") def _get_base_onCurve(self): return self._get_onCurve() def _get_onCurve(self): """ Subclasses may override this method. """ return self.points[-1] offCurve = dynamicProperty("base_offCurve", "The off curve points in the segment.") def _get_base_offCurve(self): """ Subclasses may override this method. """ return self._get_offCurve() def _get_offCurve(self): """ Subclasses may override this method. """ return self.points[:-1] # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ Subclasses may override this method. """ for point in self.points: point.transformBy(matrix) # ------------- # Interpolation # ------------- compatibilityReporterClass = SegmentCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. :: >>> compatible, report = self.isCompatible(otherSegment) >>> compatible False >>> compatible [Fatal] Segment: [0] + [0] [Fatal] Segment: [0] is line | [0] is move [Fatal] Segment: [1] + [1] [Fatal] Segment: [1] is line | [1] is qcurve This will return a ``bool`` indicating if the segment is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseSegment, self).isCompatible(other, BaseSegment) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseSegment.isCompatible`. Subclasses may override this method. """ segment1 = self segment2 = other # type if segment1.type != segment2.type: # line <-> curve can be converted if set((segment1.type, segment2.type)) != set(("curve", "line")): reporter.typeDifference = True reporter.fatal = True # ---- # Misc # ---- def round(self): """ Round coordinates in all points. """ for point in self.points: point.round()
class BaseAnchor(BaseObject, TransformationMixin, DeprecatedAnchor, RemovedAnchor, PointPositionMixin, InterpolationMixin, SelectionMixin, IdentifierMixin): """ An anchor object. This object is almost always created with :meth:`BaseGlyph.appendAnchor`. An orphan anchor can be created like this:: >>> anchor = RAnchor() """ def _reprContents(self): contents = [ ("({x}, {y})".format(x=self.x, y=self.y)), ] if self.name is not None: contents.append("name='%s'" % self.name) if self.color: contents.append("color=%r" % str(self.color)) return contents # ---- # Copy # ---- copyAttributes = ("x", "y", "name", "color") # ------- # Parents # ------- # Glyph _glyph = None glyph = dynamicProperty("glyph", "The anchor's parent :class:`BaseGlyph`.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): if self._glyph is not None: raise AssertionError("glyph for anchor already set") if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Layer layer = dynamicProperty("layer", "The anchor's parent :class:`BaseLayer`.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # Font font = dynamicProperty("font", "The anchor's parent :class:`BaseFont`.") def _get_font(self): if self._glyph is None: return None return self.glyph.font # -------- # Position # -------- # x x = dynamicProperty( "base_x", """ The x coordinate of the anchor. It must be an :ref:`type-int-float`. :: >>> anchor.x 100 >>> anchor.x = 101 """) def _get_base_x(self): value = self._get_x() value = normalizers.normalizeX(value) return value def _set_base_x(self, value): value = normalizers.normalizeX(value) self._set_x(value) def _get_x(self): """ This is the environment implementation of :attr:`BaseAnchor.x`. This must return an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_x(self, value): """ This is the environment implementation of :attr:`BaseAnchor.x`. **value** will be an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() # y y = dynamicProperty( "base_y", """ The y coordinate of the anchor. It must be an :ref:`type-int-float`. :: >>> anchor.y 100 >>> anchor.y = 101 """) def _get_base_y(self): value = self._get_y() value = normalizers.normalizeY(value) return value def _set_base_y(self, value): value = normalizers.normalizeY(value) self._set_y(value) def _get_y(self): """ This is the environment implementation of :attr:`BaseAnchor.y`. This must return an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_y(self, value): """ This is the environment implementation of :attr:`BaseAnchor.y`. **value** will be an :ref:`type-int-float`. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Identification # -------------- # index index = dynamicProperty( "base_index", """ The index of the anchor within the ordered list of the parent glyph's anchors. This attribute is read only. :: >>> anchor.index 0 """) def _get_base_index(self): value = self._get_index() value = normalizers.normalizeIndex(value) return value def _get_index(self): """ Get the anchor's index. This must return an ``int``. Subclasses may override this method. """ glyph = self.glyph if glyph is None: return None return glyph.anchors.index(self) # name name = dynamicProperty( "base_name", """ The name of the anchor. This will be a :ref:`type-string` or ``None``. >>> anchor.name 'my anchor' >>> anchor.name = None """) def _get_base_name(self): value = self._get_name() value = normalizers.normalizeAnchorName(value) return value def _set_base_name(self, value): value = normalizers.normalizeAnchorName(value) self._set_name(value) def _get_name(self): """ This is the environment implementation of :attr:`BaseAnchor.name`. This must return a :ref:`type-string` or ``None``. The returned value will be normalized with :func:`normalizers.normalizeAnchorName`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_name(self, value): """ This is the environment implementation of :attr:`BaseAnchor.name`. **value** will be a :ref:`type-string` or ``None``. It will have been normalized with :func:`normalizers.normalizeAnchorName`. Subclasses must override this method. """ self.raiseNotImplementedError() # color color = dynamicProperty( "base_color", """ The anchor's color. This will be a :ref:`type-color` or ``None``. :: >>> anchor.color None >>> anchor.color = (1, 0, 0, 0.5) """) def _get_base_color(self): value = self._get_color() if value is not None: value = normalizers.normalizeColor(value) value = Color(value) return value def _set_base_color(self, value): if value is not None: value = normalizers.normalizeColor(value) self._set_color(value) def _get_color(self): """ This is the environment implementation of :attr:`BaseAnchor.color`. This must return a :ref:`type-color` or ``None``. The returned value will be normalized with :func:`normalizers.normalizeColor`. Subclasses must override this method. """ self.raiseNotImplementedError() def _set_color(self, value): """ This is the environment implementation of :attr:`BaseAnchor.color`. **value** will be a :ref:`type-color` or ``None``. It will have been normalized with :func:`normalizers.normalizeColor`. Subclasses must override this method. """ self.raiseNotImplementedError() # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ This is the environment implementation of :meth:`BaseAnchor.transformBy`. **matrix** will be a :ref:`type-transformation`. that has been normalized with :func:`normalizers.normalizeTransformationMatrix`. Subclasses may override this method. """ t = transform.Transform(*matrix) x, y = t.transformPoint((self.x, self.y)) self.x = x self.y = y # ------------- # Interpolation # ------------- compatibilityReporterClass = AnchorCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. :: >>> compatible, report = self.isCompatible(otherAnchor) >>> compatible True >>> compatible [Warning] Anchor: "left" + "right" [Warning] Anchor: "left" has name left | "right" has name right This will return a ``bool`` indicating if the anchor is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseAnchor, self).isCompatible(other, BaseAnchor) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseAnchor.isCompatible`. Subclasses may override this method. """ anchor1 = self anchor2 = other # base names if anchor1.name != anchor2.name: reporter.nameDifference = True reporter.warning = True # ------------- # Normalization # ------------- def round(self): """ Round the anchor's coordinate. >>> anchor.round() This applies to the following: * x * y """ self._round() def _round(self): """ This is the environment implementation of :meth:`BaseAnchor.round`. Subclasses may override this method. """ self.x = normalizers.normalizeRounding(self.x) self.y = normalizers.normalizeRounding(self.y)
class BaseContour( BaseObject, TransformationMixin, InterpolationMixin, SelectionMixin, IdentifierMixin, DeprecatedContour, RemovedContour ): segmentClass = None bPointClass = None def _reprContents(self): contents = [] if self.identifier is not None: contents.append("identifier='%r'" % self.identifier) if self.glyph is not None: contents.append("in glyph") contents += self.glyph._reprContents() return contents def copyData(self, source): super(BaseContour, self).copyData(source) for sourcePoint in source.points: self.appendPoint((0, 0)) selfPoint = self.points[-1] selfPoint.copyData(sourcePoint) # ------- # Parents # ------- # Glyph _glyph = None glyph = dynamicProperty("glyph", "The contour's parent :class:`BaseGlyph`.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): if self._glyph is not None: raise AssertionError("glyph for contour already set") if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Font font = dynamicProperty("font", "The contour's parent font.") def _get_font(self): if self._glyph is None: return None return self.glyph.font # Layer layer = dynamicProperty("layer", "The contour's parent layer.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # -------------- # Identification # -------------- # index index = dynamicProperty( "base_index", """ The index of the contour within the parent glyph's contours. >>> contour.index 1 >>> contour.index = 0 The value will always be a :ref:`type-int`. """ ) def _get_base_index(self): glyph = self.glyph if glyph is None: return None value = self._get_index() value = normalizers.normalizeIndex(value) return value def _set_base_index(self, value): glyph = self.glyph if glyph is None: raise FontPartsError("The contour does not belong to a glyph.") value = normalizers.normalizeIndex(value) contourCount = len(glyph.contours) if value < 0: value = -(value % contourCount) if value >= contourCount: value = contourCount self._set_index(value) def _get_index(self): """ Subclasses may override this method. """ glyph = self.glyph return glyph.contours.index(self) def _set_index(self, value): """ Subclasses must override this method. """ self.raiseNotImplementedError() # identifier def getIdentifierForPoint(self, point): """ Create a unique identifier for and assign it to ``point``. If the point already has an identifier, the existing identifier will be returned. >>> contour.getIdentifierForPoint(point) 'ILHGJlygfds' ``point`` must be a :class:`BasePoint`. The returned value will be a :ref:`type-identifier`. """ point = normalizers.normalizePoint(point) return self._getIdentifierforPoint(point) def _getIdentifierforPoint(self, point): """ Subclasses must override this method. """ self.raiseNotImplementedError() # ---- # Pens # ---- def draw(self, pen): """ Draw the contour's outline data to the given :ref:`type-pen`. >>> contour.draw(pen) """ self._draw(pen) def _draw(self, pen, **kwargs): """ Subclasses may override this method. """ from ufoLib.pointPen import PointToSegmentPen adapter = PointToSegmentPen(pen) self.drawPoints(adapter) def drawPoints(self, pen): """ Draw the contour's outline data to the given :ref:`type-point-pen`. >>> contour.drawPoints(pointPen) """ self._drawPoints(pen) def _drawPoints(self, pen, **kwargs): """ Subclasses may override this method. """ # The try: ... except TypeError: ... # handles backwards compatibility with # point pens that have not been upgraded # to point pen protocol 2. try: pen.beginPath(self.identifier) except TypeError: pen.beginPath() for point in self.points: typ = point.type if typ == "offcurve": typ = None try: pen.addPoint(pt=(point.x, point.y), segmentType=typ, smooth=point.smooth, name=point.name, identifier=point.identifier) except TypeError: pen.addPoint(pt=(point.x, point.y), segmentType=typ, smooth=point.smooth, name=point.name) pen.endPath() # ------------------ # Data normalization # ------------------ def autoStartSegment(self): """ Automatically calculate and set the first segment in this contour. The behavior of this may vary accross environments. """ self._autoStartSegment() def _autoStartSegment(self, **kwargs): """ Subclasses may override this method. XXX port this from robofab """ startIndex = 0 startSegment = self.segments[0] for i in range(len(self.segments)): segment = self.segments[i] startOn = startSegment.onCurve on = segment.onCurve if on.y <= startOn.y: if on.y == startOn.y: if on.x < startOn.x: startSegment = segment startIndex = i else: startSegment = segment startIndex = i if startIndex != 0: self.setStartSegment(startIndex) def round(self): """ Round coordinates in all points to integers. """ self._round() def _round(self, **kwargs): """ Subclasses may override this method. """ for point in self.points: point.round() # -------------- # Transformation # -------------- def _transformBy(self, matrix, **kwargs): """ Subclasses may override this method. """ for point in self.points: point.transformBy(matrix) # ------------- # Interpolation # ------------- compatibilityReporterClass = ContourCompatibilityReporter def isCompatible(self, other): """ Evaluate interpolation compatibility with **other**. :: >>> compatible, report = self.isCompatible(otherContour) >>> compatible False >>> compatible [Fatal] Contour: [0] + [0] [Fatal] Contour: [0] contains 4 segments | [0] contains 3 segments [Fatal] Contour: [0] is closed | [0] is open This will return a ``bool`` indicating if the contour is compatible for interpolation with **other** and a :ref:`type-string` of compatibility notes. """ return super(BaseContour, self).isCompatible(other, BaseContour) def _isCompatible(self, other, reporter): """ This is the environment implementation of :meth:`BaseContour.isCompatible`. Subclasses may override this method. """ contour1 = self contour2 = other # open/closed if contour1.open != contour2.open: reporter.openDifference = True # direction if contour1.clockwise != contour2.clockwise: reporter.directionDifference = True # segment count if len(contour1) != len(contour2.segments): reporter.segmentCountDifference = True reporter.fatal = True # segment pairs for i in range(min(len(contour1), len(contour2))): segment1 = contour1[i] segment2 = contour2[i] segmentCompatibility = segment1.isCompatible(segment2)[1] if segmentCompatibility.fatal or segmentCompatibility.warning: if segmentCompatibility.fatal: reporter.fatal = True if segmentCompatibility.warning: reporter.warning = True reporter.segments.append(segmentCompatibility) # ---- # Open # ---- open = dynamicProperty("base_open", "Boolean indicating if the contour is open.") def _get_base_open(self): value = self._get_open() value = normalizers.normalizeBoolean(value) return value def _get_open(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() # --------- # Direction # --------- clockwise = dynamicProperty("base_clockwise", ("Boolean indicating if the contour's " "winding direction is clockwise.")) def _get_base_clockwise(self): value = self._get_clockwise() value = normalizers.normalizeBoolean(value) return value def _set_base_clockwise(self, value): value = normalizers.normalizeBoolean(value) self._set_clockwise(value) def _get_clockwise(self): """ Subclasses must override this method. """ self.raiseNotImplementedError() def _set_clockwise(self, value): """ Subclasses may override this method. """ if self.clockwise != value: self.reverse() def reverse(self): """ Reverse the direction of the contour. """ self._reverseContour() def _reverse(self, **kwargs): """ Subclasses may override this method. """ self.raiseNotImplementedError() # ------------------------ # Point and Contour Inside # ------------------------ def pointInside(self, point): """ Determine if ``point`` is in the black or white of the contour. >>> contour.pointInside((40, 65)) True ``point`` must be a :ref:`type-coordinate`. """ point = normalizers.normalizeCoordinateTuple(point) return self._pointInside(point) 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() def contourInside(self, otherContour): """ Determine if ``otherContour`` is in the black or white of this contour. >>> contour.contourInside(otherContour) True ``contour`` must be a :class:`BaseContour`. """ otherContour = normalizers.normalizeContour(otherContour) return self._contourInside(otherContour) def _contourInside(self, otherContour): """ Subclasses may override this method. """ self.raiseNotImplementedError() # --------------- # Bounds and Area # --------------- bounds = dynamicProperty("bounds", ("The bounds of the contour: " "(xMin, yMin, xMax, yMax) or None.")) def _get_base_bounds(self): value = self._get_bounds() if value is not None: value = normalizers.normalizeBoundingBox(value) return value 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 contour: " "A positive number or None.")) 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) # -------- # Segments # -------- # The base class implements the full segment interaction API. # Subclasses do not need to override anything within the contour # other than registering segmentClass. Subclasses may choose to # implement this API independently if desired. def _setContourInSegment(self, segment): if segment.contour is None: segment.contour = self segments = dynamicProperty("segments") def _get_segments(self): """ Subclasses may override this method. """ points = list(self.points) segments = [[]] lastWasOffCurve = False firstIsMove = points[0].type == "move" for point in points: segments[-1].append(point) if point.type != "offcurve": segments.append([]) lastWasOffCurve = point.type == "offcurve" if len(segments[-1]) == 0: del segments[-1] if lastWasOffCurve and firstIsMove: # ignore trailing off curves del segments[-1] if lastWasOffCurve and not firstIsMove: segment = segments.pop(-1) if len(segments[0]) != 1: raise AssertionError("Length of segments[0] is not 1") segment.append(segments[0][0]) del segments[0] segments.append(segment) if not lastWasOffCurve and not firstIsMove: segment = segments.pop(0) segments.append(segment) # wrap into segments wrapped = [] for points in segments: s = self.segmentClass() s._setPoints(points) self._setContourInSegment(s) wrapped.append(s) return wrapped def __getitem__(self, index): return self.segments[index] def __iter__(self): return self._iterSegments() def _iterSegments(self): segments = self.segments count = len(segments) index = 0 while count: yield segments[index] count -= 1 index += 1 def __len__(self): return self._len__segments() def _len__segments(self, **kwargs): """ Subclasses may override this method. """ return len(self.segments) def appendSegment(self, type=None, points=None, smooth=False, segment=None): """ Append a segment to the contour. """ if segment is not None: if type is not None: type = segment.type if points is None: points = [(point.x, point.y) for point in segment.points] smooth = segment.smooth type = normalizers.normalizeSegmentType(type) pts = [] for pt in points: pt = normalizers.normalizeCoordinateTuple(pt) pts.append(pt) points = pts smooth = normalizers.normalizeBoolean(smooth) self._appendSegment(type=type, points=points, smooth=smooth) def _appendSegment(self, type=None, points=None, smooth=False, **kwargs): """ Subclasses may override this method. """ self._insertSegment(len(self), type=type, points=points, smooth=smooth, **kwargs) def insertSegment(self, index, type=None, points=None, smooth=False, segment=None): """ Insert a segment into the contour. """ if segment is not None: if type is not None: type = segment.type if points is None: points = [(point.x, point.y) for point in segment.points] smooth = segment.smooth index = normalizers.normalizeIndex(index) type = normalizers.normalizeSegmentType(type) pts = [] for pt in points: pt = normalizers.normalizeCoordinateTuple(pt) pts.append(pt) points = pts smooth = normalizers.normalizeBoolean(smooth) self._insertSegment(index=index, type=type, points=points, smooth=smooth) def _insertSegment(self, index=None, type=None, points=None, smooth=False, **kwargs): """ Subclasses may override this method. """ onCurve = points[-1] offCurve = points[:-1] ptCount = sum([len(self.segments[s].points) for s in range(index)]) self.insertPoint(ptCount, onCurve, type=type, smooth=smooth) for offCurvePoint in reversed(offCurve): self.insertPoint(ptCount, offCurvePoint, type="offcurve") def removeSegment(self, segment, preserveCurve=False): """ Remove segment from the contour. If ``preserveCurve`` is set to ``True`` an attempt will be made to preserve the shape of the curve if the environment supports that functionality. """ if not isinstance(segment, int): segment = self.segments.index(segment) segment = normalizers.normalizeIndex(segment) if segment >= self._len__segments(): raise ValueError("No segment located at index %d." % segment) preserveCurve = normalizers.normalizeBoolean(preserveCurve) self._removeSegment(segment, preserveCurve) def _removeSegment(self, segment, preserveCurve, **kwargs): """ segment will be a valid segment index. preserveCurve will be a boolean. Subclasses may override this method. """ segment = self.segments[segment] for point in segment.points: self.removePoint(point, preserveCurve) def setStartSegment(self, segment): """ Set the first segment on the contour. segment can be a segment object or an index. """ segments = self.segments if not isinstance(segment, int): segmentIndex = segments.index(segment) else: segmentIndex = segment if len(self.segments) < 2: return if segmentIndex == 0: return if segmentIndex >= len(segments): raise ValueError(("The contour does not contain a segment " "at index %d" % segmentIndex)) self._setStartSegment(segmentIndex) def _setStartSegment(self, segmentIndex, **kwargs): """ Subclasses may override this method. """ segments = self.segments oldStart = self.segments[0] oldLast = self.segments[-1] # If the contour ends with a curve on top of a move, # delete the move. if oldLast.type == "curve" or oldLast.type == "qcurve": startOn = oldStart.onCurve lastOn = oldLast.onCurve if startOn.x == lastOn.x and startOn.y == lastOn.y: self.removeSegment(0) # Shift new the start index. segmentIndex = segmentIndex - 1 segments = self.segments # If the first point is a move, convert it to a line. if segments[0].type == "move": segments[0].type = "line" # Reorder the points internally. segments = segments[segmentIndex:] + segments[:segmentIndex] points = [] for segment in segments: for point in segment: points.append(((point.x, point.y), point.type, point.smooth, point.name, point.identifier)) # Clear the points. for point in self.points: self.removePoint(point) # Add the points. for point in points: position, type, smooth, name, identifier = point self.appendPoint( position, type=type, smooth=smooth, name=name, identifier=identifier ) # ------- # bPoints # ------- bPoints = dynamicProperty("bPoints") def _get_bPoints(self): bPoints = [] for point in self.points: if point.type not in ("move", "line", "curve"): continue bPoint = self.bPointClass() bPoint.contour = self bPoint._setPoint(point) bPoints.append(bPoint) return tuple(bPoints) def appendBPoint(self, type=None, anchor=None, bcpIn=None, bcpOut=None, bPoint=None): """ Append a bPoint to the contour. """ if bPoint is not None: if type is None: type = bPoint.type if anchor is None: anchor = bPoint.anchor if bcpIn is None: bcpIn = bPoint.bcpIn if bcpOut is None: bcpOut = bPoint.bcpOut type = normalizers.normalizeBPointType(type) anchor = normalizers.normalizeCoordinateTuple(anchor) if bcpIn is None: bcpIn = (0, 0) bcpIn = normalizers.normalizeCoordinateTuple(bcpIn) if bcpOut is None: bcpOut = (0, 0) bcpOut = normalizers.normalizeCoordinateTuple(bcpOut) self._appendBPoint(type, anchor, bcpIn=bcpIn, bcpOut=bcpOut) def _appendBPoint(self, type, anchor, bcpIn=None, bcpOut=None, **kwargs): """ Subclasses may override this method. """ self.insertBPoint( len(self.bPoints), type, anchor, bcpIn=bcpIn, bcpOut=bcpOut ) def insertBPoint(self, index, type=None, anchor=None, bcpIn=None, bcpOut=None, bPoint=None): """ Insert a bPoint at index in the contour. """ if bPoint is not None: if type is None: type = bPoint.type if anchor is None: anchor = bPoint.anchor if bcpIn is None: bcpIn = bPoint.bcpIn if bcpOut is None: bcpOut = bPoint.bcpOut index = normalizers.normalizeIndex(index) type = normalizers.normalizeBPointType(type) anchor = normalizers.normalizeCoordinateTuple(anchor) if bcpIn is None: bcpIn = (0, 0) bcpIn = normalizers.normalizeCoordinateTuple(bcpIn) if bcpOut is None: bcpOut = (0, 0) bcpOut = normalizers.normalizeCoordinateTuple(bcpOut) self._insertBPoint(index=index, type=type, anchor=anchor, bcpIn=bcpIn, bcpOut=bcpOut) def _insertBPoint(self, index, type, anchor, bcpIn, bcpOut, **kwargs): """ Subclasses may override this method. """ segments = self.segments # insert a curve point that we can work with nextSegment = segments[index] if nextSegment.type not in ("move", "line", "curve"): raise ValueError("Unknown segment type (%s) in contour." % nextSegment.type) if nextSegment.type == "move": prevSegment = segments[index - 1] prevOn = prevSegment.onCurve if bcpIn != (0, 0): new = self.appendSegment( "curve", [(prevOn.x, prevOn.y), absoluteBCPIn(anchor, bcpIn), anchor], smooth=False ) if type == "curve": new.smooth = True else: new = self.appendSegment( "line", [anchor], smooth=False ) # if the user wants an outgoing bcp, we must # add a curve ontop of the move if bcpOut != (0, 0): nextOn = nextSegment.onCurve self.appendSegment( "curve", [absoluteBCPOut(anchor, bcpOut), (nextOn.x, nextOn.y), (nextOn.x, nextOn.y)], smooth=False ) else: # handle the bcps if nextSegment.type != "curve": prevSegment = segments[index - 1] prevOn = prevSegment.onCurve prevOutX, prevOutY = (prevOn.x, prevOn.y) else: prevOut = nextSegment.offCurve[0] prevOutX, prevOutY = (prevOut.x, prevOut.y) newSegment = self.insertSegment( index, type="curve", points=[(prevOutX, prevOutY), anchor, anchor], smooth=False ) segments = self.segments p = index - 1 if p < 0: p = -1 prevSegment = segments[p] n = index + 1 if n >= len(segments): n = 0 nextSegment = segments[n] if nextSegment.type == "move": raise FontPartsError(("still working out curving at the " "end of a contour")) elif nextSegment.type == "qcurve": return # set the new incoming bcp newIn = newSegment.offCurve[1] nIX, nIY = absoluteBCPIn(anchor, bcpIn) newIn.x = nIX newIn.y = nIY # set the new outgoing bcp hasCurve = True if nextSegment.type != "curve": if bcpOut != (0, 0): nextSegment.type = "curve" hasCurve = True else: hasCurve = False if hasCurve: newOut = nextSegment.offCurve[0] nOX, nOY = absoluteBCPOut(anchor, bcpOut) newOut.x = nOX newOut.y = nOY # now check to see if we can convert the curve # segment to a line segment newAnchor = newSegment.onCurve newA = newSegment.offCurve[0] newB = newSegment.offCurve[1] prevAnchor = prevSegment.onCurve if ( (prevAnchor.x, prevAnchor.y) == (newA.x, newA.y) and (newAnchor.x, newAnchor.y) == (newB.x, newB.y) ): newSegment.type = "line" # the user wants a smooth segment if type == "curve": newSegment.smooth = True def removeBPoint(self, bPoint): """ Remove the bpoint from the contour. bpoint can be a point object or an index. """ if not isinstance(bPoint, int): bPoint = bPoint.index bPoint = normalizers.normalizeIndex(bPoint) if bPoint >= self._len__points(): raise ValueError("No bPoint located at index %d." % bPoint) self._removeBPoint(bPoint) def _removeBPoint(self, index, **kwargs): """ index will be a valid index. Subclasses may override this method. """ bPoint = self.bPoints[index] nextSegment = bPoint._nextSegment offCurves = nextSegment.offCurve if offCurves: offCurve = offCurves[0] self.removePoint(offCurve) segment = bPoint._segment offCurves = segment.offCurve if offCurves: offCurve = offCurves[-1] self.removePoint(offCurve) self.removePoint(bPoint._point) # ------ # Points # ------ def _setContourInPoint(self, point): if point.contour is None: point.contour = self points = dynamicProperty("points") def _get_points(self): """ Subclasses may override this method. """ return tuple([self._getitem__points(i) for i in range(self._len__points())]) def _len__points(self): return self._lenPoints() def _lenPoints(self, **kwargs): """ This must return an integer indicating the number of points in the contour. Subclasses must override this method. """ self.raiseNotImplementedError() def _getitem__points(self, index): index = normalizers.normalizeIndex(index) if index >= self._len__points(): raise ValueError("No point located at index %d." % index) point = self._getPoint(index) self._setContourInPoint(point) return point def _getPoint(self, index, **kwargs): """ This must return a wrapped point. index will be a valid index. Subclasses must override this method. """ self.raiseNotImplementedError() def _getPointIndex(self, point): for i, other in enumerate(self.points): if point == other: return i raise FontPartsError("The point could not be found.") def appendPoint(self, position=None, type="line", smooth=False, name=None, identifier=None, point=None): """ Append a point to the contour. """ if point is not None: if position is None: position = point.position type = point.type smooth = point.smooth if name is None: name = point.name if identifier is not None: identifier = point.identifier self.insertPoint( len(self.points), position=position, type=type, smooth=smooth, name=name, identifier=identifier ) def insertPoint(self, index, position=None, type="line", smooth=False, name=None, identifier=None, point=None): """ Insert a point into the contour. """ if point is not None: if position is None: position = point.position type = point.type smooth = point.smooth if name is None: name = point.name if identifier is not None: identifier = point.identifier index = normalizers.normalizeIndex(index) position = normalizers.normalizeCoordinateTuple(position) type = normalizers.normalizePointType(type) smooth = normalizers.normalizeBoolean(smooth) if name is not None: name = normalizers.normalizePointName(name) if identifier is not None: identifier = normalizers.normalizeIdentifier(identifier) self._insertPoint( index, position=position, type=type, smooth=smooth, name=name, identifier=identifier ) def _insertPoint(self, index, position, type="line", smooth=False, name=None, identifier=None, **kwargs): """ position will be a valid position (x, y). type will be a valid type. smooth will be a valid boolean. name will be a valid name or None. identifier will be a valid identifier or None. The identifier will not have been tested for uniqueness. Subclasses must override this method. """ self.raiseNotImplementedError() def removePoint(self, point, preserveCurve=False): """ Remove the point from the contour. point can be a point object or an index. If ``preserveCurve`` is set to ``True`` an attempt will be made to preserve the shape of the curve if the environment supports that functionality. """ if not isinstance(point, int): point = self.points.index(point) point = normalizers.normalizeIndex(point) if point >= self._len__points(): raise ValueError("No point located at index %d." % point) preserveCurve = normalizers.normalizeBoolean(preserveCurve) self._removePoint(point, preserveCurve) def _removePoint(self, index, preserveCurve, **kwargs): """ index will be a valid index. preserveCurve will be a boolean. Subclasses must override this method. """ self.raiseNotImplementedError() # --------- # Selection # --------- # segments selectedSegments = dynamicProperty( "base_selectedSegments", """ A list of segments selected in the contour. Getting selected segment objects: >>> for segment in contour.selectedSegments: ... segment.move((10, 20)) Setting selected segment objects: >>> contour.selectedSegments = someSegments Setting also supports segment indexes: >>> contour.selectedSegments = [0, 2] """ ) def _get_base_selectedSegments(self): selected = tuple([normalizers.normalizeSegment(segment) for segment in self._get_selectedSegments()]) return selected def _get_selectedSegments(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.segments) def _set_base_selectedSegments(self, value): normalized = [] for i in value: if isinstance(i, int): i = normalizers.normalizeSegmentIndex(i) else: i = normalizers.normalizeSegment(i) normalized.append(i) self._set_selectedSegments(normalized) def _set_selectedSegments(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.segments, value) # points selectedPoints = dynamicProperty( "base_selectedPoints", """ A list of points selected in the contour. Getting selected point objects: >>> for point in contour.selectedPoints: ... point.move((10, 20)) Setting selected point objects: >>> contour.selectedPoints = somePoints Setting also supports point indexes: >>> contour.selectedPoints = [0, 2] """ ) def _get_base_selectedPoints(self): selected = tuple([normalizers.normalizePoint(point) for point in self._get_selectedPoints()]) return selected def _get_selectedPoints(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.points) def _set_base_selectedPoints(self, value): normalized = [] for i in value: if isinstance(i, int): i = normalizers.normalizePointIndex(i) else: i = normalizers.normalizePoint(i) normalized.append(i) self._set_selectedPoints(normalized) def _set_selectedPoints(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.points, value) # bPoints selectedBPoints = dynamicProperty( "base_selectedBPoints", """ A list of bPoints selected in the contour. Getting selected bPoint objects: >>> for bPoint in contour.selectedBPoints: ... bPoint.move((10, 20)) Setting selected bPoint objects: >>> contour.selectedBPoints = someBPoints Setting also supports bPoint indexes: >>> contour.selectedBPoints = [0, 2] """ ) def _get_base_selectedBPoints(self): selected = tuple([normalizers.normalizeBPoint(bPoint) for bPoint in self._get_selectedBPoints()]) return selected def _get_selectedBPoints(self): """ Subclasses may override this method. """ return self._getSelectedSubObjects(self.bPoints) def _set_base_selectedBPoints(self, value): normalized = [] for i in value: if isinstance(i, int): i = normalizers.normalizeBPointIndex(i) else: i = normalizers.normalizeBPoint(i) normalized.append(i) self._set_selectedBPoints(normalized) def _set_selectedBPoints(self, value): """ Subclasses may override this method. """ return self._setSelectedSubObjects(self.bPoints, value)
class BaseLib(BaseDict, DeprecatedLib, RemovedLib): keyNormalizer = normalizers.normalizeLibKey valueNormalizer = normalizers.normalizeLibValue def _reprContents(self): contents = [] if self.glyph is not None: contents.append("in glyph") contents += self.glyph._reprContents() if self.font: contents.append("in font") contents += self.font._reprContents() return contents # ------- # Parents # ------- def getParent(self): """ This is a backwards compatibility method. """ glyph = self.glyph if glyph is not None: return glyph return self.font # Glyph _glyph = None glyph = dynamicProperty("glyph", "The lib's parent glyph.") def _get_glyph(self): if self._glyph is None: return None return self._glyph() def _set_glyph(self, glyph): assert self._font is None assert self._glyph is None or self._glyph() == glyph if glyph is not None: glyph = reference(glyph) self._glyph = glyph # Font _font = None font = dynamicProperty("font", "The lib's parent font.") def _get_font(self): if self._font is not None: return self._font() elif self._glyph is not None: return self.glyph.font return None def _set_font(self, font): assert self._font is None or self._font() == font assert self._glyph is None if font is not None: font = reference(font) self._font = font # Layer layer = dynamicProperty("layer", "The lib's parent layer.") def _get_layer(self): if self._glyph is None: return None return self.glyph.layer # --------------------- # RoboFab Compatibility # --------------------- def remove(self, key): del self[key] def asDict(self): d = {} for k, v in self.items(): d[k] = v return d