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, Sequence, get_args
  6from icecream import ic
  7from loguru import logger
  8
  9from lib import helpers, fonts as libFonts
 10from datatypes.data_fontlist import DFontList
 11
 12# Actually uses KFont, otherwise typing.TYPE_CHECKING would suffice to avoid circular import
 13from .c10_font import KFont
 14
 15FontOrder: TypeAlias = Literal["ascending", "descending"]
 16"""Used to sort fonts in `KFamily.serve`."""
 17
 18
 19class KFamily:
 20    """
 21    Represents a family of fonts and provides methods for querying and manipulating font collections.
 22
 23    See `classes.c10_font.KFont`.
 24    """
 25
 26    styles: list[str]
 27    """List of plain font paths"""
 28
 29    fonts: DFontList
 30    """List-like collection of KFonts (`datatypes.data_fontlist.DFontList`)."""
 31
 32    def __init__(self, fontFolder: str):
 33        """
 34        Initialize a KFamily instance.
 35
 36        Args:
 37            fontFolder: File system path to the folder containing font files.
 38        """
 39        self.styles = libFonts.getFonts(fontFolder)
 40        self.fonts = DFontList([KFont(path, self) for path in self.styles])
 41
 42    def __str__(self) -> str:
 43        """
 44        Return a string representation of the KFamily instance.
 45        """
 46        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})"
 47
 48    @property
 49    def familyName(self) -> str:
 50        """Returns the family name as a string."""
 51        return self.fonts[0].familyName
 52
 53    @property
 54    def familyAbbreviation(self) -> str:
 55        """Returns the family abbreviation, formed by the first letter of each word in the family name.
 56
 57        Example:
 58            `Stabil Grotesk` => `SG`
 59        """
 60        return "".join([word[0] for word in self.familyName.split(" ")])
 61
 62    @property
 63    def familyCount(self) -> int:
 64        """Returns the number of fonts in the family."""
 65        return len(self.fonts)
 66
 67    @property
 68    def familyNoun(self) -> str:
 69        """Returns 'font' or 'fonts' depending on the count."""
 70        return "fonts" if self.familyCount > 1 else "font"
 71
 72    @property
 73    def version(self) -> str:
 74        """Returns the version of the family, derived from the first font."""
 75        return libFonts.getFontVersion(self.fonts[0].path)
 76
 77    @property
 78    def scalar(self) -> int:
 79        """
 80        Returns the length of `classes.c10_font.KFont.styleNumber`.
 81
 82        Example:
 83            FontName-`200`Light => `3`
 84        """
 85        return len(self.fonts[0].styleNumber)
 86
 87    def get(
 88        self,
 89        criteria: list[int | str | tuple[int, int]] | int | str | tuple[int, int],
 90        strategy: helpers.Strategy = "pick",
 91        unwrapSingle: bool = True,
 92    ) -> KFont | DFontList:
 93        """
 94        Get fonts matching the given criteria.
 95
 96        Args:
 97            criteria: Style number(s), style name(s), weight-class range tuple(s),
 98                or `lib.fonts.FontType` criterion/criteria.
 99            strategy: Strategy for filtering fonts.
100            unwrapSingle: Whether to return a single KFont if only one match is found.
101
102        Example:
103            - `[50, 70]` => get 2 fonts
104            - `["upright", "display"]` => get intersection of types
105
106        Returns:
107            A single KFont or a list of KFonts matching the criteria.
108        """
109
110        if isinstance(criteria, list):
111            logger.warning(
112                "Using `KFamily.get()` with multiple criteria is deprecated. Use `findFonts()` for naming criteria and `filterFonts()` for genre criteria instead."
113            )
114
115        criteria = helpers.coerceList(criteria, strict=True)
116        # Numeric criteria are resolved position-by-position.
117        # Mixed/text criteria are combined as an intersection.
118        isAllNumeric = all([isinstance(criterion, int) for criterion in criteria])
119        if isAllNumeric:
120            result = self.findFonts(criteria)
121        else:
122            result = self.filterFonts(criteria, strategy=strategy)
123
124        result = DFontList([item for item in result if item is not None])
125
126        # Return list only for multiple fonts
127        if unwrapSingle and isinstance(result, (list, DFontList)) and len(result) == 1:
128            return result[0]
129
130        return result
131
132    def findFonts(
133        self,
134        criteria: list[int | str],
135    ) -> DFontList[KFont]:
136        """
137        Resolve criteria one-by-one to fonts, preserving input length and order.
138
139        Accepts style numbers (int or numeric string) and style names.
140
141        Args:
142            criteria: List of style numbers or style names.
143
144        Returns:
145            List of resolved fonts in the same order and length as criteria.
146        """
147        criteria = helpers.coerceList(criteria, strict=True)
148        resolved = [self._findFontPath(criterion) for criterion in criteria]
149
150        return DFontList([KFont(path, self) for path in resolved if path is not None])
151
152    def filterFonts(
153        self,
154        criteria: list[int | str | tuple[int, int]] | libFonts.FontType,
155        strategy: helpers.Strategy = "pick",
156    ) -> DFontList[KFont]:
157        """
158        Combine criteria and return fonts that satisfy their intersection.
159
160        Args:
161            criteria: List of criteria to combine. Supports style numbers,
162                style names, style-number ranges `(start, end)` by weight class,
163                and `lib.fonts.FontType` values.
164            strategy: Filtering strategy for `lib.fonts.FontType` criteria.
165
166        Returns:
167            Intersected list of matching fonts.
168        """
169        criteria = helpers.coerceList(criteria, strict=True)
170        allMatches = [
171            self._getMatchesForCriterion(criterion, strategy) for criterion in criteria
172        ]
173        result = helpers.intersect(allMatches)
174
175        return DFontList(self._toRichFonts(result))
176
177    def getRandom(
178        self,
179        criteria: libFonts.FontType | list[libFonts.FontType],
180        count=1,
181        strategy: helpers.Strategy = "pick",
182    ) -> KFont | DFontList:
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.filterFonts(criteria=criteria, strategy=strategy)
195        # -> str
196        if count == 1:
197            return random.choice(result)
198        # -> str[]
199        else:
200            return DFontList(random.sample(result, min(len(result), count)))
201
202    def getWeightPair(self, weightNumber: int) -> DFontList:
203        """
204        Get a pair of fonts for a given weight number.
205
206        Example:
207            `4` => `[40, 45]`
208
209        Args:
210            weightNumber: The weight number to match.
211
212        Returns:
213            List of KFonts 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 DFontList(result)
222
223    def getRange(self, *args, size: int = 3):
224        """
225        Get a range of evenly spaced fonts matching the given criteria.
226
227        Args:
228            *args: Criteria for selecting fonts. See `KFamily.get()`.
229            size: Number of fonts to return.
230
231        Example:
232            `'upright', 3` => `[10, 50, 90]`
233
234        Returns:
235            List of KFonts spaced evenly across the selection.
236        """
237        result = self.get(*args)
238        if isinstance(result, KFont):
239            return DFontList([result])
240        return DFontList(helpers.spaceEvenly(result, size=size))
241
242    def getExtremes(self, *args) -> DFontList:
243        """
244        Get the extreme fonts (e.g., lightest and boldest) matching the criteria.
245
246        Args:
247            *args: Criteria for selecting fonts. See `KFamily.get()`.
248
249        Example:
250            - `'upright'` => `[10, 80]`
251            - `'text'` =>    `[40, 60]`
252
253        Returns:
254            List of two KFonts representing the extremes.
255        """
256        return self.getRange(*args, size=2)
257
258    def activate(self) -> "KFamily":
259        """
260        Activate the first font in the family for use in DrawBot. Useful before `omitMissing()`.
261
262        Returns:
263            The KFamily instance (self).
264        """
265        self.fonts[0].activate()
266        return self
267
268    # Internals
269    def _getWeightClasses(self):
270        """
271        Get all unique weight classes in the family.
272
273        Example:
274            `[1, 2, 3, 4, ... ]`
275
276        Returns:
277            Sorted list of unique weight classes.
278        """
279        return sorted(set([item.getWeightClass() for item in self.fonts]))
280
281    def _toRichFonts(self, fonts: list[str]) -> list[KFont]:
282        """Convert a list of font paths to KFont instances."""
283        return [KFont(font, self) if isinstance(font, str) else font for font in fonts]
284
285    def _normalizeStyleNumber(self, criterion: int) -> int:
286        """
287        Normalize style numbers to family scalar length.
288
289        Examples:
290            `5000` => `500`
291            `55` => `550` for scalar 3
292        """
293        if len(str(criterion)) > self.scalar:
294            # Scale down: 5000 => 500
295            scaled = str(criterion)[: self.scalar]
296        else:
297            # Scale up: 55 => 550
298            scaled = f"{criterion:0<{self.scalar}}"
299        return int(scaled)
300
301    def _findFontPath(self, criterion: int | str) -> str | None:
302        """Resolve a single style number or style name to a font path."""
303        if isinstance(criterion, int):
304            return libFonts.findFontByNumber(
305                self.styles, self._normalizeStyleNumber(criterion)
306            )
307
308        if isinstance(criterion, str) and criterion.strip().isnumeric():
309            numeric = int(criterion.strip())
310            return libFonts.findFontByNumber(
311                self.styles, self._normalizeStyleNumber(numeric)
312            )
313
314        # Match by style name (case-insensitive)
315        if isinstance(criterion, str):
316            target = criterion.casefold().strip()
317            for font in self.fonts:
318                if font.styleName.casefold() == target:
319                    return font.path
320
321        logger.warning(
322            f"[KFamily._findFontPath] Could not resolve criterion: {criterion!r}"
323        )
324        return None
325
326    def _getMatchesForCriterion(
327        self,
328        criterion: int | str | tuple[int, int],
329        strategy: helpers.Strategy,
330    ) -> list[str]:
331        """Resolve one criterion to a list of matching plain font paths."""
332        if isinstance(criterion, tuple):
333            return [
334                font.path
335                for font in self.fonts
336                if helpers.isInRange(*criterion, font.getWeightClass(numeric=True))
337            ]
338
339        # ! Prefer matching by FontType if criterion is a valid FontType value
340        if isinstance(criterion, str) and criterion.casefold().strip() in get_args(
341            libFonts.FontType
342        ):
343            return libFonts.filterFonts(self.styles, criterion, strategy=strategy)
344
345        resolved = self._findFontPath(criterion)
346        if resolved:
347            return [resolved]
348
349        return libFonts.filterFonts(self.styles, criterion, strategy=strategy)
FontOrder: TypeAlias = Literal['ascending', 'descending']

Used to sort fonts in KFamily.serve.

class KFamily:
 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: DFontList
 31    """List-like collection of KFonts (`datatypes.data_fontlist.DFontList`)."""
 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 = DFontList([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        return self.fonts[0].familyName
 53
 54    @property
 55    def familyAbbreviation(self) -> str:
 56        """Returns the family abbreviation, formed by the first letter of each word in the family name.
 57
 58        Example:
 59            `Stabil Grotesk` => `SG`
 60        """
 61        return "".join([word[0] for word in self.familyName.split(" ")])
 62
 63    @property
 64    def familyCount(self) -> int:
 65        """Returns the number of fonts in the family."""
 66        return len(self.fonts)
 67
 68    @property
 69    def familyNoun(self) -> str:
 70        """Returns 'font' or 'fonts' depending on the count."""
 71        return "fonts" if self.familyCount > 1 else "font"
 72
 73    @property
 74    def version(self) -> str:
 75        """Returns the version of the family, derived from the first font."""
 76        return libFonts.getFontVersion(self.fonts[0].path)
 77
 78    @property
 79    def scalar(self) -> int:
 80        """
 81        Returns the length of `classes.c10_font.KFont.styleNumber`.
 82
 83        Example:
 84            FontName-`200`Light => `3`
 85        """
 86        return len(self.fonts[0].styleNumber)
 87
 88    def get(
 89        self,
 90        criteria: list[int | str | tuple[int, int]] | int | str | tuple[int, int],
 91        strategy: helpers.Strategy = "pick",
 92        unwrapSingle: bool = True,
 93    ) -> KFont | DFontList:
 94        """
 95        Get fonts matching the given criteria.
 96
 97        Args:
 98            criteria: Style number(s), style name(s), weight-class range tuple(s),
 99                or `lib.fonts.FontType` criterion/criteria.
