classes.c14_metrics

  1import drawBot
  2from collections import OrderedDict
  3from fontTools.ttLib import TTFont
  4from fontTools.pens.boundsPen import BoundsPen
  5from typing import TYPE_CHECKING, List, Literal, Dict, Protocol, Optional
  6
  7if TYPE_CHECKING:
  8    from datatypes import DFontProps
  9from dataclasses import dataclass
 10from lib import fonts, layout
 11from loguru import logger
 12from icecream import ic
 13
 14MetricName = Literal[
 15    "ascender",
 16    "capHeight",
 17    "midCapHeight",
 18    "xHeight",
 19    "midXHeight",
 20    "baseline",
 21    "descender",
 22]
 23"Named font metrics found in OS/2 table."
 24
 25BlockMetricName = MetricName | Literal["minY", "maxY"]
 26"Metric names for lines computed in a visual line block, including computed extrema found in given text."
 27DistinctRules = Dict[BlockMetricName, BlockMetricName]
 28"Mapping of line names to other line names for distinctness filtering, meaning 'keep lineA only if distinct from lineB'."
 29
 30DEBUG = 0
 31
 32
 33@dataclass
 34class MetricBlock:
 35    """Metric information for one rendered text block (typically one visual line).
 36
 37    Attributes:
 38        text: Visible substring represented by this block.
 39        coords: Block frame as ``(x, baselineY, width, height)``.
 40        lines: Computed metric lines for the block.
 41    """
 42
 43    text: str
 44    coords: tuple[float, float, float, float]
 45    lines: List["BlockLine"]
 46
 47    def line(self, name: BlockMetricName, required: bool = False) -> "BlockLine | None":
 48        """Return one metric line by name.
 49
 50        Args:
 51            name: Metric key (for example ``baseline`` or ``capHeight``).
 52            required: If ``True``, raise ``ValueError`` when missing.
 53
 54        Returns:
 55            The matching ``BlockLine`` or ``None``.
 56        """
 57        for line in self.lines:
 58            if line.name == name:
 59                return line
 60        if required:
 61            raise ValueError(f"Metric {name} not found in block with text: {self.text}")
 62        return None
 63
 64    def select(
 65        self,
 66        names: List[BlockMetricName],
 67        distinctFrom: Optional[DistinctRules] = None,
 68    ) -> Dict[BlockMetricName, "BlockLine"]:
 69        """Return selected metric lines with optional distinctness filtering.
 70
 71        Args:
 72            names: Metric names to include in the initial selection.
 73            distinctFrom: Optional mapping ``{lineA: lineB}`` meaning
 74                "keep ``lineA`` only if it is distinct from ``lineB``".
 75
 76        Returns:
 77            Mapping from metric name to ``BlockLine`` for visible lines.
 78        """
 79        selected = {
 80            line.name: line
 81            for line in self.lines
 82            if line.name in names and line.y is not None
 83        }
 84
 85        if not distinctFrom:
 86            return selected
 87
 88        for lineName, compareTo in distinctFrom.items():
 89            line = selected.get(lineName)
 90            if line is None:
 91                continue
 92            if not line.isDistinctFrom(selected.get(compareTo)):
 93                selected.pop(lineName, None)
 94
 95        return selected
 96
 97
 98@dataclass
 99class BlockLine:
