classes.c11_family

  1# Delay importing KFont until after KFamily to avoid circular import issues
  2from __future__ import annotations
  3import colorama
  4import random
  5from typing import TypeAlias, Literal
  6from icecream import ic
  7
  8from lib import helpers, fonts as libFonts
  9
 10# Actually uses KFont, otherwise typing.TYPE_CHECKING would suffice to avoid circular import
 11from .c10_font import KFont
 12
 13FontFormat: TypeAlias = Literal["plain", "rich"]
 14"""Output format for fonts: `plain` for file paths, `rich` for KFont instances."""
 15
 16FontOrder: TypeAlias = Literal["ascending", "descending"]
 17"""Used to sort fonts in `KFamily.serve`."""
 18
 19
 20class KFamily:
 21    """
 22    Represents a family of fonts and provides methods for querying and manipulating font collections.
 23
 24    See `classes.c10_font.KFont`.
 25    """
 26
 27    styles: list[str]
 28    """List of plain font paths"""
 29
 30    fonts: list[KFont]
 31    """List of KFonts"""
 32
 33    def __init__(self, fontFolder: str):
 34        """
 35        Initialize a KFamily instance.
 36
 37        Args:
 38            fontFolder: File system path to the folder containing font files.
 39        """
 40        self.styles = libFonts.getFonts(fontFolder)
 41        self.fonts = [KFont(path, self) for path in self.styles]
 42
 43    def __str__(self) -> str:
 44        """
 45        Return a string representation of the KFamily instance.
 46        """
 47        return f"{colorama.Fore.BLACK}{colorama.Back.LIGHTCYAN_EX}KFamily{colorama.Style.RESET_ALL} {', '.join([font.styleName for font in self.fonts])} {colorama.Style.DIM}({self.familyCount} {self.familyNoun})"
 48
 49    @property
 50    def familyName(self) -> str:
 51        """Returns the family name as a string."""
 52        font = self.fonts[0]
 53        return font.familyName
 54
 55    @property
 56    def familyCount(self) -> int:
 57        """Returns the number of fonts in the family."""
 58        return len(self.fonts)
 59
 60    @property
 61    def familyNoun(self) -> str:
 62        """Returns 'font' or 'fonts' depending on the count."""
 63        return "fonts" if self.familyCount > 1 else "font"
 64
 65    @property
 66    def scalar(self) -> int:
 67        """
 68        Returns the length of `classes.c10_font.KFont.styleNumber`.
 69
 70        Example:
 71            FontName-`200`Light => `3`
 72        """
 73        return len(self.fonts[0].styleNumber)
 74
 75    def serve(self, format: FontFormat = "rich", order: FontOrder = "ascending"):
 76        """
 77        Serve the fonts in the specified format and order.
 78
 79        Args:
 80            format: Output format, see `FontFormat`.
 81            order: Order of fonts, either 'ascending' or 'descending'.
 82
 83        Returns:
 84            List of fonts in the specified format and order.
 85        """
 86        items = self.fonts if format == "rich" else self.fonts
 87        return self._reverse(items) if order == "descending" else items
 88
 89    def get(
 90        self,
 91        criteria: list[int] | list[libFonts.FontType],
 92        format: FontFormat = "rich",
 93        reversed: bool = False,
 94        strategy: helpers.Strategy = "pick",
 95    ) -> KFont | list[KFont]:
 96        """
 97        Get fonts matching the given criteria.
 98
 99        Args:
100            criteria: List of weight numbers or font types to match. See `lib.fonts.FontType`.
101            format: Output format, see `FontFormat`.
102            reversed: Whether to reverse the result order.
103            strategy: Strategy for filtering fonts.
104
105        Example:
106            - `[50, 70]` => get 2 fonts
107            - `["upright", "display"]` => get intersection of types
108
109        Returns:
110            A single KFont or a list of KFonts matching the criteria.
111        """
112        criteria = helpers.coerceList(criteria, strict=True)
113        result = []
114
115        # Numeric argument: get individually and merge together
116        # Otherwise create intersection of criteria
117        isAllNumeric = all([isinstance(criterion, int) for criterion in criteria])
118
119        for criterion in criteria:
120            # (1, 4) => get 100, 200, 300, 400
121            if isinstance(criterion, tuple):
122                matches = [
123                    font.path
124                    for font in self.fonts
125                    if helpers.isInRange(*criterion, font.getWeightClass(numeric=True))
126                ]
127            elif isinstance(criterion, int):
128                if len(str(criterion)) > self.scalar:
129                    # Scale down: 5000 => 500
130                    scaled = str(criterion)[: self.scalar]
131                else:
132                    # Scale up: 55 => 550
133                    scaled = f"{criterion:0<{self.scalar}}"
134                matches = [libFonts.findFontByNumber(self.styles, int(scaled))]
135            else:
136                matches = libFonts.filterFonts(
137                    self.styles, criterion, strategy=strategy
138                )
139
140            result.append(matches)
141
142        # [40, 50] => flatten together
143        # ["upright", "thick"] => intersect
144        if isAllNumeric:
145            result = helpers.flatten(result)
146        else:
147            result = helpers.intersect(result)
148
149        if reversed:
150            result = self._reverse(result)
151
152        # KFont or plain font path
153        if format == "plain":
154            result = self._toPlainFonts(result)
155        else:
156            result = self._toRichFonts(result)
157
158        # Return list only for multiple fonts
159        return result[0] if helpers.isListOfOne(result) else result
160
161    def getRandom(
162        self,
163        criteria: libFonts.FontType | list[libFonts.FontType],
164        count=1,
165        strategy: helpers.Strategy = "pick",
166    ) -> KFont | list[KFont]:
167        """
168        Get a random font or fonts matching the given criteria.
169
170        Args:
171            criteria: Font type or list of font types to match.
172            count: Number of random fonts to return.
173            strategy: Strategy for filtering fonts.
174
175        Returns:
176            A single KFont or a list of KFonts.
177        """
178        result = self.get(criteria=criteria, strategy=strategy)
179        # -> str
180        if count == 1:
181            return random.choice(result)
182        # -> str[]
183        else:
184            return random.sample(result, min(len(result), count))
185
186    def getFonts(self, **args) -> list[KFont]:
187        """
188        Shorthand for get() to return `KFont` via rich `FontFormat`.
189
190        Args:
191            **args: Arguments passed to get().
192
193        Returns:
194            List of KFont instances.
195        """
196        return self.get(**args, format="rich")
197
198    def getWeightPair(
199        self, weightNumber: int, format: FontFormat = "rich"
200    ) -> list[KFont]:
201        """
202        Get a pair of fonts for a given weight number.
203
204        Example:
205            `4` => `[40, 45]`
206
207        Args:
208            weightNumber: The weight number to match.
209            format: Output format, either 'plain' or 'rich'.
210
211        Returns:
212            List of KFonts or font paths for the weight pair.
213        """
214        result = [
215            font
216            for font in self.fonts
217            if font.getWeightClass(numeric=True) == weightNumber
218        ]
219
220        return result if format == "rich" else self._toPlainFonts(result)
221
222    def getChunks(
223        self,
224        mode: Literal["weight", "slope"],
225        subset: str | list = None,
226        reversed=False,
227    ) -> list[KFont]:
228        """
229        Get fonts grouped by weight or slope.
230
231        Example:
232            - `weight` => [(100, 150), (200, 250), ...]
233            - `slope`  => [(100, 200, ...), (150, 250, ...)]
234
235        Args:
236            mode: Grouping mode, either 'weight' or 'slope'.
237            subset: Optional subset via `KFamily.get`.
238            reversed: Whether to reverse the order within each group.
239
240        Returns:
241            List of lists of KFonts grouped by the specified mode.
242        """
243        if mode == "slope":
244            criteria = ["upright", "italic"]
245        else:
246            criteria = self._getWeightClasses()
247
248        def _meetsCriterion(font: KFont, criterion):
249            if mode == "slope":
250                return font.isType(criterion)
251            else:
252                return font.getWeightClass() == criterion
253
254        def _filterForCriterion(criterion):
255            filtered = [font for font in fontList if _meetsCriterion(font, criterion)]
256
257            if reversed:
258                filtered.reverse()  # Plain list[str]
259
260            return filtered
261
262        fontList = self.get(subset) if subset else self.fonts
263
264        return helpers.removeFalsy([_filterForCriterion(c) for c in criteria])
265
266    def getRange(self, *args, size: int = 3):
267        """
268        Get a range of evenly spaced fonts matching the given criteria.
269
270        Args:
271            *args: Criteria for selecting fonts. See `KFamily.get()`.
272            size: Number of fonts to return.
273
274        Example:
275            `'upright', 3` => `[10, 50, 90]`
276
277        Returns:
278            List of KFonts spaced evenly across the selection.
279        """
280        result = self.get(*args)
281        return helpers.spaceEvenly(result, size=size)
282
283    def getExtremes(self, *args) -> list[KFont]:
284        """
285        Get the extreme fonts (e.g., lightest and boldest) matching the criteria.
286
287        Args:
288            *args: Criteria for selecting fonts. See `KFamily.get()`.
289
290        Example:
291            - `'upright'` => `[10, 80]`
292            - `'text'` =>    `[40, 60]`
293
294        Returns:
295            List of two KFonts representing the extremes.
296        """
297        return self.getRange(*args, size=2)
298
299    def activate(self):
300        """
301        Activate the first font in the family for use in DrawBot. Useful before `omitMissing()`.
302
303        Returns:
304            The KFamily instance (self).
305        """
306        self.fonts[0].activate()
307        return self
308
309    # Internals
310    def _reverse(self, items: list) -> list:
311        """
312        Reverse the order of the given items, grouping by slope if applicable.
313
314        Args:
315            items: List of fonts or font paths.
316
317        Returns:
318            List of fonts or font paths in reversed order.
319        """
320        arePlainFonts = all([isinstance(item, str) for item in items])
321
322        # Pass in plain format to lib.fonts
323        if not arePlainFonts:
324            items = self._toPlainFonts(items)
325
326        fontsUpright, fontsItalic = libFonts.groupFontsBySlope(items, reverseOrder=1)
327
328        if fontsItalic:
329            items = helpers.flatten(list(zip(fontsUpright, fontsItalic)))
330        else:
331            items = fontsUpright
332
333        # Convert back to original format
334        if not arePlainFonts:
335            items = self._toRichFonts(items)
336
337        return items
338
339    def _getWeightClasses(self):
340        """
341        Get all unique weight classes in the family.
342
343        Example:
344            `[1, 2, 3, 4, ... ]`
345
346        Returns:
347            Sorted list of unique weight classes.
348        """
349        return sorted(set([item.getWeightClass() for item in self.fonts]))
350
351    def _toPlainFonts(self, fonts: list[KFont]) -> list[str]:
352        """
353        Convert a list of KFont instances to a list of font paths.
354
355        Args:
356            fonts: List of KFont instances.
357
358        Returns:
359            List of font file paths as strings.
360        """
361        return [font.path if isinstance(font, KFont) else font for font in fonts]
362
363    def _toRichFonts(self, fonts: list[str]) -> list[KFont]:
364        """
365        Convert a list of font paths to KFont instances.
366
367        Args:
368            fonts: List of font file paths as strings.
369
370        Returns:
371            List of KFont instances.
372        """
373        return [KFont(font, self) if isinstance(font, str) else font for font in fonts]
FontFormat: TypeAlias = Literal['plain', 'rich']

