def draw(layer, instance, pen): pen = PointToSegmentPen(pen) for path in layer.paths: nodes = list(path.nodes) pen.beginPath() if nodes: if not path.closed: node = nodes.pop(0) assert node.type == "line", "Open path starts with off-curve points" pen.addPoint(tuple(node.position), segmentType="move") else: # In Glyphs.app, the starting node of a closed contour is always # stored at the end of the nodes list. nodes.insert(0, nodes.pop()) for node in nodes: node_type = node.type if node_type not in ["line", "curve", "qcurve"]: node_type = None pen.addPoint(tuple(node.position), segmentType=node_type, smooth=node.smooth) pen.endPath(); for component in layer.components: componentLayer = getLayer(component.component, instance) transform = component.transform.value componentPen = pen.pen if transform != DEFAULT_TRANSFORM: componentPen = TransformPen(pen.pen, transform) xx, xy, yx, yy = transform[:4] if xx * yy - xy * yx < 0: componentPen = ReverseContourPen(componentPen) draw(componentLayer, instance, componentPen) return pen.pen
def test_quad_onlyOffCurvePoints(self): pen = _TestSegmentPen() ppen = PointToSegmentPen(pen) ppen.beginPath() ppen.addPoint((10, 10)) ppen.addPoint((10, 40)) ppen.addPoint((40, 40)) ppen.endPath() self.assertEqual("10 10 10 40 40 40 None qcurveto closepath", repr(pen))
def test_closed(self): pen = _TestSegmentPen() ppen = PointToSegmentPen(pen) ppen.beginPath() ppen.addPoint((10, 10), "line") ppen.addPoint((10, 20), "line") ppen.addPoint((20, 20), "line") ppen.endPath() self.assertEqual("10 10 moveto 10 20 lineto 20 20 lineto closepath", repr(pen))
def test_closed_outputImpliedClosingLine(self): tpen = _TestSegmentPen() ppen = PointToSegmentPen(tpen, outputImpliedClosingLine=True) ppen.beginPath() ppen.addPoint((10, 10), "line") ppen.addPoint((10, 20), "line") ppen.addPoint((20, 20), "line") ppen.endPath() self.assertEqual( "10 10 moveto " "10 20 lineto " "20 20 lineto " "10 10 lineto " # explicit closing line "closepath", repr(tpen))
def draw(self, pen): """ Draw the component with **pen**. """ from fontTools.pens.pointPen import PointToSegmentPen pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen)
def test_roundTrip2(self): tpen = _TestPointPen() ppen = PointToSegmentPen(SegmentToPointPen(tpen)) ppen.beginPath() ppen.addPoint((0, 651), segmentType="line") ppen.addPoint((0, 101), segmentType="line") ppen.addPoint((0, 101), segmentType="line") ppen.addPoint((0, 651), segmentType="line") ppen.endPath() self.assertEqual( "beginPath() " "addPoint((0, 651), segmentType='line') " "addPoint((0, 101), segmentType='line') " "addPoint((0, 101), segmentType='line') " "addPoint((0, 651), segmentType='line') " "endPath()", repr(tpen))
def test_roundTrip1(self): spen = _TestSegmentPen() pen = SegmentToPointPen(PointToSegmentPen(spen)) pen.moveTo((10, 10)) pen.lineTo((10, 20)) pen.lineTo((20, 20)) pen.closePath() self.assertEqual("10 10 moveto 10 20 lineto 20 20 lineto closepath", repr(spen))
def test_roundTrip2(self): spen = _TestSegmentPen() pen = SegmentToPointPen(PointToSegmentPen(spen)) pen.qCurveTo((10, 20), (20, 20), (20, 10), (10, 10), None) pen.closePath() pen.addComponent('base', [1, 0, 0, 1, 0, 0]) self.assertEqual( "10 20 20 20 20 10 10 10 None qcurveto closepath " "'base' [1, 0, 0, 1, 0, 0] addcomponent", repr(spen))
def test_draw_vs_drawpoints(self): font = TTFont(sfntVersion="\x00\x01\x00\x00") font.importXML(GLYF_TTX) glyfTable = font['glyf'] pen1 = RecordingPen() pen2 = RecordingPen() glyfTable["glyph00003"].draw(pen1, glyfTable) glyfTable["glyph00003"].drawPoints(PointToSegmentPen(pen2), glyfTable) self.assertEqual(pen1.value, pen2.value)
def test_pen_recording_equivalent(datadir): font = classes.GSFont(str(datadir.join("PenTest.glyphs"))) for glyph in font.glyphs: for layer in glyph.layers: rpen1 = fontTools.pens.recordingPen.RecordingPen() rpen2 = fontTools.pens.recordingPen.RecordingPen() layer.draw(rpen1) layer.drawPoints(PointToSegmentPen(rpen2)) assert rpen1.value == rpen2.value
def test_closed_line_overlapping_start_end_points(self): # Test case from https://github.com/googlefonts/fontmake/issues/572. tpen = _TestSegmentPen() ppen = PointToSegmentPen(tpen, outputImpliedClosingLine=False) # The last oncurve point on this closed contour is a "line" segment and has # same coordinates as the starting point. ppen.beginPath() ppen.addPoint((0, 651), segmentType="line") ppen.addPoint((0, 101), segmentType="line") ppen.addPoint((0, 101), segmentType="line") ppen.addPoint((0, 651), segmentType="line") ppen.endPath() # Check that we always output an explicit 'lineTo' segment at the end, # regardless of the value of 'outputImpliedClosingLine', to disambiguate # the duplicate point from the implied closing line. self.assertEqual( "0 651 moveto " "0 101 lineto " "0 101 lineto " "0 651 lineto " "0 651 lineto " "closepath", repr(tpen))
def _get_segments(glyph): """Get a glyph's segments as extracted by GetSegmentsPen.""" pen = GetSegmentsPen() # glyph.draw(pen) # We can't simply draw the glyph with the pen, but we must initialize the # PointToSegmentPen explicitly with outputImpliedClosingLine=True. # By default PointToSegmentPen does not outputImpliedClosingLine -- unless # last and first point on closed contour are duplicated. Because we are # converting multiple glyphs at the same time, we want to make sure # this function returns the same number of segments, whether or not # the last and first point overlap. # https://github.com/googlefonts/fontmake/issues/572 # https://github.com/fonttools/fonttools/pull/1720 pointPen = PointToSegmentPen(pen, outputImpliedClosingLine=True) glyph.drawPoints(pointPen) return pen.segments
def draw(self, pen): ppen = PointToSegmentPen(pen) startIndex = 0 points = self.getPoints() for endIndex in self.contours: lastTag = self.tags[endIndex] endIndex += 1 contourTags = self.tags[startIndex:endIndex] contourPoints = points[startIndex:endIndex] ppen.beginPath() for tag, (x, y) in zip(contourTags, contourPoints): if tag == FT_CURVE_TAG_ON: segmentType = segmentTypes[lastTag] else: segmentType = None ppen.addPoint((x, y), segmentType=segmentType) lastTag = tag ppen.endPath() startIndex = endIndex
def find_shape_diffs(self): """Report differences in glyph shapes, using BooleanOperations.""" self.build_names() area_pen = GlyphAreaPen(None) pen = PointToSegmentPen(area_pen) mismatched = {} for name in self.names: glyph_a = Glyph() glyph_b = Glyph() self.glyph_set_a[name].draw(Qu2CuPen(glyph_a.getPen(), self.glyph_set_a)) self.glyph_set_b[name].draw(Qu2CuPen(glyph_b.getPen(), self.glyph_set_b)) booleanOperations.xor(list(glyph_a), list(glyph_b), pen) area = abs(area_pen.pop()) if area: mismatched[name] = area stats = self.stats["compared"] for name, area in mismatched.items(): stats.append((area, name, self.basepath))
def getReversePen(self): adapterPen = PointToSegmentPen(self.otherPen) reversePen = ReverseContourPointPen(adapterPen) return SegmentToPointPen(reversePen)
def draw(self, pen): """Draws the contours with **pen**.""" pointPen = PointToSegmentPen(pen) self.drawToPointPen(pointPen)
class BaseBezierPath(BasePen): """Base class with same interface as DrawBot Bézier path.""" contourClass = BaseBezierContour def __init__(self, path=None, glyphSet=None): """ >>> path = BaseBezierPath() >>> path <BaseBezierPath> """ self._contours = [] #super().__init__(glyphSet) BasePen.__init__(self, glyphSet) def __repr__(self): return "<BaseBezierPath>" def _points(self, onCurve=True, offCurve=True): """Internal points representation, corresponding to the DrawBot Bézier path.""" points = [] if not onCurve and not offCurve: return points for contour in self._contours: for segment in contour: pts = segment.points if not onCurve: pts = pts[:-1] elif not offCurve: pts = pts[-1:] points.extend([(p.x, p.y) for p in pts]) return tuple(points) def _get_points(self): return self._points() points = property(_get_points, doc="Return a list of all points.") def _get_onCurvePoints(self): return self._points(offCurve=False) onCurvePoints = property(_get_onCurvePoints, doc="Return a list of all on curve points.") def _get_offCurvePoints(self): return self._points(onCurve=False) offCurvePoints = property(_get_offCurvePoints, doc="Return a list of all off curve points.") def _get_contours(self): """Internal contour representation, corresponding to the DrawBot Bézier path.""" contours = [] for contour in self._contours: for segment in contour: if segment.instruction == MOVETO: contours.append(BaseBezierContour()) if segment.instruction == CLOSEPATH: contours[-1].open = False if segment.points: contours[-1].append([(p.x, p.y) for p in segment.points]) if len(contours) >= 2 and len(contours[-1]) == 1 and contours[-1][0] == contours[-2][0]: contours.pop() return tuple(contours) contours = property(_get_contours, doc="Return a list of contours with all point coordinates sorted in segments. A contour object has an `open` attribute.") def __len__(self): return len(self.contours) def __getitem__(self, index): return self.contours[index] def __iter__(self): contours = self.contours count = len(contours) index = 0 while index < count: contour = contours[index] yield contour index += 1 def addSegment(self, instruction, points): """Adds a new segment to the current contour.""" segment = BaseBezierSegment(instruction, points) contour = self.getContour() contour.append(segment) def getContour(self): """Gets the current contour if it exists, else make one.""" if len(self._contours) == 0: contour = self.contourClass() self._contours.append(contour) else: contour = self._contours[-1] return contour def getPoint(self, p, onCurve=True): x, y = p point = BaseBezierPoint(x, y, onCurve=onCurve) return point # FontTools PointToSegmentPen routines. def beginPath(self, identifier=None): """Begin using the path as a point pen and start a new subpath.""" self._pointToSegmentPen = PointToSegmentPen(self) self._pointToSegmentPen.beginPath() def addPoint(self, point, segmentType=None, smooth=False, name=None, identifier=None, **kwargs): """Use the path as a point pen and add a point to the current subpath. `beginPath` must have been called prior to adding points with `addPoint` calls.""" if not hasattr(self, "_pointToSegmentPen"): msg = "path.beginPath() must be called before the path can be used as a point pen." raise PageBotError(msg) self._pointToSegmentPen.addPoint( point, segmentType=segmentType, smooth=smooth, name=name, identifier=identifier, **kwargs ) def endPath(self): """Ends the current subpath. Calling this method has two distinct meanings depending on the context: When the Bézier path is used as a segment pen (using `moveTo`, `lineTo`, etc.), the current subpath will be finished as an open contour. When the Bézier path is used as a point pen (using `beginPath`, `addPoint` and `endPath`), the path will process all the points added with `addPoint`, finishing the current subpath.""" if hasattr(self, "_pointToSegmentPen"): pointToSegmentPen = self._pointToSegmentPen del self._pointToSegmentPen pointToSegmentPen.endPath() else: msg = "path.beginPath() must be called before the path can be used as a point pen." raise PageBotError(msg) def draw(self, pen): """Draws the contours with **pen**.""" pointPen = PointToSegmentPen(pen) self.drawToPointPen(pointPen) def bounds(self): """Returns the bounding box of the path.""" pen = BoundsPen(self) self.draw(pen) return pen.bounds def drawToPointPen(self, pointPen): """Draws the Bézier path into a point pen.""" contours = self.contours for contour in contours: contour.drawToPointPen(pointPen) def drawToPen(self, pen): """Draws the Bézier path into a pen.""" contours = self.contours for contour in contours: contour.drawToPen(pen) # Drawing. def addComponent(self, glyphName, transformation): """ Adds a sub glyph. The 'transformation' argument must be a 6-tuple containing an affine transformation, or a Transform object from the fontTools.misc.transform module. More precisely: it should be a sequence containing 6 numbers. A `glyphSet` is required during initialization of the BezierPath object. """ raise NotImplementedError # Shapes. def rect(self, x, y, w, h): """Adds a rectangle at position `x`, `y` with a size of `w`, `h`.""" x1 = x + w y1 = y + h p0 = (x, y) p1 = (x1, y) p2 = (x1, y1) p3 = (x, y1) self.moveTo(p0) self.lineTo(p1) self.lineTo(p2) self.lineTo(p3) self.closePath() def oval(self, x, y, w, h): """Adds an oval at position `x`, `y` with a size of `w`, `h`""" # Control point offsets. kappa = .5522848 offsetX = (w / 2) * kappa offsetY = (h / 2) * kappa # Middle and other extreme points. x0 = x + (w / 2) y0 = y + (h / 2) x1 = x + w y1 = y + h self.moveTo((x0, y0)) cp1 = (x, y0 - offsetY) cp2 = (x0 - offsetX, y) p = (x1, y0) self.curveTo(cp1, cp2, p) cp1 = (x0 + offsetX, y) cp2 = (x1, y0 - offsetY) p = (x1, y0) self.curveTo(cp1, cp2, p) cp1 = (x1, y0 + offsetY) cp2 = (x0 + offsetX, y1) p = (x0, y1) self.curveTo(cp1, cp2, p) cp1 = (x0 - offsetX, y1) cp2 = (x, y0 + offsetY) p = (x, y0) self.curveTo(cp1, cp2, p) def line(self, point1, point2): """Adds a line between two given points.""" self.moveTo(point1) self.lineTo(point2) def polygon(self, *points, **kwargs): """Draws a polygon with `n` points. Optionally a `close` argument can be provided to open or close the path. By default a `polygon` is a closed path.""" self.moveTo(points[0]) for point in points[1:]: self.lineTo(point) # TODO: optionally close. def arc(self, center, radius, startAngle, endAngle, clockwise): """Arc with `center` and a given `radius`, from `startAngle` to `endAngle`, going clockwise if `clockwise` is True and counter clockwise if `clockwise` is False.""" raise NotImplementedError def arcTo(self, point1, point2, radius): """Arc defined by a circle inscribed inside the angle specified by three points: the current point, `point1`, and `point2`. The arc is drawn between the two points of the circle that are tangent to the two legs of the angle.""" raise NotImplementedError # Text. def text(self, txt, offset=None, font=_FALLBACKFONT, fontSize=10, align=None): """Draws a `txt` with a `font` and `fontSize` at an `offset` in the Bézier path. If a font path is given the font will be installed and used directly. * Optionally an alignment can be set. Possible `align` values are: `"left"`, `"center"` and `"right"`. * The default alignment is `left`. * Optionally `txt` can be a `FormattedString`. """ raise NotImplementedError def textBox(self, txt, box, font=None, fontSize=10, align=None, hyphenation=None): """ Draws a `txt` with a `font` and `fontSize` in a `box` in the Bézier path. If a font path is given the font will be installed and used directly. Optionally an alignment can be set. * Possible `align` values are: `"left"`, `"center"` and `"right"`. * The default alignment is `left`. * Optionally `hyphenation` can be provided. * Optionally `txt` can be a `FormattedString`. * Optionally `box` can be a `BezierPath`. """ raise NotImplementedError # Path operations. # These are specific for a DrawBot path, dropping from interface. #def getNSBezierPath(self): def getBezierPath(self): """Returns the equivalent of an NSBezierPath.""" #def setNSBezierPath(self, path): def setBezierPath(self, path): """Sets the equivalent of an NSBezierPath.""" def traceImage(self, path, threshold=.2, blur=None, invert=False, turd=2, tolerance=0.2, offset=None): """Converts a given image to a vector outline. Optionally some tracing options can be provided: * `threshold`: the threshold used to bitmap an image * `blur`: the image can be blurred * `invert`: invert to the image * `turd`: the size of small turd that can be ignored * `tolerance`: the precision tolerance of the vector outline * `offset`: add the traced vector outline with an offset to the BezierPath """ # TODO: use potrace, see drawBot.context.tools.TraceImage. def pointInside(self, xy): """Checks if a point `x`, `y` is inside a path.""" raise NotImplementedError def controlPointBounds(self): """Returns the bounding box of the path including the offcurve points.""" raise NotImplementedError def optimizePath(self): raise NotImplementedError def copy(self): """Copy the Bézier path.""" raise NotImplementedError def reverse(self): """Reverse the path direction.""" raise NotImplementedError def appendPath(self, otherPath): """Append a path.""" raise NotImplementedError # transformations # NOTE: currently handled within context. def translate(self, x=0, y=0): """Translates the path with a given offset.""" self.transform((1, 0, 0, 1, x, y)) def rotate(self, angle, center=(0, 0)): """Rotates the path around the `center` point (which is the origin by default) with a given angle in degrees.""" angle = math.radians(angle) c = math.cos(angle) s = math.sin(angle) self.transform((c, s, -s, c, 0, 0), center) def scale(self, x=1, y=None, center=(0, 0)): """Scales the path with a given `x` (horizontal scale) and `y` (vertical scale). If only 1 argument is provided a proportional scale is applied. The center of scaling can optionally be set via the `center` keyword argument. By default this is the origin.""" if y is None: y = x self.transform((x, 0, 0, y, 0, 0), center) def skew(self, angle1, angle2=0, center=(0, 0)): """Skews the path with given `angle1` and `angle2`. If only one argument is provided a proportional skew is applied. The center of skewing can optionally be set via the `center` keyword argument. By default this is the origin.""" angle1 = math.radians(angle1) angle2 = math.radians(angle2) self.transform((1, math.tan(angle2), math.tan(angle1), 1, 0, 0), center) def transform(self, transformMatrix, center=(0, 0)): """Transforms a path with a transform matrix (xy, xx, yy, yx, x, y).""" # Boolean operations. def _contoursForBooleanOperations(self): # contours are temporary objects # redirect drawToPointPen to drawPoints contours = self.contours for contour in contours: contour.drawPoints = contour.drawToPointPen if contour.open: raise PageBotError("open contours are not supported during boolean operations") return contours def union(self, other): """Returns the union between two Bézier paths.""" assert isinstance(other, self.__class__) contours = self._contoursForBooleanOperations() + other._contoursForBooleanOperations() result = self.__class__() booleanOperations.union(contours, result) return result def removeOverlap(self): """Remove all overlaps in a Bézier path.""" contours = self._contoursForBooleanOperations() result = self.__class__() booleanOperations.union(contours, result) # TODO: #self.setNSBezierPath(result.getNSBezierPath()) #self.setBezierPath(result.getBezierPath()) return self def difference(self, other): """Returns the difference between two Bézier paths. """ subjectContours = self._contoursForBooleanOperations() clipContours = other._contoursForBooleanOperations() result = self.__class__() booleanOperations.difference(subjectContours, clipContours, result) return result def intersection(self, other): """Returns the intersection between two Bézier paths.""" assert isinstance(other, self.__class__) subjectContours = self._contoursForBooleanOperations() clipContours = other._contoursForBooleanOperations() result = self.__class__() booleanOperations.intersection(subjectContours, clipContours, result) return result def xor(self, other): """Returns the xor between two Bézier paths.""" assert isinstance(other, self.__class__) subjectContours = self._contoursForBooleanOperations() clipContours = other._contoursForBooleanOperations() result = self.__class__() booleanOperations.xor(subjectContours, clipContours, result) return result def intersectionPoints(self, other=None): """ Returns a list of intersection points as `x`, `y` tuples. Optionaly provides another path object to find intersection points. """ contours = self._contoursForBooleanOperations() if other is not None: assert isinstance(other, self.__class__) contours += other._contoursForBooleanOperations() return booleanOperations.getIntersections(contours) def expandStroke(self, width, lineCap="round", lineJoin="round", miterLimit=10): """Returns a new Bézier path with an expanded stroke around the original path, with a given `width`. Note: the new path will not contain the original path. The following optional arguments are available with respect to line caps and joins: * `lineCap`: Possible values are `"butt"`, `"square"` or `"round"` * `lineJoin`: Possible values are `"bevel"`, `"miter"` or `"round"` * `miterLimit`: The miter limit to use for `"miter"` lineJoin option """ # TODO: find cross-platform alternative to Quartz.CGPathCreateCopyByStrokingPath. # def __add__(self, otherPath): #new = self.copy() #new.appendPath(otherPath) #return new pass def __iadd__(self, other): self.appendPath(other) return self def __mod__(self, other): return self.difference(other) __rmod__ = __mod__ def __imod__(self, other): #result = self.difference(other) #self.setBezierPath(result.getBezierPath()) #return self pass def __or__(self, other): return self.union(other) __ror__ = __or__ def __ior__(self, other): #result = self.union(other) #self.setBezierPath(result.getBezierPath()) #return self pass def __and__(self, other): return self.intersection(other) __rand__ = __and__ def __iand__(self, other): #result = self.intersection(other) #self.setBezierPath(result.getBezierPath()) #return self pass def __xor__(self, other): return self.xor(other) __rxor__ = __xor__ def __ixor__(self, other): #result = self.xor(other) #self.setBezierPath(result.getBezierPath()) #return self pass
def test_roundTrip1(self): tpen = _TestPointPen() ppen = PointToSegmentPen(SegmentToPointPen(tpen)) ppen.beginPath() ppen.addPoint((10, 10), "line") ppen.addPoint((10, 20)) ppen.addPoint((20, 20)) ppen.addPoint((20, 40), "curve") ppen.endPath() self.assertEqual( "beginPath() addPoint((10, 10), segmentType='line') addPoint((10, 20)) " "addPoint((20, 20)) addPoint((20, 40), segmentType='curve') endPath()", repr(tpen))
class BezierPath(BasePen): def __init__(self, path=None, glyphSet=None): super().__init__(glyphSet) if path is None: path = skia.Path() self.path = path def _moveTo(self, pt): self.path.moveTo(*pt) def _lineTo(self, pt): self.path.lineTo(*pt) def _curveToOne(self, pt1, pt2, pt3): x1, y1 = pt1 x2, y2 = pt2 x3, y3 = pt3 self.path.cubicTo(x1, y1, x2, y2, x3, y3) def _qCurveToOne(self, pt1, pt2): x1, y1 = pt1 x2, y2 = pt2 self.path.quadTo(x1, y1, x2, y2) def _closePath(self): self.path.close() def beginPath(self, identifier=None): self._pointToSegmentPen = PointToSegmentPen(self) self._pointToSegmentPen.beginPath() def addPoint(self, point, segmentType=None, smooth=False, name=None, identifier=None, **kwargs): if not hasattr(self, "_pointToSegmentPen"): raise AttributeError( "path.beginPath() must be called before the path can be used as a point pen" ) self._pointToSegmentPen.addPoint(point, segmentType=segmentType, smooth=smooth, name=name, identifier=identifier, **kwargs) def endPath(self): if hasattr(self, "_pointToSegmentPen"): # We are drawing as a point pen pointToSegmentPen = self._pointToSegmentPen del self._pointToSegmentPen pointToSegmentPen.endPath() def arc(self, center, radius, startAngle, endAngle, clockwise): cx, cy = center diameter = radius * 2 rect = (cx - radius, cy - radius, diameter, diameter) sweepAngle = (endAngle - startAngle) % 360 if clockwise: sweepAngle -= 360 self.path.arcTo(rect, startAngle, sweepAngle, False) def arcTo(self, point1, point2, radius): self.path.arcTo(point1, point2, radius) def rect(self, x, y, w, h): self.path.addRect((x, y, w, h)) def oval(self, x, y, w, h): self.path.addOval((x, y, w, h)) def line(self, pt1, pt2): points = [(x, y) for x, y in [pt1, pt2]] self.path.addPoly(points, False) def polygon(self, firstPoint, *points, close=True): points = [(x, y) for x, y in (firstPoint, ) + points] self.path.addPoly(points, close) def pointInside(self, point): x, y = point return self.path.contains(x, y) def bounds(self): if self.path.countVerbs() == 0: return None return tuple(self.path.computeTightBounds()) def controlPointBounds(self): if self.path.countVerbs() == 0: return None return tuple(self.path.getBounds()) def reverse(self): path = skia.Path() path.reverseAddPath(self.path) self.path = path def appendPath(self, other): self.path.addPath(other.path) def copy(self): path = skia.Path(self.path) return BezierPath(path=path) def translate(self, x, y): self.path.offset(x, y) def scale(self, x, y=None, center=(0, 0)): if y is None: y = x self.transform((x, 0, 0, y, 0, 0), center=center) def rotate(self, angle, center=(0, 0)): t = Transform() t = t.rotate(math.radians(angle)) self.transform(t, center=center) def skew(self, x, y=0, center=(0, 0)): t = Transform() t = t.skew(math.radians(x), math.radians(y)) self.transform(t, center=center) def transform(self, transform, center=(0, 0)): cx, cy = center t = Transform() t = t.translate(cx, cy) t = t.transform(transform) t = t.translate(-cx, -cy) matrix = skia.Matrix() matrix.setAffine(t) self.path.transform(matrix) def drawToPen(self, pen): it = skia.Path.Iter(self.path, False) needEndPath = False for verb, points in it: penVerb, startIndex, numPoints = _pathVerbsToPenMethod.get( verb, (None, None, None)) if penVerb is None: continue assert len(points) == numPoints, (verb, numPoints, len(points)) if penVerb == "conicTo": # We should only call _convertConicToCubicDirty() # if it.conicWeight() == sqrt(2)/2, but skia-python doesn't # give the correct value. # https://github.com/kyamagu/skia-python/issues/116 # if abs(it.conicWeight() - 0.707...) > 1e-10: # logging.warning("unsupported conic form (weight != sqrt(2)/2): conic to cubic conversion will be bad") # TODO: we should fall back to skia.Path.ConvertConicToQuads(), # but that call is currently also not working. pen.curveTo(*_convertConicToCubicDirty(*points)) elif penVerb == "closePath": needEndPath = False pen.closePath() else: if penVerb == "moveTo": if needEndPath: pen.endPath() needEndPath = True pointArgs = ((x, y) for x, y in points[startIndex:]) getattr(pen, penVerb)(*pointArgs) if needEndPath: pen.endPath() def drawToPointPen(self, pen): self.drawToPen(SegmentToPointPen(pen)) def text(self, txt, offset=None, font=None, fontSize=10, align=None): if not txt: return textStyle = TextStyle(font=font, fontSize=fontSize) glyphsInfo = textStyle.shape(txt) textStyle.alignGlyphPositions(glyphsInfo, align) gids = sorted(set(glyphsInfo.gids)) paths = [textStyle.skFont.getPath(gid) for gid in gids] for path in paths: path.transform(FLIP_MATRIX) paths = dict(zip(gids, paths)) x, y = (0, 0) if offset is None else offset for gid, pos in zip(glyphsInfo.gids, glyphsInfo.positions): path = paths[gid] self.path.addPath(path, pos[0] + x, pos[1] + y) def _doPathOp(self, other, operator): from pathops import Path, op path1 = Path() path2 = Path() self.drawToPen(path1.getPen()) other.drawToPen(path2.getPen()) result = op( path1, path2, operator, fix_winding=True, keep_starting_points=True, ) resultPath = BezierPath() result.draw(resultPath) return resultPath def union(self, other): from pathops import PathOp return self._doPathOp(other, PathOp.UNION) def intersection(self, other): from pathops import PathOp return self._doPathOp(other, PathOp.INTERSECTION) def difference(self, other): from pathops import PathOp return self._doPathOp(other, PathOp.DIFFERENCE) def xor(self, other): from pathops import PathOp return self._doPathOp(other, PathOp.XOR) def removeOverlap(self): from pathops import Path path = Path() self.drawToPen(path.getPen()) path.simplify( fix_winding=True, keep_starting_points=False, ) resultPath = BezierPath() path.draw(resultPath) self.path = resultPath.path __mod__ = difference def __imod__(self, other): result = self.difference(other) self.path = result.path return self __or__ = union def __ior__(self, other): result = self.union(other) self.path = result.path return self __and__ = intersection def __iand__(self, other): result = self.intersection(other) self.path = result.path return self __xor__ = xor def __ixor__(self, other): result = self.xor(other) self.path = result.path return self
def getPointPen(self): """Return a PointPen adapter that can 'draw' on this glyph.""" return PointToSegmentPen(self._pen)
def beginPath(self, identifier=None): self._pointToSegmentPen = PointToSegmentPen(self) self._pointToSegmentPen.beginPath()
def draw(self, pen): self.drawPoints(PointToSegmentPen(pen))
def beginPath(self, identifier=None): """Begin using the path as a point pen and start a new subpath.""" self._pointToSegmentPen = PointToSegmentPen(self) self._pointToSegmentPen.beginPath()
def draw(self, pen): """draw self using pen""" pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen)
def draw(self, pen, filterRedundantPoints=False): """draw self using pen""" from fontTools.pens.pointPen import PointToSegmentPen pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen, filterRedundantPoints=filterRedundantPoints)
def draw(self, pen: AbstractPen) -> None: """Draws contour into given pen.""" pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen)
def draw(self, pen): """Use another SegmentPen to replay the glyph's outline commands, indirectly through an adapter. """ pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen)
def draw(self, pen: AbstractPen) -> None: """Draws glyph into given pen.""" # TODO: Document pen interface more or link to somewhere. pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen)
def draw(self, pen): pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen)
def test_quad(self): pen = _TestSegmentPen() ppen = PointToSegmentPen(pen) ppen.beginPath(identifier='foo') ppen.addPoint((10, 10), "line") ppen.addPoint((10, 40)) ppen.addPoint((40, 40)) ppen.addPoint((10, 40), "qcurve") ppen.endPath() self.assertEqual("10 10 moveto 10 40 40 40 10 40 qcurveto closepath", repr(pen))