100    """One horizontal metric line belonging to a ``MetricBlock``.
101
102    Attributes:
103        name: Metric label.
104        y: Absolute Y coordinate in page space.
105        offset: Relative Y offset from baseline.
106        isDistinct: Whether this line is distinct from all sibling lines in block.
107        threshold: Distinctness threshold in points.
108        value: Raw metric value in font units (unitsPerEm space).
109    """
110
111    name: BlockMetricName
112    y: float | None
113    offset: float | None
114    isDistinct: bool
115    threshold: float
116    value: float
117
118    def __post_init__(self) -> None:
119        if self.value is None:
120            raise ValueError(f"BlockLine '{self.name}' value cannot be None")
121
122    def isIn(self, names: List[BlockMetricName]) -> bool:
123        """Check whether this line name is present in a name list."""
124        return self.name in names
125
126    def isDistinctFrom(self, other: "BlockLine | None") -> bool:
127        """Compare this line with another line using the line threshold.
128
129        Args:
130            other: Another ``BlockLine`` (or ``None``).
131
132        Returns:
133            ``True`` if both offsets exist and differ more than ``threshold``.
134        """
135        if self.offset is None or other is None or other.offset is None:
136            return False
137        return abs(self.offset - other.offset) > self.threshold
138
139    def annotate(
140        self,
141        x: float,
142        fontProps: "DFontProps",
143        *,
144        dy: layout.UnitSource = 0,
145        edge: Literal["top", "bottom"] = "top",
146        text: str | None = None,
147    ) -> None:
148        """Draw a text label for this metric line.
149
150        Args:
151            x: Horizontal position for the label baseline.
152            fontProps: Font properties applied when rendering the label.
153            dy: Additional Y offset (implicit unit: mm). Positive values increase the gap
154                between the label and the metric line.
155            edge: ``"top"`` places the label above the line (text
156                baseline sits at ``self.y + dy``); ``"bottom"`` places
157                the label below (text floats under the line).
158            text: Label string. Defaults to ``self.name``.
159        """
160        if self.y is None:
161            return
162        label = text if text is not None else self.name
163        dy = layout.parseUnit(dy, implicitUnit="mm")
164        if edge == "top":
165            label_y = self.y + dy
166        else:
167            label_y = self.y - fontProps.lineHeight - dy
168        with drawBot.savedState():
169            fontProps.apply()
170            drawBot.text(label, (x, label_y))
171
172
173@dataclass
174class _BoundsSeed:
175    """Internal text-bound seed used before line clustering.
176
177    Attributes:
178        text: Seed substring.
179        bounds: Seed bounds from DrawBot as ``(x, y, w, h)``.
180        baselineY: Absolute baseline Y for this seed.
181        index: Original order index from DrawBot output.
182    """
183
184    text: str
185    bounds: tuple[float, float, float, float]
186    baselineY: float
187    index: int
188
189
190# Declare external DB class shape for type checking
191class DBBoundsItem(Protocol):
192    """Protocol for DrawBot `textBoxCharacterBounds()` item."""
193
194    formattedSubString: str
195    bounds: tuple[float, float, float, float]
196    baselineOffset: float
197
198
199class KMetrics:
200    """Compute and expose vertical metric lines for rendered text.
201
202    The class reads OpenType vertical metrics from a font file, scales them to a
203    target font size, and combines them with DrawBot bounds to produce line-based
204    ``MetricBlock`` objects suitable for drawing overlays.
205    """
206
207    def __init__(
208        self,
209        fontPath: str,
210        fontSize: Optional[float] = None,
211        threshold: float = layout.mm(2),
212    ):
213        """
214        A class to extract and calculate key vertical metrics from a font.
215
216        Args:
217            fontPath (str): Path to the font file.
218            fontSize (float | None): Optional type size in points. If not provided,
219                call setFontSize() before accessing scaled metrics.
220            threshold (float): Minimum distance in points to consider two metrics
221                as distinct; higher number means more aggressive simplification.
222        """
223        self._fontName = fonts.getFontName(fontPath)
224        self._font = TTFont(fontPath)
225        self._threshold = threshold
226
227        self._glyphSet = self._font.getGlyphSet()
228        self._cmap = self._font.getBestCmap()
229        self._upm: float = self._font["head"].unitsPerEm
230
231        self._os2 = self._font["OS/2"]
232        self._hhea = self._font["hhea"]
233
234        self._offsets: Dict[MetricName, float] = dict(
235            ascender=self._os2.sTypoAscender,
236            capHeight=self._os2.sCapHeight,
237            midCapHeight=self._os2.sCapHeight / 2,
238            xHeight=self._os2.sxHeight,
239            midXHeight=self._os2.sxHeight / 2,
240            baseline=0,
241            descender=self._os2.sTypoDescender,
242        )
243
244        if fontSize is not None:
245            self.setFontSize(fontSize)
246
247    def printOS2Table(self) -> None:
248        """Print all public attributes from the font OS/2 table for debugging."""
249        for attr in dir(self._os2):
250            if not attr.startswith("_"):
251                print(f"{attr}: {getattr(self._os2, attr)}")
252
253    @property
254    def scale(self) -> float:
255        """Return the current font-size scale factor (points per font unit)."""
256        if not hasattr(self, "_scale"):
257            raise ValueError("Font size not set. Call setFontSize(fontSize) first.")
258        return self._scale
259
260    @scale.setter
261    def scale(self, fontSize: float):
262        """Set internal scale from a font size in points."""
263        self._scale = fontSize / self._upm
264
265    def setFontSize(self, fontSize: float) -> "KMetrics":
266        """Configure font size for all scaled metric calculations.
267
268        Args:
269            fontSize: Target type size in points.
270
271        Returns:
272            ``self`` for fluent usage.
273        """
274        self.scale = fontSize
275        return self
276
277    # ? Naming distinction: offset = relative, position = absolute
278    def getOffsetYByName(self, name: MetricName) -> float:
279        """Get the scaled relative Y offset for a metric name.
280
281        Args:
282            name: One metric key from ``MetricName``.
283
284        Returns:
285            Relative Y offset in points.
286        """
287        try:
288            return self._offsets[name] * self.scale
289        except KeyError:
290            raise ValueError(f"Invalid metric name: {name}")
291
292    def getOffsetExtremaForText(self, text: str) -> tuple[float, float]:
293        """Compute glyph-extrema offsets for a text string.
294
295        Args:
296            text: Text to inspect using glyph outlines.
297
298        Returns:
299            Tuple ``(minY, maxY)`` as relative offsets in points.
300        """
301        min_y, max_y = self.getRawOffsetExtremaForText(text)
302        scale = lambda v: v * self.scale if v is not None else None
303        min_y, max_y = [scale(v) for v in (min_y, max_y)]
304
305        if min_y is None or max_y is None:
306            raise ValueError(f"Could not compute extrema for text: {text!r}")
307
308        return min_y, max_y
309
310    def getRawOffsetExtremaForText(self, text: str) -> tuple[float, float]:
311        """Compute glyph-extrema offsets for a text string in font units.
312
313        Args:
314            text: Text to inspect using glyph outlines.
315
316        Returns:
317            Tuple ``(minY, maxY)`` in font units (unitsPerEm space).
318        """
319        min_y, max_y = None, None
320        for char in text:
321            glyph_name = self._cmap.get(ord(char))
322            if glyph_name is None:
323                if DEBUG:
324                    print(f"No {self._fontName} glyph for character '{char}'")
325                continue
326            pen = BoundsPen(self._glyphSet)
327            self._glyphSet[glyph_name].draw(pen)
328            if pen.bounds:
329                _, yMin, _, yMax = pen.bounds
330                if min_y is None or yMin < min_y:
331                    min_y = yMin
332                if max_y is None or yMax > max_y:
333                    max_y = yMax
334
335        if min_y is None or max_y is None:
336            raise ValueError(f"Could not compute extrema for text: {text!r}")
337
338        return min_y, max_y
339
340    def _epsilon(self) -> float:
341        """Return clustering tolerance used to group seeds by baseline.
342
343        Returns:
344            Tolerance in points.
345        """
346        return max(0.5, self.scale * self._upm * 0.01)
347
348    def _clusterByBaseline(
349        self, seeds: List[_BoundsSeed], epsilon: float
350    ) -> List[List[_BoundsSeed]]:
351        """Group bounds seeds into visual lines by baseline proximity.
352
353        Args:
354            seeds: Flat seed list from DrawBot bounds output.
355            epsilon: Maximum baseline delta allowed in a cluster.
356
357        Returns:
358            List of seed groups, each group representing one visual line.
359        """
360        sortedSeeds = sorted(seeds, key=lambda s: (-s.baselineY, s.bounds[0], s.index))
361        clusters: List[List[_BoundsSeed]] = []
362
363        for seed in sortedSeeds:
364            targetIndex = None
365            for clusterIndex, cluster in enumerate(clusters):
366                clusterBaseline = sum(item.baselineY for item in cluster) / len(cluster)
367                if abs(clusterBaseline - seed.baselineY) <= epsilon:
368                    targetIndex = clusterIndex
369                    break
370
371            if targetIndex is None:
372                clusters.append([seed])
373            else:
374                clusters[targetIndex].append(seed)
375
376        return [
377            sorted(cluster, key=lambda s: (s.bounds[0], s.index))
378            for cluster in clusters
379        ]
380
381    def _toBlock(self, seeds: List[_BoundsSeed]) -> MetricBlock:
382        """Convert one clustered seed group into a ``MetricBlock``.
383
384        Args:
385            seeds: One line-cluster of bounds seeds.
386
387        Returns:
388            A fully populated ``MetricBlock`` with absolute metric lines.
389        """
390        text = "".join(seed.text for seed in seeds)
391
392        minX = min(seed.bounds[0] for seed in seeds)
393        minY = min(seed.bounds[1] for seed in seeds)
394        maxX = max(seed.bounds[0] + seed.bounds[2] for seed in seeds)
395        maxY = max(seed.bounds[1] + seed.bounds[3] for seed in seeds)
396        baselineY = sum(seed.baselineY for seed in seeds) / len(seeds)
397
398        coords = (minX, baselineY, maxX - minX, maxY - minY)
399
400        rawValues: Dict[BlockMetricName, float] = {
401            name: value for name, value in self._offsets.items()
402        }
403        textMinYRaw, textMaxYRaw = self.getRawOffsetExtremaForText(text)
404        rawValues["minY"] = textMinYRaw
405        rawValues["maxY"] = textMaxYRaw
406
407        relativeOffsets: Dict[BlockMetricName, float] = {
408            name: (value * self.scale) for name, value in rawValues.items()
409        }
410
411        sortedOffsets = OrderedDict(
412            sorted(relativeOffsets.items(), key=lambda pair: pair[1] or 0, reverse=True)
413        )
414
415        lines: List[BlockLine] = []
416        for name, offset in sortedOffsets.items():
417            rawValue = rawValues.get(name)
418            if rawValue is None:
419                raise ValueError(
420                    f"Metric '{name}' has no raw value for text block: {text!r}"
421                )
422
423            offsetsExceptThis = [
424                otherOffset
425                for otherName, otherOffset in sortedOffsets.items()
426                if otherName != name and otherOffset is not None
427            ]
428
429            isDistinctFromOther = (
430                all(
431                    abs(offset - otherOffset) > self._threshold
432                    for otherOffset in offsetsExceptThis
433                )
434                if offset is not None
435                else False
436            )
437
438            y = baselineY + offset if offset is not None else None
439            lines.append(
440                BlockLine(
441                    name=name,
442                    y=y,
443                    offset=offset,
444                    isDistinct=isDistinctFromOther,
445                    threshold=self._threshold,
446                    value=rawValue,
447                )
448            )
449
450        return MetricBlock(text=text, coords=coords, lines=lines)
451
452    def getBlocks(
453        self,
454        text: str,
455        coords: tuple,
456    ) -> List[MetricBlock]:
457        """Return metric blocks extracted from rendered text bounds.
458
459        Args:
460            text: Input string rendered in the current DrawBot text context.
461            coords: Text box coordinates ``(x, y, w, h)`` passed to
462                ``drawBot.textBoxCharacterBounds()``.
463
464        Returns:
465            A list of ``MetricBlock`` instances grouped by visual line using
466            baseline clustering.
467        """
468        boundsItems: List[DBBoundsItem] = drawBot.textBoxCharacterBounds(text, coords)
469        seeds = [
470            _BoundsSeed(
471                text=item.formattedSubString,
472                bounds=item.bounds,
473                baselineY=item.bounds[1] + item.baselineOffset,
474                index=index,
475            )
476            for index, item in enumerate(boundsItems)
477            if item.formattedSubString.strip() != ""
478        ]
479
480        if len(seeds) == 0:
481            return []
482
483        groups = self._clusterByBaseline(seeds, epsilon=self._epsilon())
484
485        return [self._toBlock(group) for group in groups]
486
487    def getBlock(
488        self,
489        text: str,
490        coords: tuple,
491    ) -> MetricBlock | None:
492        """Return the first metric block for a rendered text box.
493
494        Args:
495            text: Input string rendered in DrawBot.
496            coords: Text box coordinates ``(x, y, w, h)``.
497
498        Returns:
499            First ``MetricBlock`` from ``getBlocks()`` or ``None`` if no visible
500            seeds are produced.
501        """
502        blocks = self.getBlocks(text=text, coords=coords)
503        return blocks[0] if blocks else None
504
505    def selectLines(
506        self,
507        block: MetricBlock,
508        names: List[BlockMetricName],
509        distinctFrom: Optional[DistinctRules] = None,
510    ) -> Dict[BlockMetricName, BlockLine]:
511        """Select a named subset of lines from a block.
512
513        Args:
514            block: Source ``MetricBlock``.
515            names: Metric names to keep.
516            distinctFrom: Optional mapping ``{lineA: lineB}`` to keep ``lineA``
517                only when distinct from ``lineB``.
518
519        Returns:
520            Mapping of selected names to ``BlockLine``.
521        """
522        return block.select(names=names, distinctFrom=distinctFrom)
MetricName = typing.Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender']