100            strategy: Strategy for filtering fonts.
101            unwrapSingle: Whether to return a single KFont if only one match is found.
102
103        Example:
104            - `[50, 70]` => get 2 fonts
105            - `["upright", "display"]` => get intersection of types
106
107        Returns:
108            A single KFont or a list of KFonts matching the criteria.
109        """
110
111        if isinstance(criteria, list):
112            logger.warning(
113                "Using `KFamily.get()` with multiple criteria is deprecated. Use `findFonts()` for naming criteria and `filterFonts()` for genre criteria instead."
114            )
115
116        criteria = helpers.coerceList(criteria, strict=True)
117        # Numeric criteria are resolved position-by-position.
118        # Mixed/text criteria are combined as an intersection.
119        isAllNumeric = all([isinstance(criterion, int) for criterion in criteria])
120        if isAllNumeric:
121            result = self.findFonts(criteria)
122        else:
123            result = self.filterFonts(criteria, strategy=strategy)
124
125        result = DFontList([item for item in result if item is not None])
126
127        # Return list only for multiple fonts
128        if unwrapSingle and isinstance(result, (list, DFontList)) and len(result) == 1:
129            return result[0]
130
131        return result
132
133    def findFonts(
134        self,
135        criteria: list[int | str],
136    ) -> DFontList[KFont]:
137        """
138        Resolve criteria one-by-one to fonts, preserving input length and order.
139
140        Accepts style numbers (int or numeric string) and style names.
141
142        Args:
143            criteria: List of style numbers or style names.
144
145        Returns:
146            List of resolved fonts in the same order and length as criteria.
147        """
148        criteria = helpers.coerceList(criteria, strict=True)
149        resolved = [self._findFontPath(criterion) for criterion in criteria]
150
151        return DFontList([KFont(path, self) for path in resolved if path is not None])
152
153    def filterFonts(
154        self,
155        criteria: list[int | str | tuple[int, int]] | libFonts.FontType,
156        strategy: helpers.Strategy = "pick",
157    ) -> DFontList[KFont]:
158        """
159        Combine criteria and return fonts that satisfy their intersection.
160
161        Args:
162            criteria: List of criteria to combine. Supports style numbers,
163                style names, style-number ranges `(start, end)` by weight class,
164                and `lib.fonts.FontType` values.
165            strategy: Filtering strategy for `lib.fonts.FontType` criteria.
166
167        Returns:
168            Intersected list of matching fonts.
169        """
170        criteria = helpers.coerceList(criteria, strict=True)
171        allMatches = [
172            self._getMatchesForCriterion(criterion, strategy) for criterion in criteria
173        ]
174        result = helpers.intersect(allMatches)
175
176        return DFontList(self._toRichFonts(result))
177
178    def getRandom(
179        self,
180        criteria: libFonts.FontType | list[libFonts.FontType],
181        count=1,
182        strategy: helpers.Strategy = "pick",
183    ) -> KFont | DFontList:
184        """
185        Get a random font or fonts matching the given criteria.
186
187        Args:
188            criteria: Font type or list of font types to match.
189            count: Number of random fonts to return.
190            strategy: Strategy for filtering fonts.
191
192        Returns:
193            A single KFont or a list of KFonts.
194        """
195        result = self.filterFonts(criteria=criteria, strategy=strategy)
196        # -> str
197        if count == 1:
198            return random.choice(result)
199        # -> str[]
200        else:
201            return DFontList(random.sample(result, min(len(result), count)))
202
203    def getWeightPair(self, weightNumber: int) -> DFontList:
204        """
205        Get a pair of fonts for a given weight number.
206
207        Example:
208            `4` => `[40, 45]`
209
210        Args:
211            weightNumber: The weight number to match.
212
213        Returns:
214            List of KFonts for the weight pair.
215        """
216        result = [
217            font
218            for font in self.fonts
219            if font.getWeightClass(numeric=True) == weightNumber
220        ]
221
222        return DFontList(result)
223
224    def getRange(self, *args, size: int = 3):
225        """
226        Get a range of evenly spaced fonts matching the given criteria.
227
228        Args:
229            *args: Criteria for selecting fonts. See `KFamily.get()`.
230            size: Number of fonts to return.
231
232        Example:
233            `'upright', 3` => `[10, 50, 90]`
234
235        Returns:
236            List of KFonts spaced evenly across the selection.
237        """
238        result = self.get(*args)
239        if isinstance(result, KFont):
240            return DFontList([result])
241        return DFontList(helpers.spaceEvenly(result, size=size))
242
243    def getExtremes(self, *args) -> DFontList:
244        """
245        Get the extreme fonts (e.g., lightest and boldest) matching the criteria.
246
247        Args:
248            *args: Criteria for selecting fonts. See `KFamily.get()`.
249
250        Example:
251            - `'upright'` => `[10, 80]`
252            - `'text'` =>    `[40, 60]`
253
254        Returns:
255            List of two KFonts representing the extremes.
256        """
257        return self.getRange(*args, size=2)
258
259    def activate(self) -> "KFamily":
260        """
261        Activate the first font in the family for use in DrawBot. Useful before `omitMissing()`.
262
263        Returns:
264            The KFamily instance (self).
265        """
266        self.fonts[0].activate()
267        return self
268
269    # Internals
270    def _getWeightClasses(self):
271        """
272        Get all unique weight classes in the family.
273
274        Example:
275            `[1, 2, 3, 4, ... ]`
276
277        Returns:
278            Sorted list of unique weight classes.
279        """
280        return sorted(set([item.getWeightClass() for item in self.fonts]))
281
282    def _toRichFonts(self, fonts: list[str]) -> list[KFont]:
283        """Convert a list of font paths to KFont instances."""
284        return [KFont(font, self) if isinstance(font, str) else font for font in fonts]
285
286    def _normalizeStyleNumber(self, criterion: int) -> int:
287        """
288        Normalize style numbers to family scalar length.
289
290        Examples:
291            `5000` => `500`
292            `55` => `550` for scalar 3
293        """
294        if len(str(criterion)) > self.scalar:
295            # Scale down: 5000 => 500
296            scaled = str(criterion)[: self.scalar]
297        else:
298            # Scale up: 55 => 550
299            scaled = f"{criterion:0<{self.scalar}}"
300        return int(scaled)
301
302    def _findFontPath(self, criterion: int | str) -> str | None:
303        """Resolve a single style number or style name to a font path."""
304        if isinstance(criterion, int):
305            return libFonts.findFontByNumber(
306                self.styles, self._normalizeStyleNumber(criterion)
307            )
308
309        if isinstance(criterion, str) and criterion.strip().isnumeric():
310            numeric = int(criterion.strip())
311            return libFonts.findFontByNumber(
312                self.styles, self._normalizeStyleNumber(numeric)
313            )
314
315        # Match by style name (case-insensitive)
316        if isinstance(criterion, str):
317            target = criterion.casefold().strip()
318            for font in self.fonts:
319                if font.styleName.casefold() == target:
320                    return font.path
321
322        logger.warning(
323            f"[KFamily._findFontPath] Could not resolve criterion: {criterion!r}"
324        )
325        return None
326
327    def _getMatchesForCriterion(
328        self,
329        criterion: int | str | tuple[int, int],
330        strategy: helpers.Strategy,
331    ) -> list[str]:
332        """Resolve one criterion to a list of matching plain font paths."""
333        if isinstance(criterion, tuple):
334            return [
335                font.path
336                for font in self.fonts
337                if helpers.isInRange(*criterion, font.getWeightClass(numeric=True))
338            ]
339
340        # ! Prefer matching by FontType if criterion is a valid FontType value
341        if isinstance(criterion, str) and criterion.casefold().strip() in get_args(
342            libFonts.FontType
343        ):
344            return libFonts.filterFonts(self.styles, criterion, strategy=strategy)
345
346        resolved = self._findFontPath(criterion)
347        if resolved:
348            return [resolved]
349
350        return libFonts.filterFonts(self.styles, criterion, strategy=strategy)

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

See classes.c10_font.KFont.

KFamily(fontFolder: str)
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 = DFontList([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

List-like collection of KFonts (datatypes.data_fontlist.DFontList).

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

Returns the family name as a string.

familyAbbreviation: str
54    @property
55    def familyAbbreviation(self) -> str:
56        """Returns the family abbreviation, formed by the first letter of each word in the family name.
57
58        Example:
59            `Stabil Grotesk` => `SG`
60        """
61        return "".join([word[0] for word in self.familyName.split(" ")])

Returns the family abbreviation, formed by the first letter of each word in the family name.

Example:

Stabil Grotesk => SG

familyCount: int
63    @property
64    def familyCount(self) -> int:
65        """Returns the number of fonts in the family."""
66        return len(self.fonts)

Returns the number of fonts in the family.

familyNoun: str
68    @property
69    def familyNoun(self) -> str:
70        """Returns 'font' or 'fonts' depending on the count."""
71        return "fonts" if self.familyCount > 1 else "font"

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

version: str
73    @property
74    def version(self) -> str:
75        """Returns the version of the family, derived from the first font."""
76        return libFonts.getFontVersion(self.fonts[0].path)

Returns the version of the family, derived from the first font.

scalar: int
78    @property
79    def scalar(self) -> int:
80        """
81        Returns the length of `classes.c10_font.KFont.styleNumber`.
82
83        Example:
84            FontName-`200`Light => `3`
85        """
86        return len(self.fonts[0].styleNumber)

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

Example:

FontName-200Light => 3

def get( self, criteria: list[int | str | tuple[int, int]] | int | str | tuple[int, int], strategy: Literal['pick', 'omit'] = 'pick', unwrapSingle: bool = True) -> classes.c10_font.KFont | datatypes.data_fontlist.DFontList:
 88    def get(
 89        self,
 90        criteria: list[int | str | tuple[int, int]] | int | str | tuple[int, int],
 91        strategy: helpers.Strategy = "pick",
 92        unwrapSingle: bool = True,
 93    ) -> KFont | DFontList:
 94        """
 95        Get fonts matching the given criteria.
 96
 97        Args:
 98            criteria: Style number(s), style name(s), weight-class range tuple(s),
 99                or `lib.fonts.FontType` criterion/criteria.
