classes.c16_glyph_inspector

  1import drawBot
  2from fontTools.ttLib import TTFont
  3from fontTools.pens.basePen import BasePen
  4from fontTools.pens.boundsPen import BoundsPen
  5from typing import TypedDict, Any, Callable
  6from loguru import logger
  7from datatypes import DLineStyle
  8from lib import graphics
  9
 10
 11class TTGlyph(TypedDict):
 12    """A simplified representation of a TrueType glyph for inspection purposes. This is not a full implementation of a TTGlyph but includes key properties."""
 13
 14    name: str
 15    width: int
 16    height: int | None
 17    lsb: int
 18    tsb: int | None
 19    draw: Callable
 20    recalcBounds: bool
 21    glyphSet: Any  # fontTools glyph set object (private type in practice)
 22
 23
 24def setFill(color=(0,)):
 25    drawBot.fill(*color)
 26    drawBot.stroke(None)
 27
 28
 29def setStroke(width=0.5, pattern: graphics.LinePattern = "solid"):
 30    DLineStyle(
 31        strokeWidth=width,
 32        strokeColor=(0,),
 33        lineDash=5 if pattern == "dashed" else None,
 34        lineCap="butt",
 35        clearFill=True,
 36    ).apply()
 37
 38
 39class ShowBeziersPen(BasePen):
 40    upscale = lambda self, value: value / self.scale
 41
 42    def __init__(
 43        self,
 44        glyphSet: TTGlyph,
 45        anchorSize: float,
 46        handleSize: float,
 47        glyphStroke: float,
 48        handleStroke: float,
 49        scale: float = 1,
 50    ):
 51        super().__init__(glyphSet)
 52        self.prevPt = None
 53        self.scale = scale
 54        self.anchorSize = anchorSize
 55        self.handleSize = handleSize
 56        self.glyphStroke = glyphStroke
 57        self.handleStroke = handleStroke
 58
 59    @property
 60    def _anchorSize(self):
 61        return self.upscale(self.anchorSize)
 62
 63    @property
 64    def _handleSize(self):
 65        return self.upscale(self.handleSize)
 66
 67    def _drawAnchor(self, pt):
 68        setFill()
 69        x, y = pt
 70        drawBot.rect(
 71            x - self._anchorSize / 2,
 72            y - self._anchorSize / 2,
 73            self._anchorSize,
 74            self._anchorSize,
 75        )
 76
 77    def _drawHandle(self, pt):
 78        setFill()
 79        x, y = pt
 80        drawBot.oval(
 81            x - self._handleSize / 2,
 82            y - self._handleSize / 2,
 83            self._handleSize,
 84            self._handleSize,
 85        )
 86
 87    def _moveTo(self, pt):
 88        self._drawAnchor(pt)
 89        self.prevPt = pt
 90
 91    def _lineTo(self, pt):
 92        self._drawAnchor(pt)
 93        self.prevPt = pt
 94
 95    def _curveToOne(self, pt1, pt2, pt3):
 96        # Handle lines
 97        setStroke(self.upscale(self.handleStroke))
 98        drawBot.line(pt1, self.prevPt)
 99        drawBot.line(pt2, pt3)