Named font metrics found in OS/2 table.

BlockMetricName = typing.Union[typing.Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], typing.Literal['minY', 'maxY']]

Metric names for lines computed in a visual line block, including computed extrema found in given text.

DistinctRules = typing.Dict[typing.Union[typing.Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], typing.Literal['minY', 'maxY']], typing.Union[typing.Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], typing.Literal['minY', 'maxY']]]

Mapping of line names to other line names for distinctness filtering, meaning 'keep lineA only if distinct from lineB'.

DEBUG = 0
@dataclass
class MetricBlock:
34@dataclass
35class MetricBlock:
36    """Metric information for one rendered text block (typically one visual line).
37
38    Attributes:
39        text: Visible substring represented by this block.
40        coords: Block frame as ``(x, baselineY, width, height)``.
41        lines: Computed metric lines for the block.
42    """
43
44    text: str
45    coords: tuple[float, float, float, float]
46    lines: List["BlockLine"]
47
48    def line(self, name: BlockMetricName, required: bool = False) -> "BlockLine | None":
49        """Return one metric line by name.
50
51        Args:
52            name: Metric key (for example ``baseline`` or ``capHeight``).
53            required: If ``True``, raise ``ValueError`` when missing.
54
55        Returns:
56            The matching ``BlockLine`` or ``None``.
57        """
58        for line in self.lines:
59            if line.name == name:
60                return line
61        if required:
62            raise ValueError(f"Metric {name} not found in block with text: {self.text}")
63        return None
64
65    def select(
66        self,
67        names: List[BlockMetricName],
68        distinctFrom: Optional[DistinctRules] = None,
69    ) -> Dict[BlockMetricName, "BlockLine"]:
70        """Return selected metric lines with optional distinctness filtering.
71
72        Args:
73            names: Metric names to include in the initial selection.
74            distinctFrom: Optional mapping ``{lineA: lineB}`` meaning
75                "keep ``lineA`` only if it is distinct from ``lineB``".
76
77        Returns:
78            Mapping from metric name to ``BlockLine`` for visible lines.
79        """
80        selected = {
81            line.name: line
82            for line in self.lines
83            if line.name in names and line.y is not None
84        }
85
86        if not distinctFrom:
87            return selected
88
89        for lineName, compareTo in distinctFrom.items():
90            line = selected.get(lineName)
91            if line is None:
92                continue
93            if not line.isDistinctFrom(selected.get(compareTo)):
94                selected.pop(lineName, None)
95
96        return selected

