Source code for fontParts.base.contour

from fontParts.base.errors import FontPartsError
from fontParts.base.base import (
    BaseObject,
    TransformationMixin,
    InterpolationMixin,
    SelectionMixin,
    IdentifierMixin,
    dynamicProperty,
    reference
)
from fontParts.base import normalizers
from fontParts.base.compatibility import ContourCompatibilityReporter
from fontParts.base.bPoint import absoluteBCPIn, absoluteBCPOut
from fontParts.base.deprecated import DeprecatedContour, RemovedContour


[docs] 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)
[docs] def _get_index(self): """ Subclasses may override this method. """ glyph = self.glyph return glyph.contours.index(self)
[docs] 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 # ----
[docs] def draw(self, pen): """ Draw the contour's outline data to the given :ref:`type-pen`. >>> contour.draw(pen) """ self._draw(pen)
[docs] def _draw(self, pen, **kwargs): """ Subclasses may override this method. """ from fontTools.ufoLib.pointPen import PointToSegmentPen adapter = PointToSegmentPen(pen) self.drawPoints(adapter)
[docs] def drawPoints(self, pen): """ Draw the contour's outline data to the given :ref:`type-point-pen`. >>> contour.drawPoints(pointPen) """ self._drawPoints(pen)
[docs] 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 # ------------------
[docs] def autoStartSegment(self): """ Automatically calculate and set the first segment in this contour. The behavior of this may vary accross environments. """ self._autoStartSegment()
[docs] def _autoStartSegment(self, **kwargs): """ Subclasses may override this method. XXX port this from robofab """ self.raiseNotImplementedError()
[docs] def round(self): """ Round coordinates in all points to integers. """ self._round()
[docs] def _round(self, **kwargs): """ Subclasses may override this method. """ for point in self.points: point.round()
# -------------- # Transformation # --------------
[docs] 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)
[docs] def _get_clockwise(self): """ Subclasses must override this method. """ self.raiseNotImplementedError()
[docs] def _set_clockwise(self, value): """ Subclasses may override this method. """ if self.clockwise != value: self.reverse()
[docs] def reverse(self): """ Reverse the direction of the contour. """ self._reverseContour()
[docs] def _reverse(self, **kwargs): """ Subclasses may override this method. """ self.raiseNotImplementedError()
# ------------------------ # Point and Contour Inside # ------------------------
[docs] 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)
[docs] def _pointInside(self, point): """ Subclasses may override this method. """ from fontTools.pens.pointInsidePen import PointInsidePen pen = PointInsidePen(glyphSet=None, testPoint=point, evenOdd=False) self.draw(pen) return pen.getResult()
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
[docs] def _get_bounds(self): """ Subclasses may override this method. """ from fontTools.pens.boundsPen import BoundsPen pen = BoundsPen(self.layer) self.draw(pen) return pen.bounds
area = dynamicProperty("area", ("The area of the 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")
[docs] def _get_segments(self): """ Subclasses may override this method. """ points = list(self.points) if not points: return [] 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 and len(segments) > 1: segment = segments.pop(-1) segment.extend(segments[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
[docs] def __getitem__(self, index): return self.segments[index]
[docs] 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
[docs] def __len__(self): return self._len__segments()
[docs] def _len__segments(self, **kwargs): """ Subclasses may override this method. """ return len(self.segments)
[docs] 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)
[docs] 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)
[docs] 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)
[docs] def _insertSegment(self, index=None, type=None, points=None, smooth=False, **kwargs): """ Subclasses may override this method. """ onCurve = points[-1] offCurve = points[:-1] segments = self.segments addPointCount = 1 if self.open: index += 1 addPointCount = 0 ptCount = sum([len(segments[s].points) for s in range(index)]) + addPointCount self.insertPoint(ptCount, onCurve, type=type, smooth=smooth) for offCurvePoint in reversed(offCurve): self.insertPoint(ptCount, offCurvePoint, type="offcurve")
[docs] 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)
[docs] 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)
[docs] def setStartSegment(self, segment): """ Set the first segment on the contour. segment can be a segment object or an index. """ if self.open: raise FontPartsError("An open contour can not change the starting segment.") 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)
[docs] def _setStartSegment(self, segmentIndex, **kwargs): """ Subclasses may override this method. """ # get the previous segment and set # its on curve as the first point # in the contour. this matches the # iteration behavior of self.segments. segmentIndex -= 1 segments = self.segments segment = segments[segmentIndex] self.setStartPoint(segment.points[-1])
# ------- # 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)
[docs] 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)
[docs] 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 )
[docs] 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)
[docs] def _insertBPoint(self, index, type, anchor, bcpIn, bcpOut, **kwargs): """ Subclasses may override this method. """ # insert a simple line segment at the given anchor # look it up as a bPoint and change the bcpIn and bcpOut there # this avoids code duplication self._insertSegment(index=index, type="line", points=[anchor], smooth=False) bPoints = self.bPoints index += 1 if index >= len(bPoints): # its an append instead of an insert # so take the last bPoint index = -1 bPoint = bPoints[index] bPoint.bcpIn = bcpIn bPoint.bcpOut = bcpOut bPoint.type = type
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")
[docs] 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()
[docs] 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
[docs] 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.")
[docs] 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 )
[docs] 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 )
[docs] 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()
[docs] 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)
[docs] def _removePoint(self, index, preserveCurve, **kwargs): """ index will be a valid index. preserveCurve will be a boolean. Subclasses must override this method. """ self.raiseNotImplementedError()
def setStartPoint(self, point): """ Set the first point on the contour. point can be a point object or an index. """ if self.open: raise FontPartsError("An open contour can not change the starting point.") points = self.points if not isinstance(point, int): pointIndex = points.index(point) else: pointIndex = point if pointIndex == 0: return if pointIndex >= len(points): raise ValueError(("The contour does not contain a point at index %d" % pointIndex)) self._setStartPoint(pointIndex) def _setStartPoint(self, pointIndex, **kwargs): """ Subclasses may override this method. """ points = self.points points = points[pointIndex:] + points[:pointIndex] # Clear the points. for point in self.points: self.removePoint(point) # Add the points. for point in points: self.appendPoint( (point.x, point.y), type=point.type, smooth=point.smooth, name=point.name, identifier=point.identifier ) # --------- # 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)