100
101        [self._drawHandle(pt) for pt in [pt1, pt2]]
102        self._drawAnchor(pt3)
103        self.prevPt = pt3
104
105    # Custom method to render the glyph with both outline and bezier handles
106    def render(self, glyph: TTGlyph):
107        with drawBot.savedState():
108            drawBot.scale(self.scale)
109
110            # Draw glyph outline first
111            setStroke(self.upscale(self.glyphStroke))
112            glyphPath = drawBot.BezierPath(glyphSet=glyph)
113            glyph.draw(glyphPath)
114            drawBot.drawPath(glyphPath)
115
116            # Draw bezier handles on top
117            glyph.draw(self)
118
119
120class KGlyphInspector:
121    TTGlyph = TTGlyph  # Expose type alias as a class attribute for external use
122
123    """Inspect and draw font glyphs in DrawBot at a target point size."""
124
125    def __init__(self, fontPath: str, fontSize: int):
126        """Load a font and prepare glyph access for the requested size.
127
128        Args:
129            fontPath: Absolute path to a font file.
130            fontSize: Target drawing size in points.
131
132        Notes:
133            `self.scale` converts glyph-space units (UPM) to page-space points.
134        """
135        self.fontFile = TTFont(fontPath)
136
137        self.scale = float(fontSize / self.fontFile["head"].unitsPerEm)
138
139        self._cmap = self.fontFile.getBestCmap()
140        self._glyphSet = self.fontFile.getGlyphSet()
141
142    def getGlyphs(self, text: str) -> list[TTGlyph]:
143        """Return glyph objects for each character in a text string.
144
145        Args:
146            text: Text to convert into ordered glyphs.
147
148        Returns:
149            Glyph list in the same order as the input text.
150
151        Raises:
152            KeyError: If a character is missing from the font cmap.
153        """
154        try:
155            chars = list(text)
156            glyphNames = [self._cmap[ord(char)] for char in chars]
157            return [self._glyphSet[name] for name in glyphNames]
158        except KeyError as e:
159            logger.error(f"Character '{e.args[0]}' not found in font's cmap.")
160            raise
161
162    def calcGlyphBounds(self, glyph: TTGlyph) -> tuple[int, int, int, int]:
163        """Measure raw glyph bounds in glyph-space units.
164
165        Useful when you need geometric extents for alignment or debugging.
166        """
167        boundsPen = BoundsPen(glyph)
168        glyph.draw(boundsPen)
169        return boundsPen.bounds
170
171    def drawGlyph(self, glyph: TTGlyph):
172        """Draw a filled glyph outline using the inspector's scale."""
173        with drawBot.savedState():
174            drawBot.scale(self.scale)
175            glyphPath = drawBot.BezierPath(glyphSet=glyph)
176            glyph.draw(glyphPath)
177            drawBot.drawPath(glyphPath)
178
179    def drawGlyphBeziers(
180        self,
181        glyph: TTGlyph,
182        anchorSize: float = 5,
183        handleSize: float = 5,
184        glyphStroke: float = 1,
185        handleStroke: float = 0.5,
186    ):
187        """Draw glyph outline plus bezier anchors/handles for inspection.
188
189        Args:
190            glyph: Glyph object from `getGlyphs`.
191            anchorSize: Anchor marker size in page points.
192            handleSize: Handle marker size in page points.
193            glyphStroke: Outline stroke width in page points.
194            handleStroke: Handle line stroke width in page points.
195        """
196        beziersPen = ShowBeziersPen(
197            glyph,
198            anchorSize=anchorSize,
199            handleSize=handleSize,
200            glyphStroke=glyphStroke,
201            handleStroke=handleStroke,
202            scale=self.scale,
203        )
204        beziersPen.render(glyph)
class TTGlyph(typing.TypedDict):
12class TTGlyph(TypedDict):
13    """A simplified representation of a TrueType glyph for inspection purposes. This is not a full implementation of a TTGlyph but includes key properties."""
14
15    name: str
16    width: int
17    height: int | None
18    lsb: int
19    tsb: int | None
20    draw: Callable
21    recalcBounds: bool
22    glyphSet: Any  # fontTools glyph set object (private type in practice)

A simplified representation of a TrueType glyph for inspection purposes. This is not a full implementation of a TTGlyph but includes key properties.