Metric information for one rendered text block (typically one visual line).

Attributes:
  • text: Visible substring represented by this block.
  • coords: Block frame as (x, baselineY, width, height).
  • lines: Computed metric lines for the block.
MetricBlock( text: str, coords: tuple[float, float, float, float], lines: List[BlockLine])
text: str
coords: tuple[float, float, float, float]
lines: List[BlockLine]
def line( self, name: Union[Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], Literal['minY', 'maxY']], required: bool = False) -> BlockLine | None:
48    def line(self, name: BlockMetricName, required: bool = False) -> "BlockLine | None":
49        """Return one metric line by name.
50
51        Args:
52            name: Metric key (for example ``baseline`` or ``capHeight``).
53            required: If ``True``, raise ``ValueError`` when missing.
54
55        Returns:
56            The matching ``BlockLine`` or ``None``.
57        """
58        for line in self.lines:
59            if line.name == name:
60                return line
61        if required:
62            raise ValueError(f"Metric {name} not found in block with text: {self.text}")
63        return None

Return one metric line by name.

Arguments:
  • name: Metric key (for example baseline or capHeight).
  • required: If True, raise ValueError when missing.
Returns:

The matching BlockLine or None.

def select( self, names: List[Union[Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], Literal['minY', 'maxY']]], distinctFrom: Optional[Dict[Union[Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], Literal['minY', 'maxY']], Union[Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], Literal['minY', 'maxY']]]] = None) -> Dict[Union[Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], Literal['minY', 'maxY']], BlockLine]:
65    def select(
66        self,
67        names: List[BlockMetricName],
68        distinctFrom: Optional[DistinctRules] = None,
69    ) -> Dict[BlockMetricName, "BlockLine"]:
70        """Return selected metric lines with optional distinctness filtering.
71
72        Args:
73            names: Metric names to include in the initial selection.
74            distinctFrom: Optional mapping ``{lineA: lineB}`` meaning
75                "keep ``lineA`` only if it is distinct from ``lineB``".
76
77        Returns:
78            Mapping from metric name to ``BlockLine`` for visible lines.
79        """
80        selected = {
81            line.name: line
82            for line in self.lines
83            if line.name in names and line.y is not None
84        }
85
86        if not distinctFrom:
87            return selected
88
89        for lineName, compareTo in distinctFrom.items():
90            line = selected.get(lineName)
91            if line is None:
92                continue
93            if not line.isDistinctFrom(selected.get(compareTo)):
94                selected.pop(lineName, None)
95
96        return selected

Return selected metric lines with optional distinctness filtering.

Arguments:
  • names: Metric names to include in the initial selection.
  • distinctFrom: Optional mapping {lineA: lineB} meaning "keep lineA only if it is distinct from lineB".
Returns:

Mapping from metric name to BlockLine for visible lines.

@dataclass
class BlockLine:
 99@dataclass
100class BlockLine:
101    """One horizontal metric line belonging to a ``MetricBlock``.
102
103    Attributes:
104        name: Metric label.
105        y: Absolute Y coordinate in page space.
106        offset: Relative Y offset from baseline.
107        isDistinct: Whether this line is distinct from all sibling lines in block.
108        threshold: Distinctness threshold in points.
109        value: Raw metric value in font units (unitsPerEm space).
110    """
111
112    name: BlockMetricName
113    y: float | None
114    offset: float | None
115    isDistinct: bool
116    threshold: float
117    value: float
118
119    def __post_init__(self) -> None:
120        if self.value is None:
121            raise ValueError(f"BlockLine '{self.name}' value cannot be None")
122
123    def isIn(self, names: List[BlockMetricName]) -> bool:
124        """Check whether this line name is present in a name list."""
125        return self.name in names
126
127    def isDistinctFrom(self, other: "BlockLine | None") -> bool:
128        """Compare this line with another line using the line threshold.
129
130        Args:
131            other: Another ``BlockLine`` (or ``None``).
132
133        Returns:
134            ``True`` if both offsets exist and differ more than ``threshold``.
135        """
136        if self.offset is None or other is None or other.offset is None:
137            return False
138        return abs(self.offset - other.offset) > self.threshold
139
140    def annotate(
141        self,
142        x: float,
143        fontProps: "DFontProps",
144        *,
145        dy: layout.UnitSource = 0,
146        edge: Literal["top", "bottom"] = "top",
147        text: str | None = None,
148    ) -> None:
149        """Draw a text label for this metric line.
150
151        Args:
152            x: Horizontal position for the label baseline.
153            fontProps: Font properties applied when rendering the label.
154            dy: Additional Y offset (implicit unit: mm). Positive values increase the gap
155                between the label and the metric line.
156            edge: ``"top"`` places the label above the line (text
157                baseline sits at ``self.y + dy``); ``"bottom"`` places
158                the label below (text floats under the line).
159            text: Label string. Defaults to ``self.name``.
160        """
161        if self.y is None:
162            return
163        label = text if text is not None else self.name
164        dy = layout.parseUnit(dy, implicitUnit="mm")
165        if edge == "top":
166            label_y = self.y + dy
167        else:
168            label_y = self.y - fontProps.lineHeight - dy
169        with drawBot.savedState():
170            fontProps.apply()
171            drawBot.text(label, (x, label_y))

One horizontal metric line belonging to a MetricBlock.

