svgPathPen.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. from typing import Callable
  2. from fontTools.pens.basePen import BasePen
  3. def pointToString(pt, ntos=str):
  4. return " ".join(ntos(i) for i in pt)
  5. class SVGPathPen(BasePen):
  6. """Pen to draw SVG path d commands.
  7. Example::
  8. >>> pen = SVGPathPen(None)
  9. >>> pen.moveTo((0, 0))
  10. >>> pen.lineTo((1, 1))
  11. >>> pen.curveTo((2, 2), (3, 3), (4, 4))
  12. >>> pen.closePath()
  13. >>> pen.getCommands()
  14. 'M0 0 1 1C2 2 3 3 4 4Z'
  15. Args:
  16. glyphSet: a dictionary of drawable glyph objects keyed by name
  17. used to resolve component references in composite glyphs.
  18. ntos: a callable that takes a number and returns a string, to
  19. customize how numbers are formatted (default: str).
  20. Note:
  21. Fonts have a coordinate system where Y grows up, whereas in SVG,
  22. Y grows down. As such, rendering path data from this pen in
  23. SVG typically results in upside-down glyphs. You can fix this
  24. by wrapping the data from this pen in an SVG group element with
  25. transform, or wrap this pen in a transform pen. For example:
  26. spen = svgPathPen.SVGPathPen(glyphset)
  27. pen= TransformPen(spen , (1, 0, 0, -1, 0, 0))
  28. glyphset[glyphname].draw(pen)
  29. print(tpen.getCommands())
  30. """
  31. def __init__(self, glyphSet, ntos: Callable[[float], str] = str):
  32. BasePen.__init__(self, glyphSet)
  33. self._commands = []
  34. self._lastCommand = None
  35. self._lastX = None
  36. self._lastY = None
  37. self._ntos = ntos
  38. def _handleAnchor(self):
  39. """
  40. >>> pen = SVGPathPen(None)
  41. >>> pen.moveTo((0, 0))
  42. >>> pen.moveTo((10, 10))
  43. >>> pen._commands
  44. ['M10 10']
  45. """
  46. if self._lastCommand == "M":
  47. self._commands.pop(-1)
  48. def _moveTo(self, pt):
  49. """
  50. >>> pen = SVGPathPen(None)
  51. >>> pen.moveTo((0, 0))
  52. >>> pen._commands
  53. ['M0 0']
  54. >>> pen = SVGPathPen(None)
  55. >>> pen.moveTo((10, 0))
  56. >>> pen._commands
  57. ['M10 0']
  58. >>> pen = SVGPathPen(None)
  59. >>> pen.moveTo((0, 10))
  60. >>> pen._commands
  61. ['M0 10']
  62. """
  63. self._handleAnchor()
  64. t = "M%s" % (pointToString(pt, self._ntos))
  65. self._commands.append(t)
  66. self._lastCommand = "M"
  67. self._lastX, self._lastY = pt
  68. def _lineTo(self, pt):
  69. """
  70. # duplicate point
  71. >>> pen = SVGPathPen(None)
  72. >>> pen.moveTo((10, 10))
  73. >>> pen.lineTo((10, 10))
  74. >>> pen._commands
  75. ['M10 10']
  76. # vertical line
  77. >>> pen = SVGPathPen(None)
  78. >>> pen.moveTo((10, 10))
  79. >>> pen.lineTo((10, 0))
  80. >>> pen._commands
  81. ['M10 10', 'V0']
  82. # horizontal line
  83. >>> pen = SVGPathPen(None)
  84. >>> pen.moveTo((10, 10))
  85. >>> pen.lineTo((0, 10))
  86. >>> pen._commands
  87. ['M10 10', 'H0']
  88. # basic
  89. >>> pen = SVGPathPen(None)
  90. >>> pen.lineTo((70, 80))
  91. >>> pen._commands
  92. ['L70 80']
  93. # basic following a moveto
  94. >>> pen = SVGPathPen(None)
  95. >>> pen.moveTo((0, 0))
  96. >>> pen.lineTo((10, 10))
  97. >>> pen._commands
  98. ['M0 0', ' 10 10']
  99. """
  100. x, y = pt
  101. # duplicate point
  102. if x == self._lastX and y == self._lastY:
  103. return
  104. # vertical line
  105. elif x == self._lastX:
  106. cmd = "V"
  107. pts = self._ntos(y)
  108. # horizontal line
  109. elif y == self._lastY:
  110. cmd = "H"
  111. pts = self._ntos(x)
  112. # previous was a moveto
  113. elif self._lastCommand == "M":
  114. cmd = None
  115. pts = " " + pointToString(pt, self._ntos)
  116. # basic
  117. else:
  118. cmd = "L"
  119. pts = pointToString(pt, self._ntos)
  120. # write the string
  121. t = ""
  122. if cmd:
  123. t += cmd
  124. self._lastCommand = cmd
  125. t += pts
  126. self._commands.append(t)
  127. # store for future reference
  128. self._lastX, self._lastY = pt
  129. def _curveToOne(self, pt1, pt2, pt3):
  130. """
  131. >>> pen = SVGPathPen(None)
  132. >>> pen.curveTo((10, 20), (30, 40), (50, 60))
  133. >>> pen._commands
  134. ['C10 20 30 40 50 60']
  135. """
  136. t = "C"
  137. t += pointToString(pt1, self._ntos) + " "
  138. t += pointToString(pt2, self._ntos) + " "
  139. t += pointToString(pt3, self._ntos)
  140. self._commands.append(t)
  141. self._lastCommand = "C"
  142. self._lastX, self._lastY = pt3
  143. def _qCurveToOne(self, pt1, pt2):
  144. """
  145. >>> pen = SVGPathPen(None)
  146. >>> pen.qCurveTo((10, 20), (30, 40))
  147. >>> pen._commands
  148. ['Q10 20 30 40']
  149. >>> from fontTools.misc.roundTools import otRound
  150. >>> pen = SVGPathPen(None, ntos=lambda v: str(otRound(v)))
  151. >>> pen.qCurveTo((3, 3), (7, 5), (11, 4))
  152. >>> pen._commands
  153. ['Q3 3 5 4', 'Q7 5 11 4']
  154. """
  155. assert pt2 is not None
  156. t = "Q"
  157. t += pointToString(pt1, self._ntos) + " "
  158. t += pointToString(pt2, self._ntos)
  159. self._commands.append(t)
  160. self._lastCommand = "Q"
  161. self._lastX, self._lastY = pt2
  162. def _closePath(self):
  163. """
  164. >>> pen = SVGPathPen(None)
  165. >>> pen.closePath()
  166. >>> pen._commands
  167. ['Z']
  168. """
  169. self._commands.append("Z")
  170. self._lastCommand = "Z"
  171. self._lastX = self._lastY = None
  172. def _endPath(self):
  173. """
  174. >>> pen = SVGPathPen(None)
  175. >>> pen.endPath()
  176. >>> pen._commands
  177. []
  178. """
  179. self._lastCommand = None
  180. self._lastX = self._lastY = None
  181. def getCommands(self):
  182. return "".join(self._commands)
  183. def main(args=None):
  184. """Generate per-character SVG from font and text"""
  185. if args is None:
  186. import sys
  187. args = sys.argv[1:]
  188. from fontTools.ttLib import TTFont
  189. import argparse
  190. parser = argparse.ArgumentParser(
  191. "fonttools pens.svgPathPen", description="Generate SVG from text"
  192. )
  193. parser.add_argument("font", metavar="font.ttf", help="Font file.")
  194. parser.add_argument("text", metavar="text", help="Text string.")
  195. parser.add_argument(
  196. "-y",
  197. metavar="<number>",
  198. help="Face index into a collection to open. Zero based.",
  199. )
  200. parser.add_argument(
  201. "--variations",
  202. metavar="AXIS=LOC",
  203. default="",
  204. help="List of space separated locations. A location consist in "
  205. "the name of a variation axis, followed by '=' and a number. E.g.: "
  206. "wght=700 wdth=80. The default is the location of the base master.",
  207. )
  208. options = parser.parse_args(args)
  209. fontNumber = int(options.y) if options.y is not None else 0
  210. font = TTFont(options.font, fontNumber=fontNumber)
  211. text = options.text
  212. location = {}
  213. for tag_v in options.variations.split():
  214. fields = tag_v.split("=")
  215. tag = fields[0].strip()
  216. v = float(fields[1])
  217. location[tag] = v
  218. hhea = font["hhea"]
  219. ascent, descent = hhea.ascent, hhea.descent
  220. glyphset = font.getGlyphSet(location=location)
  221. cmap = font["cmap"].getBestCmap()
  222. s = ""
  223. width = 0
  224. for u in text:
  225. g = cmap[ord(u)]
  226. glyph = glyphset[g]
  227. pen = SVGPathPen(glyphset)
  228. glyph.draw(pen)
  229. commands = pen.getCommands()
  230. s += '<g transform="translate(%d %d) scale(1 -1)"><path d="%s"/></g>\n' % (
  231. width,
  232. ascent,
  233. commands,
  234. )
  235. width += glyph.width
  236. print('<?xml version="1.0" encoding="UTF-8"?>')
  237. print(
  238. '<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">'
  239. % (width, ascent - descent)
  240. )
  241. print(s, end="")
  242. print("</svg>")
  243. if __name__ == "__main__":
  244. import sys
  245. if len(sys.argv) == 1:
  246. import doctest
  247. sys.exit(doctest.testmod().failed)
  248. sys.exit(main())