100            strategy: Strategy for filtering fonts.
101            unwrapSingle: Whether to return a single KFont if only one match is found.
102
103        Example:
104            - `[50, 70]` => get 2 fonts
105            - `["upright", "display"]` => get intersection of types
106
107        Returns:
108            A single KFont or a list of KFonts matching the criteria.
109        """
110
111        if isinstance(criteria, list):
112            logger.warning(
113                "Using `KFamily.get()` with multiple criteria is deprecated. Use `findFonts()` for naming criteria and `filterFonts()` for genre criteria instead."
114            )
115
116        criteria = helpers.coerceList(criteria, strict=True)
117        # Numeric criteria are resolved position-by-position.
118        # Mixed/text criteria are combined as an intersection.
119        isAllNumeric = all([isinstance(criterion, int) for criterion in criteria])
120        if isAllNumeric:
121            result = self.findFonts(criteria)
122        else:
123            result = self.filterFonts(criteria, strategy=strategy)
124
125        result = DFontList([item for item in result if item is not None])
126
127        # Return list only for multiple fonts
128        if unwrapSingle and isinstance(result, (list, DFontList)) and len(result) == 1:
129            return result[0]
130
131        return result

Get fonts matching the given criteria.

Arguments:
  • criteria: Style number(s), style name(s), weight-class range tuple(s), or lib.fonts.FontType criterion/criteria.
  • strategy: Strategy for filtering fonts.
  • unwrapSingle: Whether to return a single KFont if only one match is found.
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 findFonts( self, criteria: list[int | str]) -> datatypes.data_fontlist.DFontList[classes.c10_font.KFont]:
133    def findFonts(
134        self,
135        criteria: list[int | str],
136    ) -> DFontList[KFont]:
137        """
138        Resolve criteria one-by-one to fonts, preserving input length and order.
139
140        Accepts style numbers (int or numeric string) and style names.
141
142        Args:
143            criteria: List of style numbers or style names.
144
145        Returns:
146            List of resolved fonts in the same order and length as criteria.
147        """
148        criteria = helpers.coerceList(criteria, strict=True)
149        resolved = [self._findFontPath(criterion) for criterion in criteria]
150
151        return DFontList([KFont(path, self) for path in resolved if path is not None])

Resolve criteria one-by-one to fonts, preserving input length and order.

Accepts style numbers (int or numeric string) and style names.

Arguments:
  • criteria: List of style numbers or style names.
Returns:

List of resolved fonts in the same order and length as criteria.

def filterFonts( self, criteria: Union[list[int | str | tuple[int, int]], Literal['upright', 'italic', 'display', 'text', 'thin', 'thick', 'any']], strategy: Literal['pick', 'omit'] = 'pick') -> datatypes.data_fontlist.DFontList[classes.c10_font.KFont]:
153    def filterFonts(
154        self,
155        criteria: list[int | str | tuple[int, int]] | libFonts.FontType,
156        strategy: helpers.Strategy = "pick",
157    ) -> DFontList[KFont]:
158        """
159        Combine criteria and return fonts that satisfy their intersection.
160
161        Args:
162            criteria: List of criteria to combine. Supports style numbers,
163                style names, style-number ranges `(start, end)` by weight class,
164                and `lib.fonts.FontType` values.
165            strategy: Filtering strategy for `lib.fonts.FontType` criteria.
166
167        Returns:
168            Intersected list of matching fonts.
169        """
170        criteria = helpers.coerceList(criteria, strict=True)
171        allMatches = [
172            self._getMatchesForCriterion(criterion, strategy) for criterion in criteria
173        ]
174        result = helpers.intersect(allMatches)
175
176        return DFontList(self._toRichFonts(result))

Combine criteria and return fonts that satisfy their intersection.

Arguments:
  • criteria: List of criteria to combine. Supports style numbers, style names, style-number ranges (start, end) by weight class, and lib.fonts.FontType values.
  • strategy: Filtering strategy for lib.fonts.FontType criteria.
Returns:

Intersected list of matching fonts.

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 | datatypes.data_fontlist.DFontList:
178    def getRandom(
179        self,
180        criteria: libFonts.FontType | list[libFonts.FontType],
181        count=1,
182        strategy: helpers.Strategy = "pick",
183    ) -> KFont | DFontList:
184        """
185        Get a random font or fonts matching the given criteria.
186
187        Args:
188            criteria: Font type or list of font types to match.
189            count: Number of random fonts to return.
190            strategy: Strategy for filtering fonts.
191
192        Returns:
193            A single KFont or a list of KFonts.
194        """
195        result = self.filterFonts(criteria=criteria, strategy=strategy)
196        # -> str
197        if count == 1:
198            return random.choice(result)
199        # -> str[]
200        else:
201            return DFontList(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 getWeightPair(self, weightNumber: int) -> datatypes.data_fontlist.DFontList:
203    def getWeightPair(self, weightNumber: int) -> DFontList:
204        """
205        Get a pair of fonts for a given weight number.
206
207        Example:
208            `4` => `[40, 45]`
209
210        Args:
211            weightNumber: The weight number to match.
212
213        Returns:
214            List of KFonts for the weight pair.
215        """
216        result = [
217            font
218            for font in self.fonts
219            if font.getWeightClass(numeric=True) == weightNumber
220        ]
221
222        return DFontList(result)

Get a pair of fonts for a given weight number.

Example:

4 => [40, 45]

Arguments:
  • weightNumber: The weight number to match.
Returns:

List of KFonts for the weight pair.

def getRange(self, *args, size: int = 3):
224    def getRange(self, *args, size: int = 3):
225        """
226        Get a range of evenly spaced fonts matching the given criteria.
227
228        Args:
229            *args: Criteria for selecting fonts. See `KFamily.get()`.
230            size: Number of fonts to return.
231
232        Example:
233            `'upright', 3` => `[10, 50, 90]`
234
235        Returns:
236            List of KFonts spaced evenly across the selection.
237        """
238        result = self.get(*args)
239        if isinstance(result, KFont):
240            return DFontList([result])
241        return DFontList(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) -> datatypes.data_fontlist.DFontList:
243    def getExtremes(self, *args) -> DFontList:
244        """
245        Get the extreme fonts (e.g., lightest and boldest) matching the criteria.
246
247        Args:
248            *args: Criteria for selecting fonts. See `KFamily.get()`.
249
250        Example:
251            - `'upright'` => `[10, 80]`
252            - `'text'` =>    `[40, 60]`
253
254        Returns:
255            List of two KFonts representing the extremes.
256        """
257        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) -> KFamily:
259    def activate(self) -> "KFamily":
260        """
261        Activate the first font in the family for use in DrawBot. Useful before `omitMissing()`.
262
263        Returns:
264            The KFamily instance (self).
265        """
266        self.fonts[0].activate()
267        return self

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

Returns:

The KFamily instance (self).