Attributes:
  • name: Metric label.
  • y: Absolute Y coordinate in page space.
  • offset: Relative Y offset from baseline.
  • isDistinct: Whether this line is distinct from all sibling lines in block.
  • threshold: Distinctness threshold in points.
  • value: Raw metric value in font units (unitsPerEm space).
BlockLine( name: Union[Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], Literal['minY', 'maxY']], y: float | None, offset: float | None, isDistinct: bool, threshold: float, value: float)
name: Union[Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], Literal['minY', 'maxY']]
y: float | None
offset: float | None
isDistinct: bool
threshold: float
value: float
def isIn( self, names: List[Union[Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], Literal['minY', 'maxY']]]) -> bool:
123    def isIn(self, names: List[BlockMetricName]) -> bool:
124        """Check whether this line name is present in a name list."""
125        return self.name in names

Check whether this line name is present in a name list.

def isDistinctFrom(self, other: BlockLine | None) -> bool:
127    def isDistinctFrom(self, other: "BlockLine | None") -> bool:
128        """Compare this line with another line using the line threshold.
129
130        Args:
131            other: Another ``BlockLine`` (or ``None``).
132
133        Returns:
134            ``True`` if both offsets exist and differ more than ``threshold``.
135        """
136        if self.offset is None or other is None or other.offset is None:
137            return False
138        return abs(self.offset - other.offset) > self.threshold

Compare this line with another line using the line threshold.

Arguments:
Returns:

True if both offsets exist and differ more than threshold.

def annotate( self, x: float, fontProps: datatypes.data_fontprops.DFontProps, *, dy: int | float | str | None = 0, edge: Literal['top', 'bottom'] = 'top', text: str | None = None) -> None:
140    def annotate(
141        self,
142        x: float,
143        fontProps: "DFontProps",
144        *,
145        dy: layout.UnitSource = 0,
146        edge: Literal["top", "bottom"] = "top",
147        text: str | None = None,
148    ) -> None:
149        """Draw a text label for this metric line.
150
151        Args:
152            x: Horizontal position for the label baseline.
153            fontProps: Font properties applied when rendering the label.
154            dy: Additional Y offset (implicit unit: mm). Positive values increase the gap
155                between the label and the metric line.
156            edge: ``"top"`` places the label above the line (text
157                baseline sits at ``self.y + dy``); ``"bottom"`` places
158                the label below (text floats under the line).
159            text: Label string. Defaults to ``self.name``.
160        """
161        if self.y is None:
162            return
163        label = text if text is not None else self.name
164        dy = layout.parseUnit(dy, implicitUnit="mm")
165        if edge == "top":
166            label_y = self.y + dy
167        else:
168            label_y = self.y - fontProps.lineHeight - dy
169        with drawBot.savedState():
170            fontProps.apply()
171            drawBot.text(label, (x, label_y))

Draw a text label for this metric line.

Arguments:
  • x: Horizontal position for the label baseline.
  • fontProps: Font properties applied when rendering the label.
  • dy: Additional Y offset (implicit unit: mm). Positive values increase the gap between the label and the metric line.
  • edge: "top" places the label above the line (text baseline sits at self.y + dy); "bottom" places the label below (text floats under the line).
  • text: Label string. Defaults to self.name.
class DBBoundsItem(typing.Protocol):
192class DBBoundsItem(Protocol):
193    """Protocol for DrawBot `textBoxCharacterBounds()` item."""
194
195    formattedSubString: str
196    bounds: tuple[float, float, float, float]
197    baselineOffset: float

Protocol for DrawBot textBoxCharacterBounds() item.