Output format for fonts: plain for file paths, rich for KFont instances.

FontOrder: TypeAlias = Literal['ascending', 'descending']

Used to sort fonts in KFamily.serve.

class KFamily:
 21class KFamily:
 22    """
 23    Represents a family of fonts and provides methods for querying and manipulating font collections.
 24
 25    See `classes.c10_font.KFont`.
 26    """
 27
 28    styles: list[str]
 29    """List of plain font paths"""
 30
 31    fonts: list[KFont]
 32    """List of KFonts"""
 33
 34    def __init__(self, fontFolder: str):
 35        """
 36        Initialize a KFamily instance.
 37
 38        Args:
 39            fontFolder: File system path to the folder containing font files.
 40        """
 41        self.styles = libFonts.getFonts(fontFolder)
 42        self.fonts = [KFont(path, self) for path in self.styles]
 43
 44    def __str__(self) -> str:
 45        """
 46        Return a string representation of the KFamily instance.
 47        """
 48        return f"{colorama.Fore.BLACK}{colorama.Back.LIGHTCYAN_EX}KFamily{colorama.Style.RESET_ALL} {', '.join([font.styleName for font in self.fonts])} {colorama.Style.DIM}({self.familyCount} {self.familyNoun})"
 49
 50    @property
 51    def familyName(self) -> str:
 52        """Returns the family name as a string."""
 53        font = self.fonts[0]
 54        return font.familyName
 55
 56    @property
 57    def familyCount(self) -> int:
 58        """Returns the number of fonts in the family."""
 59        return len(self.fonts)
 60
 61    @property
 62    def familyNoun(self) -> str:
 63        """Returns 'font' or 'fonts' depending on the count."""
 64        return "fonts" if self.familyCount > 1 else "font"
 65
 66    @property
 67    def scalar(self) -> int:
 68        """
 69        Returns the length of `classes.c10_font.KFont.styleNumber`.
 70
 71        Example:
 72            FontName-`200`Light => `3`
 73        """
 74        return len(self.fonts[0].styleNumber)
 75
 76    def serve(self, format: FontFormat = "rich", order: FontOrder = "ascending"):
 77        """
 78        Serve the fonts in the specified format and order.
 79
 80        Args:
 81            format: Output format, see `FontFormat`.
 82            order: Order of fonts, either 'ascending' or 'descending'.
 83
 84        Returns:
 85            List of fonts in the specified format and order.
 86        """
 87        items = self.fonts if format == "rich" else self.fonts
 88        return self._reverse(items) if order == "descending" else items
 89
 90    def get(
 91        self,
 92        criteria: list[int] | list[libFonts.FontType],
 93        format: FontFormat = "rich",
 94        reversed: bool = False,
 95        strategy: helpers.Strategy = "pick",
 96    ) -> KFont | list[KFont]:
 97        """
 98        Get fonts matching the given criteria.
 99
100        Args:
101            criteria: List of weight numbers or font types to match. See `lib.fonts.FontType`.
102            format: Output format, see `FontFormat`.
103            reversed: Whether to reverse the result order.
104            strategy: Strategy for filtering fonts.
105
106        Example:
107            - `[50, 70]` => get 2 fonts
108            - `["upright", "display"]` => get intersection of types
109
110        Returns:
111            A single KFont or a list of KFonts matching the criteria.
112        """
113        criteria = helpers.coerceList(criteria, strict=True)
114        result = []
115
116        # Numeric argument: get individually and merge together
117        # Otherwise create intersection of criteria
118        isAllNumeric = all([isinstance(criterion, int) for criterion in criteria])
119
120        for criterion in criteria:
121            # (1, 4) => get 100, 200, 300, 400
122            if isinstance(criterion, tuple):
123                matches = [
124                    font.path
125                    for font in self.fonts
126                    if helpers.isInRange(*criterion, font.getWeightClass(numeric=True))
127                ]
128            elif isinstance(criterion, int):
129                if len(str(criterion)) > self.scalar:
130                    # Scale down: 5000 => 500
131                    scaled = str(criterion)[: self.scalar]
132                else:
133                    # Scale up: 55 => 550
134                    scaled = f"{criterion:0<{self.scalar}}"
135                matches = [libFonts.findFontByNumber(self.styles, int(scaled))]
136            else:
137                matches = libFonts.filterFonts(
138                    self.styles, criterion, strategy=strategy
139                )
140
141            result.append(matches)
142
143        # [40, 50] => flatten together
144        # ["upright", "thick"] => intersect
145        if isAllNumeric:
146            result = helpers.flatten(result)
147        else:
148            result = helpers.intersect(result)
149
150        if reversed:
151            result = self._reverse(result)
152
153        # KFont or plain font path
154        if format == "plain":
155            result = self._toPlainFonts(result)
156        else:
157            result = self._toRichFonts(result)
158
159        # Return list only for multiple fonts
160        return result[0] if helpers.isListOfOne(result) else result
161
162    def getRandom(
163        self,
164        criteria: libFonts.FontType | list[libFonts.FontType],
165        count=1,
166        strategy: helpers.Strategy = "pick",
167    ) -> KFont | list[KFont]:
168        """
169        Get a random font or fonts matching the given criteria.
170
171        Args:
172            criteria: Font type or list of font types to match.
173            count: Number of random fonts to return.
174            strategy: Strategy for filtering fonts.
175
176        Returns:
177            A single KFont or a list of KFonts.
178        """
179        result = self.get(criteria=criteria, strategy=strategy)
180        # -> str
181        if count == 1:
182            return random.choice(result)
183        # -> str[]
184        else:
185            return random.sample(result, min(len(result), count))
186
187    def getFonts(self, **args) -> list[KFont]:
188        """
189        Shorthand for get() to return `KFont` via rich `FontFormat`.
190
191        Args:
192            **args: Arguments passed to get().
193
194        Returns:
195            List of KFont instances.
196        """
197        return self.get(**args, format="rich")
198
199    def getWeightPair(
200        self, weightNumber: int, format: FontFormat = "rich"
201    ) -> list[KFont]:
202        """
203        Get a pair of fonts for a given weight number.
204
205        Example:
206            `4` => `[40, 45]`
207
208        Args:
209            weightNumber: The weight number to match.
210            format: Output format, either 'plain' or 'rich'.
211
212        Returns:
213            List of KFonts or font paths for the weight pair.
214        """
215        result = [
216            font
217            for font in self.fonts
218            if font.getWeightClass(numeric=True) == weightNumber
219        ]
220
221        return result if format == "rich" else self._toPlainFonts(result)
222
223    def getChunks(
224        self,
225        mode: Literal["weight", "slope"],
226        subset: str | list = None,
227        reversed=False,
228    ) -> list[KFont]:
229        """
230        Get fonts grouped by weight or slope.
231
232        Example:
233            - `weight` => [(100, 150), (200, 250), ...]
234            - `slope`  => [(100, 200, ...), (150, 250, ...)]
235
236        Args:
237            mode: Grouping mode, either 'weight' or 'slope'.
238            subset: Optional subset via `KFamily.get`.
239            reversed: Whether to reverse the order within each group.
240
241        Returns:
242            List of lists of KFonts grouped by the specified mode.
243        """
244        if mode == "slope":
245            criteria = ["upright", "italic"]
246        else:
247            criteria = self._getWeightClasses()
248
249        def _meetsCriterion(font: KFont, criterion):
250            if mode == "slope":
251                return font.isType(criterion)
252            else:
253                return font.getWeightClass() == criterion
254
255        def _filterForCriterion(criterion):
256            filtered = [font for font in fontList if _meetsCriterion(font, criterion)]
257
258            if reversed:
259                filtered.reverse()  # Plain list[str]
260
261            return filtered
262
263        fontList = self.get(subset) if subset else self.fonts
264
265        return helpers.removeFalsy([_filterForCriterion(c) for c in criteria])
266
267    def getRange(self, *args, size: int = 3):
268        """
269        Get a range of evenly spaced fonts matching the given criteria.
270
271        Args:
272            *args: Criteria for selecting fonts. See `KFamily.get()`.
273            size: Number of fonts to return.
274
275        Example:
276            `'upright', 3` => `[10, 50, 90]`
277
278        Returns:
279            List of KFonts spaced evenly across the selection.
280        """
281        result = self.get(*args)
282        return helpers.spaceEvenly(result, size=size)
283
284    def getExtremes(self, *args) -> list[KFont]:
285        """
286        Get the extreme fonts (e.g., lightest and boldest) matching the criteria.
287
288        Args:
289            *args: Criteria for selecting fonts. See `KFamily.get()`.
290
291        Example:
292            - `'upright'` => `[10, 80]`
293            - `'text'` =>    `[40, 60]`
294
295        Returns:
296            List of two KFonts representing the extremes.
297        """
298        return self.getRange(*args, size=2)
299
300    def activate(self):
301        """
302        Activate the first font in the family for use in DrawBot. Useful before `omitMissing()`.
303
304        Returns:
305            The KFamily instance (self).
306        """
307        self.fonts[0].activate()
308        return self
309
310    # Internals
311    def _reverse(self, items: list) -> list:
312        """
313        Reverse the order of the given items, grouping by slope if applicable.
314
315        Args:
316            items: List of fonts or font paths.
317
318        Returns:
319            List of fonts or font paths in reversed order.
320        """
321        arePlainFonts = all([isinstance(item, str) for item in items])
322
323        # Pass in plain format to lib.fonts
324        if not arePlainFonts:
325            items = self._toPlainFonts(items)
326
327        fontsUpright, fontsItalic = libFonts.groupFontsBySlope(items, reverseOrder=1)
328
329        if fontsItalic:
330            items = helpers.flatten(list(zip(fontsUpright, fontsItalic)))
331        else:
332            items = fontsUpright
333
334        # Convert back to original format
335        if not arePlainFonts:
336            items = self._toRichFonts(items)
337
338        return items
339
340    def _getWeightClasses(self):
341        """
342        Get all unique weight classes in the family.
343
344        Example:
345            `[1, 2, 3, 4, ... ]`
346
347        Returns:
348            Sorted list of unique weight classes.
349        """
350        return sorted(set([item.getWeightClass() for item in self.fonts]))
351
352    def _toPlainFonts(self, fonts: list[KFont]) -> list[str]:
353        """
354        Convert a list of KFont instances to a list of font paths.
355
356        Args:
357            fonts: List of KFont instances.
358
359        Returns:
360            List of font file paths as strings.
361        """
362        return [font.path if isinstance(font, KFont) else font for font in fonts]
363
364    def _toRichFonts(self, fonts: list[str]) -> list[KFont]:
365        """
366        Convert a list of font paths to KFont instances.
367
368        Args:
369            fonts: List of font file paths as strings.
370
371        Returns:
372            List of KFont instances.
373        """
374        return [KFont(font, self) if isinstance(font, str) else font for font in fonts]