name: str
width: int
height: int | None
lsb: int
tsb: int | None
draw: Callable
recalcBounds: bool
glyphSet: Any
def setFill(color=(0,)):
25def setFill(color=(0,)):
26    drawBot.fill(*color)
27    drawBot.stroke(None)
def setStroke(width=0.5, pattern: Literal['solid', 'dashed'] = 'solid'):
30def setStroke(width=0.5, pattern: graphics.LinePattern = "solid"):
31    DLineStyle(
32        strokeWidth=width,
33        strokeColor=(0,),
34        lineDash=5 if pattern == "dashed" else None,
35        lineCap="butt",
36        clearFill=True,
37    ).apply()
class ShowBeziersPen(fontTools.pens.basePen.BasePen):
 40class ShowBeziersPen(BasePen):
 41    upscale = lambda self, value: value / self.scale
 42
 43    def __init__(
 44        self,
 45        glyphSet: TTGlyph,
 46        anchorSize: float,
 47        handleSize: float,
 48        glyphStroke: float,
 49        handleStroke: float,
 50        scale: float = 1,
 51    ):
 52        super().__init__(glyphSet)
 53        self.prevPt = None
 54        self.scale = scale
 55        self.anchorSize = anchorSize
 56        self.handleSize = handleSize
 57        self.glyphStroke = glyphStroke
 58        self.handleStroke = handleStroke
 59
 60    @property
 61    def _anchorSize(self):
 62        return self.upscale(self.anchorSize)
 63
 64    @property
 65    def _handleSize(self):
 66        return self.upscale(self.handleSize)
 67
 68    def _drawAnchor(self, pt):
 69        setFill()
 70        x, y = pt
 71        drawBot.rect(
 72            x - self._anchorSize / 2,
 73            y - self._anchorSize / 2,
 74            self._anchorSize,
 75            self._anchorSize,
 76        )
 77
 78    def _drawHandle(self, pt):
 79        setFill()
 80        x, y = pt
 81        drawBot.oval(
 82            x - self._handleSize / 2,
 83            y - self._handleSize / 2,
 84            self._handleSize,
 85            self._handleSize,
 86        )
 87
 88    def _moveTo(self, pt):
 89        self._drawAnchor(pt)
 90        self.prevPt = pt
 91
 92    def _lineTo(self, pt):
 93        self._drawAnchor(pt)
 94        self.prevPt = pt
 95
 96    def _curveToOne(self, pt1, pt2, pt3):
 97        # Handle lines
 98        setStroke(self.upscale(self.handleStroke))
 99        drawBot.line(pt1, self.prevPt)
100        drawBot.line(pt2, pt3)
101
102        [self._drawHandle(pt) for pt in [pt1, pt2]]
103        self._drawAnchor(pt3)
104        self.prevPt = pt3
105
106    # Custom method to render the glyph with both outline and bezier handles
107    def render(self, glyph: TTGlyph):
108        with drawBot.savedState():
109            drawBot.scale(self.scale)
110
111            # Draw glyph outline first
112            setStroke(self.upscale(self.glyphStroke))
113            glyphPath = drawBot.BezierPath(glyphSet=glyph)
114            glyph.draw(glyphPath)
115            drawBot.drawPath(glyphPath)
116
117            # Draw bezier handles on top
118            glyph.draw(self)

Base class for drawing pens. You must override _moveTo, _lineTo and _curveToOne. You may additionally override _closePath, _endPath, addComponent, addVarComponent, and/or _qCurveToOne. You should not override any other methods.

ShowBeziersPen( glyphSet: TTGlyph, anchorSize: float, handleSize: float, glyphStroke: float, handleStroke: float, scale: float = 1)
43    def __init__(
44        self,
45        glyphSet: TTGlyph,
46        anchorSize: float,
47        handleSize: float,
48        glyphStroke: float,
49        handleStroke: float,
50        scale: float = 1,
51    ):
52        super().__init__(glyphSet)
53        self.prevPt = None
54        self.scale = scale
55        self.anchorSize = anchorSize
56        self.handleSize = handleSize
57        self.glyphStroke = glyphStroke
58        self.handleStroke = handleStroke

Takes a 'glyphSet' argument (dict), in which the glyphs that are referenced as components are looked up by their name.

If the optional 'reverseFlipped' argument is True, components whose transformation matrix has a negative determinant will be decomposed with a reversed path direction to compensate for the flip.

The optional 'skipMissingComponents' argument can be set to True/False to override the homonymous class attribute for a given pen instance.