DBBoundsItem(*args, **kwargs)
1771def _no_init_or_replace_init(self, *args, **kwargs):
1772    cls = type(self)
1773
1774    if cls._is_protocol:
1775        raise TypeError('Protocols cannot be instantiated')
1776
1777    # Already using a custom `__init__`. No need to calculate correct
1778    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1779    if cls.__init__ is not _no_init_or_replace_init:
1780        return
1781
1782    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1783    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1784    # searches for a proper new `__init__` in the MRO. The new `__init__`
1785    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1786    # instantiation of the protocol subclass will thus use the new
1787    # `__init__` and no longer call `_no_init_or_replace_init`.
1788    for base in cls.__mro__:
1789        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1790        if init is not _no_init_or_replace_init:
1791            cls.__init__ = init
1792            break
1793    else:
1794        # should not happen
1795        cls.__init__ = object.__init__
1796
1797    cls.__init__(self, *args, **kwargs)
formattedSubString: str
bounds: tuple[float, float, float, float]
baselineOffset: float
class KMetrics:
200class KMetrics:
201    """Compute and expose vertical metric lines for rendered text.
202
203    The class reads OpenType vertical metrics from a font file, scales them to a
204    target font size, and combines them with DrawBot bounds to produce line-based
205    ``MetricBlock`` objects suitable for drawing overlays.
206    """
207
208    def __init__(
209        self,
210        fontPath: str,
211        fontSize: Optional[float] = None,
212        threshold: float = layout.mm(2),
213    ):
214        """
215        A class to extract and calculate key vertical metrics from a font.
216
217        Args:
218            fontPath (str): Path to the font file.
219            fontSize (float | None): Optional type size in points. If not provided,
220                call setFontSize() before accessing scaled metrics.
221            threshold (float): Minimum distance in points to consider two metrics
222                as distinct; higher number means more aggressive simplification.
223        """
224        self._fontName = fonts.getFontName(fontPath)
225        self._font = TTFont(fontPath)
226        self._threshold = threshold
227
228        self._glyphSet = self._font.getGlyphSet()
229        self._cmap = self._font.getBestCmap()
230        self._upm: float = self._font["head"].unitsPerEm
231
232        self._os2 = self._font["OS/2"]
233        self._hhea = self._font["hhea"]
234
235        self._offsets: Dict[MetricName, float] = dict(
236            ascender=self._os2.sTypoAscender,
237            capHeight=self._os2.sCapHeight,
238            midCapHeight=self._os2.sCapHeight / 2,
239            xHeight=self._os2.sxHeight,
240            midXHeight=self._os2.sxHeight / 2,
241            baseline=0,
242            descender=self._os2.sTypoDescender,
243        )
244
245        if fontSize is not None:
246            self.setFontSize(fontSize)
247
248    def printOS2Table(self) -> None:
249        """Print all public attributes from the font OS/2 table for debugging."""
250        for attr in dir(self._os2):
251            if not attr.startswith("_"):
252                print(f"{attr}: {getattr(self._os2, attr)}")
253
254    @property
255    def scale(self) -> float:
256        """Return the current font-size scale factor (points per font unit)."""
257        if not hasattr(self, "_scale"):
258            raise ValueError("Font size not set. Call setFontSize(fontSize) first.")
259        return self._scale
260
261    @scale.setter
262    def scale(self, fontSize: float):
263        """Set internal scale from a font size in points."""
264        self._scale = fontSize / self._upm
265
266    def setFontSize(self, fontSize: float) -> "KMetrics":
267        """Configure font size for all scaled metric calculations.
268
269        Args:
270            fontSize: Target type size in points.
271
272        Returns:
273            ``self`` for fluent usage.
274        """
275        self.scale = fontSize
276        return self
277
278    # ? Naming distinction: offset = relative, position = absolute
279    def getOffsetYByName(self, name: MetricName) -> float:
280        """Get the scaled relative Y offset for a metric name.
281
282        Args:
283            name: One metric key from ``MetricName``.
284
285        Returns:
286            Relative Y offset in points.
287        """
288        try:
289            return self._offsets[name] * self.scale
290        except KeyError:
291            raise ValueError(f"Invalid metric name: {name}")
292
293    def getOffsetExtremaForText(self, text: str) -> tuple[float, float]:
294        """Compute glyph-extrema offsets for a text string.
295
296        Args:
297            text: Text to inspect using glyph outlines.
298
299        Returns:
300            Tuple ``(minY, maxY)`` as relative offsets in points.
301        """
302        min_y, max_y = self.getRawOffsetExtremaForText(text)
303        scale = lambda v: v * self.scale if v is not None else None
304        min_y, max_y = [scale(v) for v in (min_y, max_y)]
305
306        if min_y is None or max_y is None:
307            raise ValueError(f"Could not compute extrema for text: {text!r}")
308
309        return min_y, max_y
310
311    def getRawOffsetExtremaForText(self, text: str) -> tuple[float, float]:
312        """Compute glyph-extrema offsets for a text string in font units.
313
314        Args:
315            text: Text to inspect using glyph outlines.
316
317        Returns:
318            Tuple ``(minY, maxY)`` in font units (unitsPerEm space).
319        """
320        min_y, max_y = None, None
321        for char in text:
322            glyph_name = self._cmap.get(ord(char))
323            if glyph_name is None:
324                if DEBUG:
325                    print(f"No {self._fontName} glyph for character '{char}'")
326                continue
327            pen = BoundsPen(self._glyphSet)
328            self._glyphSet[glyph_name].draw(pen)
329            if pen.bounds:
330                _, yMin, _, yMax = pen.bounds
331                if min_y is None or yMin < min_y:
332                    min_y = yMin
333                if max_y is None or yMax > max_y:
334                    max_y = yMax
335
336        if min_y is None or max_y is None:
337            raise ValueError(f"Could not compute extrema for text: {text!r}")
338
339        return min_y, max_y
340
341    def _epsilon(self) -> float:
342        """Return clustering tolerance used to group seeds by baseline.
343
344        Returns:
345            Tolerance in points.
346        """
347        return max(0.5, self.scale * self._upm * 0.01)
348
349    def _clusterByBaseline(
350        self, seeds: List[_BoundsSeed], epsilon: float
351    ) -> List[List[_BoundsSeed]]:
352        """Group bounds seeds into visual lines by baseline proximity.
353
354        Args:
355            seeds: Flat seed list from DrawBot bounds output.
356            epsilon: Maximum baseline delta allowed in a cluster.
357
358        Returns:
359            List of seed groups, each group representing one visual line.
360        """
361        sortedSeeds = sorted(seeds, key=lambda s: (-s.baselineY, s.bounds[0], s.index))
362        clusters: List[List[_BoundsSeed]] = []
363
364        for seed in sortedSeeds:
365            targetIndex = None
366            for clusterIndex, cluster in enumerate(clusters):
367                clusterBaseline = sum(item.baselineY for item in cluster) / len(cluster)
368                if abs(clusterBaseline - seed.baselineY) <= epsilon:
369                    targetIndex = clusterIndex
370                    break
371
372            if targetIndex is None:
373                clusters.append([seed])
374            else:
375                clusters[targetIndex].append(seed)
376
377        return [
378            sorted(cluster, key=lambda s: (s.bounds[0], s.index))
379            for cluster in clusters
380        ]
381
382    def _toBlock(self, seeds: List[_BoundsSeed]) -> MetricBlock:
383        """Convert one clustered seed group into a ``MetricBlock``.
384
385        Args:
386            seeds: One line-cluster of bounds seeds.
387
388        Returns:
389            A fully populated ``MetricBlock`` with absolute metric lines.
390        """
391        text = "".join(seed.text for seed in seeds)
392
393        minX = min(seed.bounds[0] for seed in seeds)
394        minY = min(seed.bounds[1] for seed in seeds)
395        maxX = max(seed.bounds[0] + seed.bounds[2] for seed in seeds)
396        maxY = max(seed.bounds[1] + seed.bounds[3] for seed in seeds)
397        baselineY = sum(seed.baselineY for seed in seeds) / len(seeds)
398
399        coords = (minX, baselineY, maxX - minX, maxY - minY)
400
401        rawValues: Dict[BlockMetricName, float] = {
402            name: value for name, value in self._offsets.items()
403        }
404        textMinYRaw, textMaxYRaw = self.getRawOffsetExtremaForText(text)
405        rawValues["minY"] = textMinYRaw
406        rawValues["maxY"] = textMaxYRaw
407
408        relativeOffsets: Dict[BlockMetricName, float] = {
409            name: (value * self.scale) for name, value in rawValues.items()
410        }
411
412        sortedOffsets = OrderedDict(
413            sorted(relativeOffsets.items(), key=lambda pair: pair[1] or 0, reverse=True)
414        )
415
416        lines: List[BlockLine] = []
417        for name, offset in sortedOffsets.items():
418            rawValue = rawValues.get(name)
419            if rawValue is None:
420                raise ValueError(
421                    f"Metric '{name}' has no raw value for text block: {text!r}"
422                )
423
424            offsetsExceptThis = [
425                otherOffset
426                for otherName, otherOffset in sortedOffsets.items()
427                if otherName != name and otherOffset is not None
428            ]
429
430            isDistinctFromOther = (
431                all(
432                    abs(offset - otherOffset) > self._threshold
433                    for otherOffset in offsetsExceptThis
434                )
435                if offset is not None
436                else False
437            )
438
439            y = baselineY + offset if offset is not None else None
440            lines.append(
441                BlockLine(
442                    name=name,
443                    y=y,
444                    offset=offset,
445                    isDistinct=isDistinctFromOther,
446                    threshold=self._threshold,
447                    value=rawValue,
448                )
449            )
450
451        return MetricBlock(text=text, coords=coords, lines=lines)
452
453    def getBlocks(
454        self,
455        text: str,
456        coords: tuple,
457    ) -> List[MetricBlock]:
458        """Return metric blocks extracted from rendered text bounds.
459
460        Args:
461            text: Input string rendered in the current DrawBot text context.
462            coords: Text box coordinates ``(x, y, w, h)`` passed to
463                ``drawBot.textBoxCharacterBounds()``.
464
465        Returns:
466            A list of ``MetricBlock`` instances grouped by visual line using
467            baseline clustering.
468        """
469        boundsItems: List[DBBoundsItem] = drawBot.textBoxCharacterBounds(text, coords)
470        seeds = [
471            _BoundsSeed(
472                text=item.formattedSubString,
473                bounds=item.bounds,
474                baselineY=item.bounds[1] + item.baselineOffset,
475                index=index,
476            )
477            for index, item in enumerate(boundsItems)
478            if item.formattedSubString.strip() != ""
479        ]
480
481        if len(seeds) == 0:
482            return []
483
484        groups = self._clusterByBaseline(seeds, epsilon=self._epsilon())
485
486        return [self._toBlock(group) for group in groups]
487
488    def getBlock(
489        self,
490        text: str,
491        coords: tuple,
492    ) -> MetricBlock | None:
493        """Return the first metric block for a rendered text box.
494
495        Args:
496            text: Input string rendered in DrawBot.
497            coords: Text box coordinates ``(x, y, w, h)``.
498
499        Returns:
500            First ``MetricBlock`` from ``getBlocks()`` or ``None`` if no visible
501            seeds are produced.
502        """
503        blocks = self.getBlocks(text=text, coords=coords)
504        return blocks[0] if blocks else None
505
506    def selectLines(
507        self,
508        block: MetricBlock,
509        names: List[BlockMetricName],
510        distinctFrom: Optional[DistinctRules] = None,
511    ) -> Dict[BlockMetricName, BlockLine]:
512        """Select a named subset of lines from a block.
513
514        Args:
515            block: Source ``MetricBlock``.
516            names: Metric names to keep.
517            distinctFrom: Optional mapping ``{lineA: lineB}`` to keep ``lineA``
518                only when distinct from ``lineB``.
519
520        Returns:
521            Mapping of selected names to ``BlockLine``.
522        """
523        return block.select(names=names, distinctFrom=distinctFrom)

