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]
Output format for fonts: plain for file paths, rich for KFont instances.
Used to sort fonts in KFamily.serve.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
- *args: Criteria for selecting fonts. See
KFamily.get().
Example:
'upright'=>[10, 80]'text'=>[40, 60]
Returns:
List of two KFonts representing the extremes.
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).