| from typing import Callable |
| from fontTools.pens.basePen import BasePen |
| |
| |
| def pointToString(pt, ntos=str): |
| return " ".join(ntos(i) for i in pt) |
| |
| |
| class SVGPathPen(BasePen): |
| """Pen to draw SVG path d commands. |
| |
| Example:: |
| >>> pen = SVGPathPen(None) |
| >>> pen.moveTo((0, 0)) |
| >>> pen.lineTo((1, 1)) |
| >>> pen.curveTo((2, 2), (3, 3), (4, 4)) |
| >>> pen.closePath() |
| >>> pen.getCommands() |
| 'M0 0 1 1C2 2 3 3 4 4Z' |
| |
| Args: |
| glyphSet: a dictionary of drawable glyph objects keyed by name |
| used to resolve component references in composite glyphs. |
| ntos: a callable that takes a number and returns a string, to |
| customize how numbers are formatted (default: str). |
| |
| Note: |
| Fonts have a coordinate system where Y grows up, whereas in SVG, |
| Y grows down. As such, rendering path data from this pen in |
| SVG typically results in upside-down glyphs. You can fix this |
| by wrapping the data from this pen in an SVG group element with |
| transform, or wrap this pen in a transform pen. For example: |
| |
| spen = svgPathPen.SVGPathPen(glyphset) |
| pen= TransformPen(spen , (1, 0, 0, -1, 0, 0)) |
| glyphset[glyphname].draw(pen) |
| print(tpen.getCommands()) |
| """ |
| |
| def __init__(self, glyphSet, ntos: Callable[[float], str] = str): |
| BasePen.__init__(self, glyphSet) |
| self._commands = [] |
| self._lastCommand = None |
| self._lastX = None |
| self._lastY = None |
| self._ntos = ntos |
| |
| def _handleAnchor(self): |
| """ |
| >>> pen = SVGPathPen(None) |
| >>> pen.moveTo((0, 0)) |
| >>> pen.moveTo((10, 10)) |
| >>> pen._commands |
| ['M10 10'] |
| """ |
| if self._lastCommand == "M": |
| self._commands.pop(-1) |
| |
| def _moveTo(self, pt): |
| """ |
| >>> pen = SVGPathPen(None) |
| >>> pen.moveTo((0, 0)) |
| >>> pen._commands |
| ['M0 0'] |
| |
| >>> pen = SVGPathPen(None) |
| >>> pen.moveTo((10, 0)) |
| >>> pen._commands |
| ['M10 0'] |
| |
| >>> pen = SVGPathPen(None) |
| >>> pen.moveTo((0, 10)) |
| >>> pen._commands |
| ['M0 10'] |
| """ |
| self._handleAnchor() |
| t = "M%s" % (pointToString(pt, self._ntos)) |
| self._commands.append(t) |
| self._lastCommand = "M" |
| self._lastX, self._lastY = pt |
| |
| def _lineTo(self, pt): |
| """ |
| # duplicate point |
| >>> pen = SVGPathPen(None) |
| >>> pen.moveTo((10, 10)) |
| >>> pen.lineTo((10, 10)) |
| >>> pen._commands |
| ['M10 10'] |
| |
| # vertical line |
| >>> pen = SVGPathPen(None) |
| >>> pen.moveTo((10, 10)) |
| >>> pen.lineTo((10, 0)) |
| >>> pen._commands |
| ['M10 10', 'V0'] |
| |
| # horizontal line |
| >>> pen = SVGPathPen(None) |
| >>> pen.moveTo((10, 10)) |
| >>> pen.lineTo((0, 10)) |
| >>> pen._commands |
| ['M10 10', 'H0'] |
| |
| # basic |
| >>> pen = SVGPathPen(None) |
| >>> pen.lineTo((70, 80)) |
| >>> pen._commands |
| ['L70 80'] |
| |
| # basic following a moveto |
| >>> pen = SVGPathPen(None) |
| >>> pen.moveTo((0, 0)) |
| >>> pen.lineTo((10, 10)) |
| >>> pen._commands |
| ['M0 0', ' 10 10'] |
| """ |
| x, y = pt |
| # duplicate point |
| if x == self._lastX and y == self._lastY: |
| return |
| # vertical line |
| elif x == self._lastX: |
| cmd = "V" |
| pts = self._ntos(y) |
| # horizontal line |
| elif y == self._lastY: |
| cmd = "H" |
| pts = self._ntos(x) |
| # previous was a moveto |
| elif self._lastCommand == "M": |
| cmd = None |
| pts = " " + pointToString(pt, self._ntos) |
| # basic |
| else: |
| cmd = "L" |
| pts = pointToString(pt, self._ntos) |
| # write the string |
| t = "" |
| if cmd: |
| t += cmd |
| self._lastCommand = cmd |
| t += pts |
| self._commands.append(t) |
| # store for future reference |
| self._lastX, self._lastY = pt |
| |
| def _curveToOne(self, pt1, pt2, pt3): |
| """ |
| >>> pen = SVGPathPen(None) |
| >>> pen.curveTo((10, 20), (30, 40), (50, 60)) |
| >>> pen._commands |
| ['C10 20 30 40 50 60'] |
| """ |
| t = "C" |
| t += pointToString(pt1, self._ntos) + " " |
| t += pointToString(pt2, self._ntos) + " " |
| t += pointToString(pt3, self._ntos) |
| self._commands.append(t) |
| self._lastCommand = "C" |
| self._lastX, self._lastY = pt3 |
| |
| def _qCurveToOne(self, pt1, pt2): |
| """ |
| >>> pen = SVGPathPen(None) |
| >>> pen.qCurveTo((10, 20), (30, 40)) |
| >>> pen._commands |
| ['Q10 20 30 40'] |
| >>> from fontTools.misc.roundTools import otRound |
| >>> pen = SVGPathPen(None, ntos=lambda v: str(otRound(v))) |
| >>> pen.qCurveTo((3, 3), (7, 5), (11, 4)) |
| >>> pen._commands |
| ['Q3 3 5 4', 'Q7 5 11 4'] |
| """ |
| assert pt2 is not None |
| t = "Q" |
| t += pointToString(pt1, self._ntos) + " " |
| t += pointToString(pt2, self._ntos) |
| self._commands.append(t) |
| self._lastCommand = "Q" |
| self._lastX, self._lastY = pt2 |
| |
| def _closePath(self): |
| """ |
| >>> pen = SVGPathPen(None) |
| >>> pen.closePath() |
| >>> pen._commands |
| ['Z'] |
| """ |
| self._commands.append("Z") |
| self._lastCommand = "Z" |
| self._lastX = self._lastY = None |
| |
| def _endPath(self): |
| """ |
| >>> pen = SVGPathPen(None) |
| >>> pen.endPath() |
| >>> pen._commands |
| [] |
| """ |
| self._lastCommand = None |
| self._lastX = self._lastY = None |
| |
| def getCommands(self): |
| return "".join(self._commands) |
| |
| |
| def main(args=None): |
| """Generate per-character SVG from font and text""" |
| |
| if args is None: |
| import sys |
| |
| args = sys.argv[1:] |
| |
| from fontTools.ttLib import TTFont |
| import argparse |
| |
| parser = argparse.ArgumentParser( |
| "fonttools pens.svgPathPen", description="Generate SVG from text" |
| ) |
| parser.add_argument("font", metavar="font.ttf", help="Font file.") |
| parser.add_argument("text", metavar="text", nargs="?", help="Text string.") |
| parser.add_argument( |
| "-y", |
| metavar="<number>", |
| help="Face index into a collection to open. Zero based.", |
| ) |
| parser.add_argument( |
| "--glyphs", |
| metavar="whitespace-separated list of glyph names", |
| type=str, |
| help="Glyphs to show. Exclusive with text option", |
| ) |
| parser.add_argument( |
| "--variations", |
| metavar="AXIS=LOC", |
| default="", |
| help="List of space separated locations. A location consist in " |
| "the name of a variation axis, followed by '=' and a number. E.g.: " |
| "wght=700 wdth=80. The default is the location of the base master.", |
| ) |
| |
| options = parser.parse_args(args) |
| |
| fontNumber = int(options.y) if options.y is not None else 0 |
| |
| font = TTFont(options.font, fontNumber=fontNumber) |
| text = options.text |
| glyphs = options.glyphs |
| |
| location = {} |
| for tag_v in options.variations.split(): |
| fields = tag_v.split("=") |
| tag = fields[0].strip() |
| v = float(fields[1]) |
| location[tag] = v |
| |
| hhea = font["hhea"] |
| ascent, descent = hhea.ascent, hhea.descent |
| |
| glyphset = font.getGlyphSet(location=location) |
| cmap = font["cmap"].getBestCmap() |
| |
| if glyphs is not None and text is not None: |
| raise ValueError("Options --glyphs and --text are exclusive") |
| |
| if glyphs is None: |
| glyphs = " ".join(cmap[ord(u)] for u in text) |
| |
| glyphs = glyphs.split() |
| |
| s = "" |
| width = 0 |
| for g in glyphs: |
| glyph = glyphset[g] |
| |
| pen = SVGPathPen(glyphset) |
| glyph.draw(pen) |
| commands = pen.getCommands() |
| |
| s += '<g transform="translate(%d %d) scale(1 -1)"><path d="%s"/></g>\n' % ( |
| width, |
| ascent, |
| commands, |
| ) |
| |
| width += glyph.width |
| |
| print('<?xml version="1.0" encoding="UTF-8"?>') |
| print( |
| '<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">' |
| % (width, ascent - descent) |
| ) |
| print(s, end="") |
| print("</svg>") |
| |
| |
| if __name__ == "__main__": |
| import sys |
| |
| if len(sys.argv) == 1: |
| import doctest |
| |
| sys.exit(doctest.testmod().failed) |
| |
| sys.exit(main()) |