def upscale(self, value):
41    upscale = lambda self, value: value / self.scale
prevPt
scale
anchorSize
handleSize
glyphStroke
handleStroke
def render(self, glyph: TTGlyph):
107    def render(self, glyph: TTGlyph):
108        with drawBot.savedState():
109            drawBot.scale(self.scale)
110
111            # Draw glyph outline first
112            setStroke(self.upscale(self.glyphStroke))
113            glyphPath = drawBot.BezierPath(glyphSet=glyph)
114            glyph.draw(glyphPath)
115            drawBot.drawPath(glyphPath)
116
117            # Draw bezier handles on top
118            glyph.draw(self)
class KGlyphInspector:
121class KGlyphInspector:
122    TTGlyph = TTGlyph  # Expose type alias as a class attribute for external use
123
124    """Inspect and draw font glyphs in DrawBot at a target point size."""
125
126    def __init__(self, fontPath: str, fontSize: int):
127        """Load a font and prepare glyph access for the requested size.
128
129        Args:
130            fontPath: Absolute path to a font file.
131            fontSize: Target drawing size in points.
132
133        Notes:
134            `self.scale` converts glyph-space units (UPM) to page-space points.
135        """
136        self.fontFile = TTFont(fontPath)
137
138        self.scale = float(fontSize / self.fontFile["head"].unitsPerEm)
139
140        self._cmap = self.fontFile.getBestCmap()
141        self._glyphSet = self.fontFile.getGlyphSet()
142
143    def getGlyphs(self, text: str) -> list[TTGlyph]:
144        """Return glyph objects for each character in a text string.
145
146        Args:
147            text: Text to convert into ordered glyphs.
148
149        Returns:
150            Glyph list in the same order as the input text.
151
152        Raises:
153            KeyError: If a character is missing from the font cmap.
154        """
155        try:
156            chars = list(text)
157            glyphNames = [self._cmap[ord(char)] for char in chars]
158            return [self._glyphSet[name] for name in glyphNames]
159        except KeyError as e:
160            logger.error(f"Character '{e.args[0]}' not found in font's cmap.")
161            raise
162
163    def calcGlyphBounds(self, glyph: TTGlyph) -> tuple[int, int, int, int]:
164        """Measure raw glyph bounds in glyph-space units.
165
166        Useful when you need geometric extents for alignment or debugging.
167        """
168        boundsPen = BoundsPen(glyph)
169        glyph.draw(boundsPen)
170        return boundsPen.bounds
171
172    def drawGlyph(self, glyph: TTGlyph):
173        """Draw a filled glyph outline using the inspector's scale."""
174        with drawBot.savedState():
175            drawBot.scale(self.scale)
176            glyphPath = drawBot.BezierPath(glyphSet=glyph)
177            glyph.draw(glyphPath)
178            drawBot.drawPath(glyphPath)
179
180    def drawGlyphBeziers(
181        self,
182        glyph: TTGlyph,
183        anchorSize: float = 5,
184        handleSize: float = 5,
185        glyphStroke: float = 1,
186        handleStroke: float = 0.5,
187    ):
188        """Draw glyph outline plus bezier anchors/handles for inspection.
189
190        Args:
191            glyph: Glyph object from `getGlyphs`.
192            anchorSize: Anchor marker size in page points.
193            handleSize: Handle marker size in page points.
194            glyphStroke: Outline stroke width in page points.
195            handleStroke: Handle line stroke width in page points.
196        """
197        beziersPen = ShowBeziersPen(
198            glyph,
199            anchorSize=anchorSize,
200            handleSize=handleSize,
201            glyphStroke=glyphStroke,
202            handleStroke=handleStroke,
203            scale=self.scale,
204        )
205        beziersPen.render(glyph)
KGlyphInspector(fontPath: str, fontSize: int)
126    def __init__(self, fontPath: str, fontSize: int):
127        """Load a font and prepare glyph access for the requested size.
128
129        Args:
130            fontPath: Absolute path to a font file.
131            fontSize: Target drawing size in points.
132
133        Notes:
134            `self.scale` converts glyph-space units (UPM) to page-space points.
135        """
136        self.fontFile = TTFont(fontPath)
137
138        self.scale = float(fontSize / self.fontFile["head"].unitsPerEm)
139
140        self._cmap = self.fontFile.getBestCmap()
141        self._glyphSet = self.fontFile.getGlyphSet()

Load a font and prepare glyph access for the requested size.

