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 getFontsByNames(self, names: list[str]) -> list[KFont]:
162        """
163        Get fonts by style names.
164
165        Args:
166            names: List of style names to match.
167
168        Returns:
169            List of KFont instances matching the style names.
170        """
171        names = helpers.coerceList(names)
172        names = [name.casefold() for name in names]
173        result = [font for font in self.fonts if font.styleName.casefold() in names]
174        return result
175
176    def getRandom(
177        self,
178        criteria: libFonts.FontType | list[libFonts.FontType],
179        count=1,
180        strategy: helpers.Strategy = "pick",
181    ) -> KFont | list[KFont]:
182        """
183        Get a random font or fonts matching the given criteria.
184
185        Args:
186            criteria: Font type or list of font types to match.
187            count: Number of random fonts to return.
188            strategy: Strategy for filtering fonts.
189
190        Returns:
191            A single KFont or a list of KFonts.
192        """
193        result = self.get(criteria=criteria, strategy=strategy)
194        # -> str
195        if count == 1:
196            return random.choice(result)
197        # -> str[]
198        else:
199            return random.sample(result, min(len(result), count))
200
201    def getFonts(self, **args) -> KFont | list[KFont]:
202        """
203        Shorthand for get() to return `KFont` via rich `FontFormat`.
204
205        Args:
206            **args: Arguments passed to get().
207
208        Returns:
209            KFont or list of KFont instances.
210        """
211        return self.get(**args, format="rich")
212
213    def getWeightPair(
214        self, weightNumber: int, format: FontFormat = "rich"
215    ) -> list[KFont]:
216        """
217        Get a pair of fonts for a given weight number.
218
219        Example:
220            `4` => `[40, 45]`
221
222        Args:
223            weightNumber: The weight number to match.
224            format: Output format, either 'plain' or 'rich'.
225
226        Returns:
227            List of KFonts or font paths for the weight pair.
228        """
229        result = [
230            font
231            for font in self.fonts
232            if font.getWeightClass(numeric=True) == weightNumber
233        ]
234
235        return result if format == "rich" else self._toPlainFonts(result)
236
237    def getChunks(
238        self,
239        mode: Literal["weight", "slope"],
240        subset: str | list = None,
241        reversed=False,
242    ) -> list[KFont]:
243        """
244        Get fonts grouped by weight or slope.
245
246        Example:
247            - `weight` => [(100, 150), (200, 250), ...]
248            - `slope`  => [(100, 200, ...), (150, 250, ...)]
249
250        Args:
251            mode: Grouping mode, either 'weight' or 'slope'.
252            subset: Optional subset via `KFamily.get`.
253            reversed: Whether to reverse the order within each group.
254
255        Returns:
256            List of lists of KFonts grouped by the specified mode.
257        """
258        if mode == "slope":
259            criteria = ["upright", "italic"]
260        else:
261            criteria = self._getWeightClasses()
262
263        def _meetsCriterion(font: KFont, criterion):
264            if mode == "slope":
265                return font.isType(criterion)
266            else:
267                return font.getWeightClass() == criterion
268
269        def _filterForCriterion(criterion):
270            filtered = [font for font in fontList if _meetsCriterion(font, criterion)]
271
272            if reversed:
273                filtered.reverse()  # Plain list[str]
274
275            return filtered
276
277        fontList = self.get(subset) if subset else self.fonts
278
279        return helpers.removeFalsy([_filterForCriterion(c) for c in criteria])
280
281    def getRange(self, *args, size: int = 3):
282        """
283        Get a range of evenly spaced fonts matching the given criteria.
284
285        Args:
286            *args: Criteria for selecting fonts. See `KFamily.get()`.
287            size: Number of fonts to return.
288
289        Example:
290            `'upright', 3` => `[10, 50, 90]`
291
292        Returns:
293            List of KFonts spaced evenly across the selection.
294        """
295        result = self.get(*args)
296        return helpers.spaceEvenly(result, size=size)
297
298    def getExtremes(self, *args) -> list[KFont]:
299        """
300        Get the extreme fonts (e.g., lightest and boldest) matching the criteria.
301
302        Args:
303            *args: Criteria for selecting fonts. See `KFamily.get()`.
304
305        Example:
306            - `'upright'` => `[10, 80]`
307            - `'text'` =>    `[40, 60]`
308
309        Returns:
310            List of two KFonts representing the extremes.
311        """
312        return self.getRange(*args, size=2)
313
314    def activate(self):
315        """
316        Activate the first font in the family for use in DrawBot. Useful before `omitMissing()`.
317
318        Returns:
319            The KFamily instance (self).
320        """
321        self.fonts[0].activate()
322        return self
323
324    # Internals
325    def _reverse(self, items: list) -> list:
326        """
327        Reverse the order of the given items, grouping by slope if applicable.
328
329        Args:
330            items: List of fonts or font paths.
331
332        Returns:
333            List of fonts or font paths in reversed order.
334        """
335        arePlainFonts = all([isinstance(item, str) for item in items])
336
337        # Pass in plain format to lib.fonts
338        if not arePlainFonts:
339            items = self._toPlainFonts(items)
340
341        fontsUpright, fontsItalic = libFonts.groupFontsBySlope(items, reverseOrder=1)
342
343        if fontsItalic:
344            items = helpers.flatten(list(zip(fontsUpright, fontsItalic)))
345        else:
346            items = fontsUpright
347
348        # Convert back to original format
349        if not arePlainFonts:
350            items = self._toRichFonts(items)
351
352        return items
353
354    def _getWeightClasses(self):
355        """
356        Get all unique weight classes in the family.
357
358        Example:
359            `[1, 2, 3, 4, ... ]`
360
361        Returns:
362            Sorted list of unique weight classes.
363        """
364        return sorted(set([item.getWeightClass() for item in self.fonts]))
365
366    def _toPlainFonts(self, fonts: list[KFont]) -> list[str]:
367        """
368        Convert a list of KFont instances to a list of font paths.
369
370        Args:
371            fonts: List of KFont instances.
372
373        Returns:
374            List of font file paths as strings.
375        """
376        return [font.path if isinstance(font, KFont) else font for font in fonts]
377
378    def _toRichFonts(self, fonts: list[str]) -> list[KFont]:
379        """
380        Convert a list of font paths to KFont instances.
381
382        Args:
383            fonts: List of font file paths as strings.
384
385        Returns:
386            List of KFont instances.
387        """
388        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 getFontsByNames(self, names: list[str]) -> list[KFont]:
163        """
164        Get fonts by style names.
165
166        Args:
167            names: List of style names to match.
168
169        Returns:
170            List of KFont instances matching the style names.
171        """
172        names = helpers.coerceList(names)
173        names = [name.casefold() for name in names]
174        result = [font for font in self.fonts if font.styleName.casefold() in names]
175        return result
176
177    def getRandom(
178        self,
179        criteria: libFonts.FontType | list[libFonts.FontType],
180        count=1,
181        strategy: helpers.Strategy = "pick",
182    ) -> KFont | list[KFont]:
183        """
184        Get a random font or fonts matching the given criteria.
185
186        Args:
187            criteria: Font type or list of font types to match.
188            count: Number of random fonts to return.
189            strategy: Strategy for filtering fonts.
190
191        Returns:
192            A single KFont or a list of KFonts.
193        """
194        result = self.get(criteria=criteria, strategy=strategy)
195        # -> str
196        if count == 1:
197            return random.choice(result)
198        # -> str[]
199        else:
200            return random.sample(result, min(len(result), count))
201
202    def getFonts(self, **args) -> KFont | list[KFont]:
203        """
204        Shorthand for get() to return `KFont` via rich `FontFormat`.
205
206        Args:
207            **args: Arguments passed to get().
208
209        Returns:
210            KFont or list of KFont instances.
211        """
212        return self.get(**args, format="rich")
213
214    def getWeightPair(
215        self, weightNumber: int, format: FontFormat = "rich"
216    ) -> list[KFont]:
217        """
218        Get a pair of fonts for a given weight number.
219
220        Example:
221            `4` => `[40, 45]`
222
223        Args:
224            weightNumber: The weight number to match.
225            format: Output format, either 'plain' or 'rich'.
226
227        Returns:
228            List of KFonts or font paths for the weight pair.
229        """
230        result = [
231            font
232            for font in self.fonts
233            if font.getWeightClass(numeric=True) == weightNumber
234        ]
235
236        return result if format == "rich" else self._toPlainFonts(result)
237
238    def getChunks(
239        self,
240        mode: Literal["weight", "slope"],
241        subset: str | list = None,
242        reversed=False,
243    ) -> list[KFont]:
244        """
245        Get fonts grouped by weight or slope.
246
247        Example:
248            - `weight` => [(100, 150), (200, 250), ...]
249            - `slope`  => [(100, 200, ...), (150, 250, ...)]
250
251        Args:
252            mode: Grouping mode, either 'weight' or 'slope'.
253            subset: Optional subset via `KFamily.get`.
254            reversed: Whether to reverse the order within each group.
255
256        Returns:
257            List of lists of KFonts grouped by the specified mode.
258        """
259        if mode == "slope":
260            criteria = ["upright", "italic"]
261        else:
262            criteria = self._getWeightClasses()
263
264        def _meetsCriterion(font: KFont, criterion):
265            if mode == "slope":
266                return font.isType(criterion)
267            else:
268                return font.getWeightClass() == criterion
269
270        def _filterForCriterion(criterion):
271            filtered = [font for font in fontList if _meetsCriterion(font, criterion)]
272
273            if reversed:
274                filtered.reverse()  # Plain list[str]
275
276            return filtered
277
278        fontList = self.get(subset) if subset else self.fonts
279
280        return helpers.removeFalsy([_filterForCriterion(c) for c in criteria])
281
282    def getRange(self, *args, size: int = 3):
283        """
284        Get a range of evenly spaced fonts matching the given criteria.
285
286        Args:
287            *args: Criteria for selecting fonts. See `KFamily.get()`.
288            size: Number of fonts to return.
289
290        Example:
291            `'upright', 3` => `[10, 50, 90]`
292
293        Returns:
294            List of KFonts spaced evenly across the selection.
295        """
296        result = self.get(*args)
297        return helpers.spaceEvenly(result, size=size)
298
299    def getExtremes(self, *args) -> list[KFont]:
300        """
301        Get the extreme fonts (e.g., lightest and boldest) matching the criteria.
302
303        Args:
304            *args: Criteria for selecting fonts. See `KFamily.get()`.
305
306        Example:
307            - `'upright'` => `[10, 80]`
308            - `'text'` =>    `[40, 60]`
309
310        Returns:
311            List of two KFonts representing the extremes.
312        """
313        return self.getRange(*args, size=2)
314
315    def activate(self):
316        """
317        Activate the first font in the family for use in DrawBot. Useful before `omitMissing()`.
318
319        Returns:
320            The KFamily instance (self).
321        """
322        self.fonts[0].activate()
323        return self
324
325    # Internals
326    def _reverse(self, items: list) -> list:
327        """
328        Reverse the order of the given items, grouping by slope if applicable.
329
330        Args:
331            items: List of fonts or font paths.
332
333        Returns:
334            List of fonts or font paths in reversed order.
335        """
336        arePlainFonts = all([isinstance(item, str) for item in items])
337
338        # Pass in plain format to lib.fonts
339        if not arePlainFonts:
340            items = self._toPlainFonts(items)
341
342        fontsUpright, fontsItalic = libFonts.groupFontsBySlope(items, reverseOrder=1)
343
344        if fontsItalic:
345            items = helpers.flatten(list(zip(fontsUpright, fontsItalic)))
346        else:
347            items = fontsUpright
348
349        # Convert back to original format
350        if not arePlainFonts:
351            items = self._toRichFonts(items)
352
353        return items
354
355    def _getWeightClasses(self):
356        """
357        Get all unique weight classes in the family.
358
359        Example:
360            `[1, 2, 3, 4, ... ]`
361
362        Returns:
363            Sorted list of unique weight classes.
364        """
365        return sorted(set([item.getWeightClass() for item in self.fonts]))
366
367    def _toPlainFonts(self, fonts: list[KFont]) -> list[str]:
368        """
369        Convert a list of KFont instances to a list of font paths.
370
371        Args:
372            fonts: List of KFont instances.
373
374        Returns:
375            List of font file paths as strings.
376        """
377        return [font.path if isinstance(font, KFont) else font for font in fonts]
378
379    def _toRichFonts(self, fonts: list[str]) -> list[KFont]:
380        """
381        Convert a list of font paths to KFont instances.
382
383        Args:
384            fonts: List of font file paths as strings.
385
386        Returns:
387            List of KFont instances.
388        """
389        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 getFontsByNames(self, names: list[str]) -> list[classes.c10_font.KFont]:
162    def getFontsByNames(self, names: list[str]) -> list[KFont]:
163        """
164        Get fonts by style names.
165
166        Args:
167            names: List of style names to match.
168
169        Returns:
170            List of KFont instances matching the style names.
171        """
172        names = helpers.coerceList(names)
173        names = [name.casefold() for name in names]
174        result = [font for font in self.fonts if font.styleName.casefold() in names]
175        return result