Represents a family of fonts and provides methods for querying and manipulating font collections.

See classes.c10_font.KFont.

KFamily(fontFolder: str)
34    def __init__(self, fontFolder: str):
35        """
36        Initialize a KFamily instance.
37
38        Args:
39            fontFolder: File system path to the folder containing font files.
40        """
41        self.styles = libFonts.getFonts(fontFolder)
42        self.fonts = [KFont(path, self) for path in self.styles]

Initialize a KFamily instance.

Arguments:
  • fontFolder: File system path to the folder containing font files.
styles: list[str]

List of plain font paths

fonts: list[classes.c10_font.KFont]

List of KFonts

familyName: str
50    @property
51    def familyName(self) -> str:
52        """Returns the family name as a string."""
53        font = self.fonts[0]
54        return font.familyName

Returns the family name as a string.

familyCount: int
56    @property
57    def familyCount(self) -> int:
58        """Returns the number of fonts in the family."""
59        return len(self.fonts)

Returns the number of fonts in the family.

familyNoun: str
61    @property
62    def familyNoun(self) -> str:
63        """Returns 'font' or 'fonts' depending on the count."""
64        return "fonts" if self.familyCount > 1 else "font"

Returns 'font' or 'fonts' depending on the count.

scalar: int
66    @property
67    def scalar(self) -> int:
68        """
69        Returns the length of `classes.c10_font.KFont.styleNumber`.
70
71        Example:
72            FontName-`200`Light => `3`
73        """
74        return len(self.fonts[0].styleNumber)