Arguments:
  • fontPath: Absolute path to a font file.
  • fontSize: Target drawing size in points.
Notes:

self.scale converts glyph-space units (UPM) to page-space points.

fontFile
scale
def getGlyphs(self, text: str) -> list[TTGlyph]:
143    def getGlyphs(self, text: str) -> list[TTGlyph]:
144        """Return glyph objects for each character in a text string.
145
146        Args:
147            text: Text to convert into ordered glyphs.
148
149        Returns:
150            Glyph list in the same order as the input text.
151
152        Raises:
153            KeyError: If a character is missing from the font cmap.
154        """
155        try:
156            chars = list(text)
157            glyphNames = [self._cmap[ord(char)] for char in chars]
158            return [self._glyphSet[name] for name in glyphNames]
159        except KeyError as e:
160            logger.error(f"Character '{e.args[0]}' not found in font's cmap.")
161            raise

Return glyph objects for each character in a text string.

Arguments:
  • text: Text to convert into ordered glyphs.
Returns:

Glyph list in the same order as the input text.

Raises:
  • KeyError: If a character is missing from the font cmap.
def calcGlyphBounds( self, glyph: TTGlyph) -> tuple[int, int, int, int]:
163    def calcGlyphBounds(self, glyph: TTGlyph) -> tuple[int, int, int, int]:
164        """Measure raw glyph bounds in glyph-space units.
165
166        Useful when you need geometric extents for alignment or debugging.
167        """
168        boundsPen = BoundsPen(glyph)
169        glyph.draw(boundsPen)
170        return boundsPen.bounds

Measure raw glyph bounds in glyph-space units.

Useful when you need geometric extents for alignment or debugging.

def drawGlyph(self, glyph: TTGlyph):
172    def drawGlyph(self, glyph: TTGlyph):
173        """Draw a filled glyph outline using the inspector's scale."""
174        with drawBot.savedState():
175            drawBot.scale(self.scale)
176            glyphPath = drawBot.BezierPath(glyphSet=glyph)
177            glyph.draw(glyphPath)
178            drawBot.drawPath(glyphPath)

Draw a filled glyph outline using the inspector's scale.

def drawGlyphBeziers( self, glyph: TTGlyph, anchorSize: float = 5, handleSize: float = 5, glyphStroke: float = 1, handleStroke: float = 0.5):
180    def drawGlyphBeziers(
181        self,
182        glyph: TTGlyph,
183        anchorSize: float = 5,
184        handleSize: float = 5,
185        glyphStroke: float = 1,
186        handleStroke: float = 0.5,
187    ):
188        """Draw glyph outline plus bezier anchors/handles for inspection.
189
190        Args:
191            glyph: Glyph object from `getGlyphs`.
192            anchorSize: Anchor marker size in page points.
193            handleSize: Handle marker size in page points.
194            glyphStroke: Outline stroke width in page points.
195            handleStroke: Handle line stroke width in page points.
196        """
197        beziersPen = ShowBeziersPen(
198            glyph,
199            anchorSize=anchorSize,
200            handleSize=handleSize,
201            glyphStroke=glyphStroke,
202            handleStroke=handleStroke,
203            scale=self.scale,
204        )
205        beziersPen.render(glyph)

Draw glyph outline plus bezier anchors/handles for inspection.

Arguments:
  • glyph: Glyph object from getGlyphs.
  • anchorSize: Anchor marker size in page points.
  • handleSize: Handle marker size in page points.
  • glyphStroke: Outline stroke width in page points.
  • handleStroke: Handle line stroke width in page points.
class KGlyphInspector.TTGlyph(typing.TypedDict):
12class TTGlyph(TypedDict):
13    """A simplified representation of a TrueType glyph for inspection purposes. This is not a full implementation of a TTGlyph but includes key properties."""
14
15    name: str
16    width: int
17    height: int | None
18    lsb: int
19    tsb: int | None
20    draw: Callable
21    recalcBounds: bool
22    glyphSet: Any  # fontTools glyph set object (private type in practice)

Inspect and draw font glyphs in DrawBot at a target point size.