• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from __future__ import print_function, division, absolute_import
2from fontTools.misc.py23 import *
3from array import array
4from fontTools.pens.basePen import LoggingPen
5from fontTools.pens.transformPen import TransformPen
6from fontTools.ttLib.tables import ttProgram
7from fontTools.ttLib.tables._g_l_y_f import Glyph
8from fontTools.ttLib.tables._g_l_y_f import GlyphComponent
9from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
10
11
12__all__ = ["TTGlyphPen"]
13
14
15# the max value that can still fit in an F2Dot14:
16# 1.99993896484375
17MAX_F2DOT14 = 0x7FFF / (1 << 14)
18
19
20class TTGlyphPen(LoggingPen):
21    """Pen used for drawing to a TrueType glyph.
22
23    If `handleOverflowingTransforms` is True, the components' transform values
24    are checked that they don't overflow the limits of a F2Dot14 number:
25    -2.0 <= v < +2.0. If any transform value exceeds these, the composite
26    glyph is decomposed.
27    An exception to this rule is done for values that are very close to +2.0
28    (both for consistency with the -2.0 case, and for the relative frequency
29    these occur in real fonts). When almost +2.0 values occur (and all other
30    values are within the range -2.0 <= x <= +2.0), they are clamped to the
31    maximum positive value that can still be encoded as an F2Dot14: i.e.
32    1.99993896484375.
33    If False, no check is done and all components are translated unmodified
34    into the glyf table, followed by an inevitable `struct.error` once an
35    attempt is made to compile them.
36    """
37
38    def __init__(self, glyphSet, handleOverflowingTransforms=True):
39        self.glyphSet = glyphSet
40        self.handleOverflowingTransforms = handleOverflowingTransforms
41        self.init()
42
43    def init(self):
44        self.points = []
45        self.endPts = []
46        self.types = []
47        self.components = []
48
49    def _addPoint(self, pt, onCurve):
50        self.points.append(pt)
51        self.types.append(onCurve)
52
53    def _popPoint(self):
54        self.points.pop()
55        self.types.pop()
56
57    def _isClosed(self):
58        return (
59            (not self.points) or
60            (self.endPts and self.endPts[-1] == len(self.points) - 1))
61
62    def lineTo(self, pt):
63        self._addPoint(pt, 1)
64
65    def moveTo(self, pt):
66        assert self._isClosed(), '"move"-type point must begin a new contour.'
67        self._addPoint(pt, 1)
68
69    def qCurveTo(self, *points):
70        assert len(points) >= 1
71        for pt in points[:-1]:
72            self._addPoint(pt, 0)
73
74        # last point is None if there are no on-curve points
75        if points[-1] is not None:
76            self._addPoint(points[-1], 1)
77
78    def closePath(self):
79        endPt = len(self.points) - 1
80
81        # ignore anchors (one-point paths)
82        if endPt == 0 or (self.endPts and endPt == self.endPts[-1] + 1):
83            self._popPoint()
84            return
85
86        # if first and last point on this path are the same, remove last
87        startPt = 0
88        if self.endPts:
89            startPt = self.endPts[-1] + 1
90        if self.points[startPt] == self.points[endPt]:
91            self._popPoint()
92            endPt -= 1
93
94        self.endPts.append(endPt)
95
96    def endPath(self):
97        # TrueType contours are always "closed"
98        self.closePath()
99
100    def addComponent(self, glyphName, transformation):
101        self.components.append((glyphName, transformation))
102
103    def _buildComponents(self, componentFlags):
104        if self.handleOverflowingTransforms:
105            # we can't encode transform values > 2 or < -2 in F2Dot14,
106            # so we must decompose the glyph if any transform exceeds these
107            overflowing = any(s > 2 or s < -2
108                              for (glyphName, transformation) in self.components
109                              for s in transformation[:4])
110        components = []
111        for glyphName, transformation in self.components:
112            if glyphName not in self.glyphSet:
113                self.log.warning(
114                    "skipped non-existing component '%s'", glyphName
115                )
116                continue
117            if (self.points or
118                    (self.handleOverflowingTransforms and overflowing)):
119                # can't have both coordinates and components, so decompose
120                tpen = TransformPen(self, transformation)
121                self.glyphSet[glyphName].draw(tpen)
122                continue
123
124            component = GlyphComponent()
125            component.glyphName = glyphName
126            component.x, component.y = transformation[4:]
127            transformation = transformation[:4]
128            if transformation != (1, 0, 0, 1):
129                if (self.handleOverflowingTransforms and
130                        any(MAX_F2DOT14 < s <= 2 for s in transformation)):
131                    # clamp values ~= +2.0 so we can keep the component
132                    transformation = tuple(MAX_F2DOT14 if MAX_F2DOT14 < s <= 2
133                                           else s for s in transformation)
134                component.transform = (transformation[:2], transformation[2:])
135            component.flags = componentFlags
136            components.append(component)
137        return components
138
139    def glyph(self, componentFlags=0x4):
140        assert self._isClosed(), "Didn't close last contour."
141
142        components = self._buildComponents(componentFlags)
143
144        glyph = Glyph()
145        glyph.coordinates = GlyphCoordinates(self.points)
146        glyph.endPtsOfContours = self.endPts
147        glyph.flags = array("B", self.types)
148        self.init()
149
150        if components:
151            glyph.components = components
152            glyph.numberOfContours = -1
153        else:
154            glyph.numberOfContours = len(glyph.endPtsOfContours)
155            glyph.program = ttProgram.Program()
156            glyph.program.fromBytecode(b"")
157
158        return glyph
159