Returns the length of classes.c10_font.KFont.styleNumber.

Example:

FontName-200Light => 3

def serve( self, format: Literal['plain', 'rich'] = 'rich', order: Literal['ascending', 'descending'] = 'ascending'):
76    def serve(self, format: FontFormat = "rich", order: FontOrder = "ascending"):
77        """
78        Serve the fonts in the specified format and order.
79
80        Args:
81            format: Output format, see `FontFormat`.
82            order: Order of fonts, either 'ascending' or 'descending'.
83
84        Returns:
85            List of fonts in the specified format and order.
86        """
87        items = self.fonts if format == "rich" else self.fonts
88        return self._reverse(items) if order == "descending" else items

Serve the fonts in the specified format and order.

Arguments:
  • format: Output format, see FontFormat.
  • order: Order of fonts, either 'ascending' or 'descending'.
Returns:

List of fonts in the specified format and order.

def get( self, criteria: list[int] | list[typing.Literal['upright', 'italic', 'display', 'text', 'thin', 'thick', 'any']], format: Literal['plain', 'rich'] = 'rich', reversed: bool = False, strategy: Literal['pick', 'omit'] = 'pick') -> classes.c10_font.KFont | list[classes.c10_font.KFont]:
 90    def get(
 91        self,
 92        criteria: list[int] | list[libFonts.FontType],
 93        format: FontFormat = "rich",
 94        reversed: bool = False,
 95        strategy: helpers.Strategy = "pick",
 96    ) -> KFont | list[KFont]:
 97        """
 98        Get fonts matching the given criteria.
 99
100        Args:
101            criteria: List of weight numbers or font types to match. See `lib.fonts.FontType`.
102            format: Output format, see `FontFormat`.
103            reversed: Whether to reverse the result order.
104            strategy: Strategy for filtering fonts.
105
106        Example:
107            - `[50, 70]` => get 2 fonts
108            - `["upright", "display"]` => get intersection of types
109
110        Returns:
111            A single KFont or a list of KFonts matching the criteria.
112        """
113        criteria = helpers.coerceList(criteria, strict=True)
114        result = []
115
116        # Numeric argument: get individually and merge together
117        # Otherwise create intersection of criteria
118        isAllNumeric = all([isinstance(criterion, int) for criterion in criteria])
119
120        for criterion in criteria:
121            # (1, 4) => get 100, 200, 300, 400
122            if isinstance(criterion, tuple):
123                matches = [
124                    font.path
125                    for font in self.fonts
126                    if helpers.isInRange(*criterion, font.getWeightClass(numeric=True))
127                ]
128            elif isinstance(criterion, int):
129                if len(str(criterion)) > self.scalar:
130                    # Scale down: 5000 => 500
131                    scaled = str(criterion)[: self.scalar]
132                else:
133                    # Scale up: 55 => 550
134                    scaled = f"{criterion:0<{self.scalar}}"
135                matches = [libFonts.findFontByNumber(self.styles, int(scaled))]
136            else:
137                matches = libFonts.filterFonts(
138                    self.styles, criterion, strategy=strategy
139                )
140
141            result.append(matches)
142
143        # [40, 50] => flatten together
144        # ["upright", "thick"] => intersect
145        if isAllNumeric:
146            result = helpers.flatten(result)
147        else:
148            result = helpers.intersect(result)
149
150        if reversed:
151            result = self._reverse(result)
152
153        # KFont or plain font path
154        if format == "plain":
155            result = self._toPlainFonts(result)
156        else:
157            result = self._toRichFonts(result)
158
159        # Return list only for multiple fonts
160        return result[0] if helpers.isListOfOne(result) else result

