diff --git a/Lib/fontTools/pens/quartzPen.py b/Lib/fontTools/pens/quartzPen.py new file mode 100644 index 000000000..d35a993bb --- /dev/null +++ b/Lib/fontTools/pens/quartzPen.py @@ -0,0 +1,46 @@ +from fontTools.misc.py23 import * +from fontTools.pens.basePen import BasePen + +from Quartz.CoreGraphics import CGPathCreateMutable, CGPathMoveToPoint +from Quartz.CoreGraphics import CGPathAddLineToPoint, CGPathAddCurveToPoint +from Quartz.CoreGraphics import CGPathAddQuadCurveToPoint, CGPathCloseSubpath + + +__all__ = ["QuartzPen"] + + +class QuartzPen(BasePen): + + """A pen that creates a CGPath + + Parameters + - path: an optional CGPath to add to + - xform: an optional CGAffineTransform to apply to the path + """ + + def __init__(self, glyphSet, path=None, xform=None): + BasePen.__init__(self, glyphSet) + if path is None: + path = CGPathCreateMutable() + self.path = path + self.xform = xform + + def _moveTo(self, pt): + x, y = pt + CGPathMoveToPoint(self.path, self.xform, x, y) + + def _lineTo(self, pt): + x, y = pt + CGPathAddLineToPoint(self.path, self.xform, x, y) + + def _curveToOne(self, p1, p2, p3): + (x1, y1), (x2, y2), (x3, y3) = p1, p2, p3 + CGPathAddCurveToPoint(self.path, self.xform, x1, y1, x2, y2, x3, y3) + + def _qCurveToOne(self, p1, p2): + (x1, y1), (x2, y2) = p1, p2 + CGPathAddQuadCurveToPoint(self.path, self.xform, x1, y1, x2, y2) + + def _closePath(self): + CGPathCloseSubpath(self.path) + diff --git a/README.rst b/README.rst index 810f5fb1e..4bc7a3d5d 100644 --- a/README.rst +++ b/README.rst @@ -184,9 +184,9 @@ are required to unlock the extra features named "ufo", etc. *Extra:* ``pathops`` -- ``Lib/fontTools/pens/cocoaPen.py`` +- ``Lib/fontTools/pens/cocoaPen.py`` and ``Lib/fontTools/pens/quartzPen.py`` - Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires: + Pens for drawing glyphs with Cocoa ``NSBezierPath`` or ``CGPath`` require: * `PyObjC `__: the bridge between Python and the Objective-C runtime (macOS platform only). @@ -211,16 +211,17 @@ Acknowledgements In alphabetical order: Olivier Berten, Samyak Bhuta, Erik van Blokland, Petr van Blokland, -Jelle Bosma, Sascha Brawer, Tom Byrer, Frédéric Coiffier, Vincent -Connare, David Corbett, Simon Cozens, Dave Crossland, Simon Daniels, -Peter Dekkers, Behdad Esfahbod, Behnam Esfahbod, Hannes Famira, Sam -Fishman, Matt Fontaine, Yannis Haralambous, Greg Hitchcock, Jeremie -Hornus, Khaled Hosny, John Hudson, Denis Moyogo Jacquerye, Jack Jansen, -Tom Kacvinsky, Jens Kutilek, Antoine Leca, Werner Lemberg, Tal Leming, -Peter Lofting, Cosimo Lupo, Masaya Nakamura, Dave Opstad, Laurence -Penney, Roozbeh Pournader, Garret Rieger, Read Roberts, Guido van -Rossum, Just van Rossum, Andreas Seidel, Georg Seifert, Chris Simpkins, -Miguel Sousa, Adam Twardoch, Adrien Tétar, Vitaly Volkov, Paul Wise. +Jelle Bosma, Sascha Brawer, Tom Byrer, Antonio Cavedoni, Frédéric +Coiffier, Vincent Connare, David Corbett, Simon Cozens, Dave Crossland, +Simon Daniels, Peter Dekkers, Behdad Esfahbod, Behnam Esfahbod, Hannes +Famira, Sam Fishman, Matt Fontaine, Yannis Haralambous, Greg Hitchcock, +Jeremie Hornus, Khaled Hosny, John Hudson, Denis Moyogo Jacquerye, Jack +Jansen, Tom Kacvinsky, Jens Kutilek, Antoine Leca, Werner Lemberg, Tal +Leming, Peter Lofting, Cosimo Lupo, Masaya Nakamura, Dave Opstad, +Laurence Penney, Roozbeh Pournader, Garret Rieger, Read Roberts, Guido +van Rossum, Just van Rossum, Andreas Seidel, Georg Seifert, Chris +Simpkins, Miguel Sousa, Adam Twardoch, Adrien Tétar, Vitaly Volkov, +Paul Wise. Copyrights ~~~~~~~~~~ diff --git a/Tests/pens/cocoaPen_test.py b/Tests/pens/cocoaPen_test.py new file mode 100644 index 000000000..51795e121 --- /dev/null +++ b/Tests/pens/cocoaPen_test.py @@ -0,0 +1,59 @@ +from fontTools.misc.py23 import * +import unittest + +try: + from fontTools.pens.cocoaPen import CocoaPen + from AppKit import NSBezierPathElementMoveTo, NSBezierPathElementLineTo + from AppKit import NSBezierPathElementCurveTo, NSBezierPathElementClosePath + + PATH_ELEMENTS = { + # NSBezierPathElement key desc + NSBezierPathElementMoveTo: 'moveto', + NSBezierPathElementLineTo: 'lineto', + NSBezierPathElementCurveTo: 'curveto', + NSBezierPathElementClosePath: 'close', + } + + PYOBJC_AVAILABLE = True +except ImportError: + PYOBJC_AVAILABLE = False + + +def draw(pen): + pen.moveTo((50, 0)) + pen.lineTo((50, 500)) + pen.lineTo((200, 500)) + pen.curveTo((350, 500), (450, 400), (450, 250)) + pen.curveTo((450, 100), (350, 0), (200, 0)) + pen.closePath() + + +def cocoaPathToString(path): + num_elements = path.elementCount() + output = [] + for i in range(num_elements - 1): + elem_type, elem_points = path.elementAtIndex_associatedPoints_(i) + elem_type = PATH_ELEMENTS[elem_type] + path_points = " ".join([f"{p.x} {p.y}" for p in elem_points]) + output.append(f"{elem_type} {path_points}") + return " ".join(output) + + +@unittest.skipUnless(PYOBJC_AVAILABLE, "pyobjc not installed") +class CocoaPenTest(unittest.TestCase): + def test_draw(self): + pen = CocoaPen(None) + draw(pen) + self.assertEqual( + "moveto 50.0 0.0 lineto 50.0 500.0 lineto 200.0 500.0 curveto 350.0 500.0 450.0 400.0 450.0 250.0 curveto 450.0 100.0 350.0 0.0 200.0 0.0 close ", + cocoaPathToString(pen.path) + ) + + def test_empty(self): + pen = CocoaPen(None) + self.assertEqual("", cocoaPathToString(pen.path)) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/pens/quartzPen_test.py b/Tests/pens/quartzPen_test.py new file mode 100644 index 000000000..12fbd2921 --- /dev/null +++ b/Tests/pens/quartzPen_test.py @@ -0,0 +1,79 @@ +from fontTools.misc.py23 import * +import unittest + +try: + from fontTools.pens.quartzPen import QuartzPen + + from Quartz.CoreGraphics import CGPathApply + from Quartz.CoreGraphics import kCGPathElementMoveToPoint + from Quartz.CoreGraphics import kCGPathElementAddLineToPoint + from Quartz.CoreGraphics import kCGPathElementAddQuadCurveToPoint + from Quartz.CoreGraphics import kCGPathElementAddCurveToPoint + from Quartz.CoreGraphics import kCGPathElementCloseSubpath + + PATH_ELEMENTS = { + # CG constant key desc num_points + kCGPathElementMoveToPoint: ('moveto', 1), + kCGPathElementAddLineToPoint: ('lineto', 1), + kCGPathElementAddCurveToPoint: ('curveto', 3), + kCGPathElementAddQuadCurveToPoint: ('qcurveto', 2), + kCGPathElementCloseSubpath: ('close', 0), + } + + PYOBJC_AVAILABLE = True +except ImportError: + PYOBJC_AVAILABLE = False + + +def draw(pen): + pen.moveTo((50, 0)) + pen.lineTo((50, 500)) + pen.lineTo((200, 500)) + pen.curveTo((350, 500), (450, 400), (450, 250)) + pen.curveTo((450, 100), (350, 0), (200, 0)) + pen.closePath() + + +def quartzPathApplier(elements, element): + num_points = 0 + elem_type = None + if element.type in PATH_ELEMENTS: + num_points = PATH_ELEMENTS[element.type][1] + elem_type = PATH_ELEMENTS[element.type][0] + elements.append((elem_type, element.points.as_tuple(num_points))) + + +def quartzPathElements(path): + elements = [] + CGPathApply(path, elements, quartzPathApplier) + return elements + + +def quartzPathToString(path): + elements = quartzPathElements(path) + output = [] + for element in elements: + elem_type, elem_points = element + path_points = " ".join([f"{p.x} {p.y}" for p in elem_points]) + output.append(f"{elem_type} {path_points}") + return " ".join(output) + + +@unittest.skipUnless(PYOBJC_AVAILABLE, "pyobjc not installed") +class QuartzPenTest(unittest.TestCase): + def test_draw(self): + pen = QuartzPen(None) + draw(pen) + self.assertEqual( + "moveto 50.0 0.0 lineto 50.0 500.0 lineto 200.0 500.0 curveto 350.0 500.0 450.0 400.0 450.0 250.0 curveto 450.0 100.0 350.0 0.0 200.0 0.0 close ", + quartzPathToString(pen.path) + ) + + def test_empty(self): + pen = QuartzPen(None) + self.assertEqual("", quartzPathToString(pen.path)) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main())