Compute and expose vertical metric lines for rendered text.

The class reads OpenType vertical metrics from a font file, scales them to a target font size, and combines them with DrawBot bounds to produce line-based MetricBlock objects suitable for drawing overlays.

KMetrics( fontPath: str, fontSize: Optional[float] = None, threshold: float = 5.669291338582678)
208    def __init__(
209        self,
210        fontPath: str,
211        fontSize: Optional[float] = None,
212        threshold: float = layout.mm(2),
213    ):
214        """
215        A class to extract and calculate key vertical metrics from a font.
216
217        Args:
218            fontPath (str): Path to the font file.
219            fontSize (float | None): Optional type size in points. If not provided,
220                call setFontSize() before accessing scaled metrics.
221            threshold (float): Minimum distance in points to consider two metrics
222                as distinct; higher number means more aggressive simplification.
223        """
224        self._fontName = fonts.getFontName(fontPath)
225        self._font = TTFont(fontPath)
226        self._threshold = threshold
227
228        self._glyphSet = self._font.getGlyphSet()
229        self._cmap = self._font.getBestCmap()
230        self._upm: float = self._font["head"].unitsPerEm
231
232        self._os2 = self._font["OS/2"]
233        self._hhea = self._font["hhea"]
234
235        self._offsets: Dict[MetricName, float] = dict(
236            ascender=self._os2.sTypoAscender,
237            capHeight=self._os2.sCapHeight,
238            midCapHeight=self._os2.sCapHeight / 2,
239            xHeight=self._os2.sxHeight,
240            midXHeight=self._os2.sxHeight / 2,
241            baseline=0,
242            descender=self._os2.sTypoDescender,
243        )
244
245        if fontSize is not None:
246            self.setFontSize(fontSize)

A class to extract and calculate key vertical metrics from a font.

Arguments:
  • fontPath (str): Path to the font file.
  • fontSize (float | None): Optional type size in points. If not provided, call setFontSize() before accessing scaled metrics.
  • threshold (float): Minimum distance in points to consider two metrics as distinct; higher number means more aggressive simplification.
def printOS2Table(self) -> None:
248    def printOS2Table(self) -> None:
249        """Print all public attributes from the font OS/2 table for debugging."""
250        for attr in dir(self._os2):
251            if not attr.startswith("_"):
252                print(f"{attr}: {getattr(self._os2, attr)}")

Print all public attributes from the font OS/2 table for debugging.

scale: float
254    @property
255    def scale(self) -> float:
256        """Return the current font-size scale factor (points per font unit)."""
257        if not hasattr(self, "_scale"):
258            raise ValueError("Font size not set. Call setFontSize(fontSize) first.")
259        return self._scale

Return the current font-size scale factor (points per font unit).

def setFontSize(self, fontSize: float) -> KMetrics:
266    def setFontSize(self, fontSize: float) -> "KMetrics":
267        """Configure font size for all scaled metric calculations.
268
269        Args:
270            fontSize: Target type size in points.
271
272        Returns:
273            ``self`` for fluent usage.
274        """
275        self.scale = fontSize
276        return self

Configure font size for all scaled metric calculations.

Arguments:
  • fontSize: Target type size in points.
Returns:

self for fluent usage.