Get fonts matching the given criteria.

Arguments:
  • criteria: List of weight numbers or font types to match. See lib.fonts.FontType.
  • format: Output format, see FontFormat.
  • reversed: Whether to reverse the result order.
  • strategy: Strategy for filtering fonts.
Example:
  • [50, 70] => get 2 fonts
  • ["upright", "display"] => get intersection of types
Returns:

A single KFont or a list of KFonts matching the criteria.

def getRandom( self, criteria: Union[Literal['upright', 'italic', 'display', 'text', 'thin', 'thick', 'any'], list[Literal['upright', 'italic', 'display', 'text', 'thin', 'thick', 'any']]], count=1, strategy: Literal['pick', 'omit'] = 'pick') -> classes.c10_font.KFont | list[classes.c10_font.KFont]:
162    def getRandom(
163        self,
164        criteria: libFonts.FontType | list[libFonts.FontType],
165        count=1,
166        strategy: helpers.Strategy = "pick",
167    ) -> KFont | list[KFont]:
168        """
169        Get a random font or fonts matching the given criteria.
170
171        Args:
172            criteria: Font type or list of font types to match.
173            count: Number of random fonts to return.
174            strategy: Strategy for filtering fonts.
175
176        Returns:
177            A single KFont or a list of KFonts.
178        """
179        result = self.get(criteria=criteria, strategy=strategy)
180        # -> str
181        if count == 1:
182            return random.choice(result)
183        # -> str[]
184        else:
185            return random.sample(result, min(len(result), count))