Get fonts by style names.

Arguments:
  • names: List of style names to match.
Returns:

List of KFont instances matching the style names.

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]:
177    def getRandom(
178        self,
179        criteria: libFonts.FontType | list[libFonts.FontType],
180        count=1,
181        strategy: helpers.Strategy = "pick",
182    ) -> KFont | list[KFont]:
183        """
184        Get a random font or fonts matching the given criteria.
185
186        Args:
187            criteria: Font type or list of font types to match.
188            count: Number of random fonts to return.
189            strategy: Strategy for filtering fonts.
190
191        Returns:
192            A single KFont or a list of KFonts.
193        """
194        result = self.get(criteria=criteria, strategy=strategy)
195        # -> str
196        if count == 1:
197            return random.choice(result)
198        # -> str[]
199        else:
200            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) -> classes.c10_font.KFont | list[classes.c10_font.KFont]:
202    def getFonts(self, **args) -> KFont | list[KFont]:
203        """
204        Shorthand for get() to return `KFont` via rich `FontFormat`.
205
206        Args:
207            **args: Arguments passed to get().
208
209        Returns:
210            KFont or list of KFont instances.
211        """
212        return self.get(**args, format="rich")

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

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

KFont or list of KFont instances.

def getWeightPair( self, weightNumber: int, format: Literal['plain', 'rich'] = 'rich') -> list[classes.c10_font.KFont]:
214    def getWeightPair(
215        self, weightNumber: int, format: FontFormat = "rich"
216    ) -> list[KFont]:
217        """
218        Get a pair of fonts for a given weight number.
219
220        Example:
221            `4` => `[40, 45]`
222
223        Args:
224            weightNumber: The weight number to match.
225            format: Output format, either 'plain' or 'rich'.
226
227        Returns:
228            List of KFonts or font paths for the weight pair.
229        """
230        result = [
231            font
232            for font in self.fonts
233            if font.getWeightClass(numeric=True) == weightNumber
234        ]
235
236        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]:
238    def getChunks(
239        self,
240        mode: Literal["weight", "slope"],
241        subset: str | list = None,
242        reversed=False,
243    ) -> list[KFont]:
244        """
245        Get fonts grouped by weight or slope.
246
247        Example:
248            - `weight` => [(100, 150), (200, 250), ...]
249            - `slope`  => [(100, 200, ...), (150, 250, ...)]
250
251        Args:
252            mode: Grouping mode, either 'weight' or 'slope'.
253            subset: Optional subset via `KFamily.get`.
254            reversed: Whether to reverse the order within each group.
255
256        Returns:
257            List of lists of KFonts grouped by the specified mode.
258        """
259        if mode == "slope":
260            criteria = ["upright", "italic"]
261        else:
262            criteria = self._getWeightClasses()
263
264        def _meetsCriterion(font: KFont, criterion):
265            if mode == "slope":
266                return font.isType(criterion)
267            else:
268                return font.getWeightClass() == criterion
269
270        def _filterForCriterion(criterion):
271            filtered = [font for font in fontList if _meetsCriterion(font, criterion)]
272
273            if reversed:
274                filtered.reverse()  # Plain list[str]
275
276            return filtered
277
278        fontList = self.get(subset) if subset else self.fonts
279
280        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):
282    def getRange(self, *args, size: int = 3):
283        """
284        Get a range of evenly spaced fonts matching the given criteria.
285
286        Args:
287            *args: Criteria for selecting fonts. See `KFamily.get()`.
288            size: Number of fonts to return.
289
290        Example:
291            `'upright', 3` => `[10, 50, 90]`
292
293        Returns:
294            List of KFonts spaced evenly across the selection.
295        """
296        result = self.get(*args)
297        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]:
299    def getExtremes(self, *args) -> list[KFont]:
300        """
301        Get the extreme fonts (e.g., lightest and boldest) matching the criteria.
302
303        Args:
304            *args: Criteria for selecting fonts. See `KFamily.get()`.
305
306        Example:
307            - `'upright'` => `[10, 80]`
308            - `'text'` =>    `[40, 60]`
309
310        Returns:
311            List of two KFonts representing the extremes.
312        """
313        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):
315    def activate(self):
316        """
317        Activate the first font in the family for use in DrawBot. Useful before `omitMissing()`.
318
319        Returns:
320            The KFamily instance (self).
321        """
322        self.fonts[0].activate()
323        return self

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

Returns:

The KFamily instance (self).