def getOffsetYByName( self, name: Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender']) -> float:
279    def getOffsetYByName(self, name: MetricName) -> float:
280        """Get the scaled relative Y offset for a metric name.
281
282        Args:
283            name: One metric key from ``MetricName``.
284
285        Returns:
286            Relative Y offset in points.
287        """
288        try:
289            return self._offsets[name] * self.scale
290        except KeyError:
291            raise ValueError(f"Invalid metric name: {name}")

Get the scaled relative Y offset for a metric name.

Arguments:
Returns:

Relative Y offset in points.

def getOffsetExtremaForText(self, text: str) -> tuple[float, float]:
293    def getOffsetExtremaForText(self, text: str) -> tuple[float, float]:
294        """Compute glyph-extrema offsets for a text string.
295
296        Args:
297            text: Text to inspect using glyph outlines.
298
299        Returns:
300            Tuple ``(minY, maxY)`` as relative offsets in points.
301        """
302        min_y, max_y = self.getRawOffsetExtremaForText(text)
303        scale = lambda v: v * self.scale if v is not None else None
304        min_y, max_y = [scale(v) for v in (min_y, max_y)]
305
306        if min_y is None or max_y is None:
307            raise ValueError(f"Could not compute extrema for text: {text!r}")
308
309        return min_y, max_y

Compute glyph-extrema offsets for a text string.

Arguments:
  • text: Text to inspect using glyph outlines.
Returns:

Tuple (minY, maxY) as relative offsets in points.

def getRawOffsetExtremaForText(self, text: str) -> tuple[float, float]:
311    def getRawOffsetExtremaForText(self, text: str) -> tuple[float, float]:
312        """Compute glyph-extrema offsets for a text string in font units.
313
314        Args:
315            text: Text to inspect using glyph outlines.
316
317        Returns:
318            Tuple ``(minY, maxY)`` in font units (unitsPerEm space).
319        """
320        min_y, max_y = None, None
321        for char in text:
322            glyph_name = self._cmap.get(ord(char))
323            if glyph_name is None:
324                if DEBUG:
325                    print(f"No {self._fontName} glyph for character '{char}'")
326                continue
327            pen = BoundsPen(self._glyphSet)
328            self._glyphSet[glyph_name].draw(pen)
329            if pen.bounds:
330                _, yMin, _, yMax = pen.bounds
331                if min_y is None or yMin < min_y:
332                    min_y = yMin
333                if max_y is None or yMax > max_y:
334                    max_y = yMax
335
336        if min_y is None or max_y is None:
337            raise ValueError(f"Could not compute extrema for text: {text!r}")
338
339        return min_y, max_y

Compute glyph-extrema offsets for a text string in font units.

Arguments:
  • text: Text to inspect using glyph outlines.
Returns:

Tuple (minY, maxY) in font units (unitsPerEm space).

def getBlocks(self, text: str, coords: tuple) -> List[MetricBlock]:
453    def getBlocks(
454        self,
455        text: str,
456        coords: tuple,
457    ) -> List[MetricBlock]:
458        """Return metric blocks extracted from rendered text bounds.
459
460        Args:
461            text: Input string rendered in the current DrawBot text context.
462            coords: Text box coordinates ``(x, y, w, h)`` passed to
463                ``drawBot.textBoxCharacterBounds()``.
464
465        Returns:
466            A list of ``MetricBlock`` instances grouped by visual line using
467            baseline clustering.
468        """
469        boundsItems: List[DBBoundsItem] = drawBot.textBoxCharacterBounds(text, coords)
470        seeds = [
471            _BoundsSeed(
472                text=item.formattedSubString,
473                bounds=item.bounds,
474                baselineY=item.bounds[1] + item.baselineOffset,
475                index=index,
476            )
477            for index, item in enumerate(boundsItems)
478            if item.formattedSubString.strip() != ""
479        ]
480
481        if len(seeds) == 0:
482            return []
483
484        groups = self._clusterByBaseline(seeds, epsilon=self._epsilon())
485
486        return [self._toBlock(group) for group in groups]

Return metric blocks extracted from rendered text bounds.

Arguments:
  • text: Input string rendered in the current DrawBot text context.
  • coords: Text box coordinates (x, y, w, h) passed to drawBot.textBoxCharacterBounds().
Returns:

A list of MetricBlock instances grouped by visual line using baseline clustering.

def getBlock(self, text: str, coords: tuple) -> MetricBlock | None:
488    def getBlock(
489        self,
490        text: str,
491        coords: tuple,
492    ) -> MetricBlock | None:
493        """Return the first metric block for a rendered text box.
494
495        Args:
496            text: Input string rendered in DrawBot.
497            coords: Text box coordinates ``(x, y, w, h)``.
498
499        Returns:
500            First ``MetricBlock`` from ``getBlocks()`` or ``None`` if no visible
501            seeds are produced.
502        """
503        blocks = self.getBlocks(text=text, coords=coords)
504        return blocks[0] if blocks else None

Return the first metric block for a rendered text box.

Arguments:
  • text: Input string rendered in DrawBot.
  • coords: Text box coordinates (x, y, w, h).
Returns:

First MetricBlock from getBlocks() or None if no visible seeds are produced.

def selectLines( self, block: MetricBlock, names: List[Union[Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], Literal['minY', 'maxY']]], distinctFrom: Optional[Dict[Union[Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], Literal['minY', 'maxY']], Union[Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], Literal['minY', 'maxY']]]] = None) -> Dict[Union[Literal['ascender', 'capHeight', 'midCapHeight', 'xHeight', 'midXHeight', 'baseline', 'descender'], Literal['minY', 'maxY']], BlockLine]:
506    def selectLines(
507        self,
508        block: MetricBlock,
509        names: List[BlockMetricName],
510        distinctFrom: Optional[DistinctRules] = None,
511    ) -> Dict[BlockMetricName, BlockLine]:
512        """Select a named subset of lines from a block.
513
514        Args:
515            block: Source ``MetricBlock``.
516            names: Metric names to keep.
517            distinctFrom: Optional mapping ``{lineA: lineB}`` to keep ``lineA``
518                only when distinct from ``lineB``.
519
520        Returns:
521            Mapping of selected names to ``BlockLine``.
522        """
523        return block.select(names=names, distinctFrom=distinctFrom)

Select a named subset of lines from a block.

Arguments:
  • block: Source MetricBlock.
  • names: Metric names to keep.
  • distinctFrom: Optional mapping {lineA: lineB} to keep lineA only when distinct from lineB.
Returns:

Mapping of selected names to BlockLine.