Get a random font or fonts matching the given criteria.

Arguments:
  • criteria: Font type or list of font types to match.
  • count: Number of random fonts to return.
  • strategy: Strategy for filtering fonts.
Returns:

A single KFont or a list of KFonts.

def getFonts(self, **args) -> list[classes.c10_font.KFont]:
187    def getFonts(self, **args) -> list[KFont]:
188        """
189        Shorthand for get() to return `KFont` via rich `FontFormat`.
190
191        Args:
192            **args: Arguments passed to get().
193
194        Returns:
195            List of KFont instances.
196        """
197        return self.get(**args, format="rich")

Shorthand for get() to return KFont via rich FontFormat.

Arguments:
  • **args: Arguments passed to get().
Returns:

List of KFont instances.

def getWeightPair( self, weightNumber: int, format: Literal['plain', 'rich'] = 'rich') -> list[classes.c10_font.KFont]:
199    def getWeightPair(
200        self, weightNumber: int, format: FontFormat = "rich"
201    ) -> list[KFont]:
202        """
203        Get a pair of fonts for a given weight number.
204
205        Example:
206            `4` => `[40, 45]`
207
208        Args:
209            weightNumber: The weight number to match.
210            format: Output format, either 'plain' or 'rich'.
211
212        Returns:
213            List of KFonts or font paths for the weight pair.
214        """
215        result = [
216            font
217            for font in self.fonts
218            if font.getWeightClass(numeric=True) == weightNumber
219        ]
220
221        return result if format == "rich" else self._toPlainFonts(result)

