123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- 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", help="Text string.")
- parser.add_argument(
- "-y",
- metavar="<number>",
- help="Face index into a collection to open. Zero based.",
- )
- 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
- 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()
- s = ""
- width = 0
- for u in text:
- g = cmap[ord(u)]
- 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())
|