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