Get a pair of fonts for a given weight number.

Example:

4 => [40, 45]

Arguments:
  • weightNumber: The weight number to match.
  • format: Output format, either 'plain' or 'rich'.
Returns:

List of KFonts or font paths for the weight pair.

def getChunks( self, mode: Literal['weight', 'slope'], subset: str | list = None, reversed=False) -> list[classes.c10_font.KFont]:
223    def getChunks(
224        self,
225        mode: Literal["weight", "slope"],
226        subset: str | list = None,
227        reversed=False,
228    ) -> list[KFont]:
229        """
230        Get fonts grouped by weight or slope.
231
232        Example:
233            - `weight` => [(100, 150), (200, 250), ...]
234            - `slope`  => [(100, 200, ...), (150, 250, ...)]
235
236        Args:
237            mode: Grouping mode, either 'weight' or 'slope'.
238            subset: Optional subset via `KFamily.get`.
239            reversed: Whether to reverse the order within each group.
240
241        Returns:
242            List of lists of KFonts grouped by the specified mode.
243        """
244        if mode == "slope":
245            criteria = ["upright", "italic"]
246        else:
247            criteria = self._getWeightClasses()
248
249        def _meetsCriterion(font: KFont, criterion):
250            if mode == "slope":
251                return font.isType(criterion)
252            else:
253                return font.getWeightClass() == criterion
254
255        def _filterForCriterion(criterion):
256            filtered = [font for font in fontList if _meetsCriterion(font, criterion)]
257
258            if reversed:
259                filtered.reverse()  # Plain list[str]
260
261            return filtered
262
263        fontList = self.get(subset) if subset else self.fonts
264
265        return helpers.removeFalsy([_filterForCriterion(c) for c in criteria])

Get fonts grouped by weight or slope.

Example:
  • weight => [(100, 150), (200, 250), ...]
  • slope => [(100, 200, ...), (150, 250, ...)]
Arguments:
  • mode: Grouping mode, either 'weight' or 'slope'.
  • subset: Optional subset via KFamily.get.
  • reversed: Whether to reverse the order within each group.
Returns:

List of lists of KFonts grouped by the specified mode.

def getRange(self, *args, size: int = 3):
267    def getRange(self, *args, size: int = 3):
268        """
269        Get a range of evenly spaced fonts matching the given criteria.
270
271        Args:
272            *args: Criteria for selecting fonts. See `KFamily.get()`.
273            size: Number of fonts to return.
274
275        Example:
276            `'upright', 3` => `[10, 50, 90]`
277
278        Returns:
279            List of KFonts spaced evenly across the selection.
280        """
281        result = self.get(*args)
282        return helpers.spaceEvenly(result, size=size)

Get a range of evenly spaced fonts matching the given criteria.

Arguments:
  • *args: Criteria for selecting fonts. See KFamily.get().
  • size: Number of fonts to return.
Example:

'upright', 3 => [10, 50, 90]

Returns:

List of KFonts spaced evenly across the selection.

def getExtremes(self, *args) -> list[classes.c10_font.KFont]:
284    def getExtremes(self, *args) -> list[KFont]:
285        """
286        Get the extreme fonts (e.g., lightest and boldest) matching the criteria.
287
288        Args:
289            *args: Criteria for selecting fonts. See `KFamily.get()`.
290
291        Example:
292            - `'upright'` => `[10, 80]`
293            - `'text'` =>    `[40, 60]`
294
295        Returns:
296            List of two KFonts representing the extremes.
297        """
298        return self.getRange(*args, size=2)

Get the extreme fonts (e.g., lightest and boldest) matching the criteria.

Arguments:
Example:
  • 'upright' => [10, 80]
  • 'text' => [40, 60]
Returns:

List of two KFonts representing the extremes.

def activate(self):
300    def activate(self):
301        """
302        Activate the first font in the family for use in DrawBot. Useful before `omitMissing()`.
303
304        Returns:
305            The KFamily instance (self).
306        """
307        self.fonts[0].activate()
308        return self

Activate the first font in the family for use in DrawBot. Useful before omitMissing().

Returns:

The KFamily instance (self).