datatypes.data_fontprops

  1from typing import TYPE_CHECKING, List, Literal, Union
  2import drawBot
  3from dataclasses import dataclass, asdict
  4from functools import cached_property
  5
  6from lib import helpers, fonts
  7
  8# Avoid circular import issues, hide from runtime, but keep for type checking
  9if TYPE_CHECKING:
 10    from datatypes import DFontList
 11    from classes import KFont
 12
 13# TODO: Add RGB/CMYK fill property
 14
 15TrackingStrategy = Literal["relative", "absolute"]
 16"""How letterSpacing input is interpreted: "relative" (default) scales by font size like Adobe tracking, "absolute" uses letterSpacing as absolute points like Sketch and CSS."""
 17
 18
 19# NamedTuple doesn’t allow custom init or internal properties
 20# Dataclass to allow cached_property
 21@dataclass
 22class DFontProps:
 23    """Data structure for font properties used in DrawBot.
 24
 25    Args:
 26        fontSize: The font size in points.
 27        leading: The leading (line spacing) multiplier.
 28        letterSpacing: Tracking input value (relative by default, absolute if trackingStrategy="absolute").
 29        trackingStrategy: Tracking strategy: "relative" for Adobe-style, "absolute" for Sketch-style.
 30        fontData: The font data or list of font data.
 31        align: Text alignment option.
 32        openType: OpenType font features.
 33    """
 34
 35    fontSize: int = 24
 36    leading: float = 1.25
 37    letterSpacing: int = 0
 38    trackingStrategy: TrackingStrategy = "relative"
 39    fontData: "KFont | DFontList | list[KFont]" = None
 40    align: fonts.TextAlign = None
 41    openType: fonts.FeatureId | list[fonts.FeatureId] | dict[fonts.FeatureId, bool] = (
 42        None
 43    )
 44
 45    @property
 46    def lineHeight(self):
 47        """The calculated line height.
 48
 49        Returns:
 50            The product of fontSize and leading, or None if fontSize undefined.
 51        """
 52        try:
 53            return self.fontSize * self.leading
 54        except Exception:
 55            return
 56
 57    @cached_property
 58    def blockHeight(self) -> float:
 59        """The height of one line of text."""
 60        with drawBot.savedState():
 61            self.apply()
 62            _, h = drawBot.textSize("H")
 63        return h
 64
 65    @property
 66    def tracking(self) -> float:
 67        """Tracking value in DrawBot points.
 68
 69        Default behavior matches Adobe-style relative tracking where `letterSpacing` units scale by font size. Set `trackingStrategy="absolute"` to use Sketch-style absolute tracking in points.
 70        """
 71        try:
 72            if self.trackingStrategy == "absolute":
 73                return self.letterSpacing
 74            return self.letterSpacing * (self.fontSize / 1000)
 75        except Exception:
 76            return 0
 77
 78    @property
 79    def fontPath(self) -> str:
 80        """The file path of the font.
 81
 82        Returns:
 83            The path to the font file, or the fontData if path is unavailable.
 84        """
 85        try:
 86            return self.fontData.path
 87        except Exception:
 88            return self.fontData
 89
 90    @property
 91    def isSingleFont(self) -> bool:
 92        """Whether fontData is a single font.
 93
 94        Returns:
 95            True if fontData is a string or KFont instance, False otherwise.
 96        """
 97        # Local import to avoid circular dependency at module level
 98        from classes import KFont
 99
100        return isinstance(self.fontData, (str, KFont))
101
102    @property
103    def openTypeFeatures(self):
104        """OpenType features as a dictionary.
105
106        Returns:
107            Dictionary of OpenType features with bool values, or None if unavailable.
108            When openType is a dict it is returned as-is (supports explicit False).
109            When openType is a tag or list of tags every entry is set to True.
110        """
111        if isinstance(self.openType, dict):
112            return dict(self.openType)
113        try:
114            return {key: True for key in helpers.coerceList(self.openType)}
115        except Exception:
116            return None
117
118    def getArgs(self, clean=False, pick: list[str] = None) -> dict:
119        """
120        Get properties as a dictionary.
121
122        Args:
123            clean: If True, remove falsy properties.
124            pick: List of property keys to include.
125
126        Returns:
127            Dictionary of font properties.
128        """
129        asDict = asdict(self)
130        asDict["lineHeight"] = self.lineHeight
131        asDict["tracking"] = self.tracking
132        asDict["openTypeFeatures"] = self.openTypeFeatures
133
134        if self.align:
135            asDict["align"] = self.align
136
137        if self.isSingleFont:
138            asDict["font"] = self.fontPath
139
140        if pick:
141            asDict = helpers.pick(asDict, pick)
142
143        if clean:
144            return helpers.omitBy(asDict)
145        else:
146            return asDict
147
148    def calc(self, clean=True) -> dict:
149        """
150        Calculate keyword arguments for FormattedString().
151
152        Args:
153            clean: If True, remove falsy properties.
154
155        Returns:
156            Dictionary of keyword arguments for FormattedString().
157        """
158
159        return self.getArgs(
160            clean=clean,
161            pick=[
162                "font",
163                "fontSize",
164                "lineHeight",
165                "tracking",
166                "align",
167                "openTypeFeatures",
168            ],
169        )
170
171    def activateFont(self):
172        """
173        Set the font for DrawBot.
174
175        Returns:
176            The current instance.
177        """
178        if self.isSingleFont:
179            drawBot.font(self.fontPath)
180        elif helpers.isSequence(self.fontData):
181            # If it's a list of fonts, activate the first one (for fallback)
182            drawBot.font(self.fontData[0].path)
183
184        return self
185
186    def unwrapFont(self) -> "KFont":
187        """Return a single KFont from fontData.
188
189        Raises:
190            ValueError: If fontData is None or empty.
191            TypeError: If the resolved item is not a KFont.
192        """
193        from classes import KFont
194
195        fontItems = helpers.coerceList(self.fontData)
196        if not fontItems:
197            raise ValueError("fontData is empty; cannot unwrap a KFont.")
198
199        firstItem = fontItems[0]
200        if not isinstance(firstItem, KFont):
201            raise TypeError(
202                f"Expected KFont as first font item, got {type(firstItem).__name__}."
203            )
204
205        return firstItem
206
207    def apply(self, activateFont: bool = True):
208        """
209        Set DrawBot properties for the current font settings.
210
211        Args:
212            activateFont: Whether to activate the font (default True).
213
214        Returns:
215            The current instance.
216        """
217        drawBot.fontSize(self.fontSize)
218        drawBot.lineHeight(self.lineHeight)
219        drawBot.tracking(self.tracking)
220
221        if activateFont:
222            self.activateFont()
223
224        if self.openTypeFeatures:
225            drawBot.openTypeFeatures(**self.openTypeFeatures)
226        else:
227            drawBot.openTypeFeatures(resetFeatures=True)
228
229        return self
230
231    def describe(self, format: Literal["string", "list"] = "string"):
232        """
233        Return human-friendly font properties.
234
235        Args:
236            format: Return as 'str' or 'list[str]'.
237
238        Returns:
239            Human-readable font properties.
240        """
241
242        props: List[float | int] = [self.fontSize]
243        lh, t = self.lineHeight, self.letterSpacing
244
245        if lh and lh != self.fontSize:
246            props.append(lh)
247        if t:
248            props.append(t)
249
250        rounded = [str(round(p)) for p in props]
251        return rounded if format == "list" else " ".join(rounded)
252
253    def describeFonts(self, includeFamily: bool = True) -> str:
254        """Human-friendly label for the font or font list."""
255        fontItems: list[KFont] = helpers.coerceList(self.fontData)
256        fontExtremes: tuple[KFont] = helpers.pickExtremes(fontItems)
257        fontNames = [font.styleName for font in fontExtremes]
258        separator = "–" if len(fontItems) > 2 else ", "
259
260        styleNames = separator.join(fontNames)
261        if includeFamily:
262            familyName = fontItems[0].familyName
263            return f"{familyName} {styleNames}"
264        else:
265            return styleNames
266
267    def updateFont(self, value: Union["KFont", list["KFont"]]) -> "DFontProps":
268        """
269        Update font and return self.
270
271        Args:
272            value: The new font or list of fonts.
273
274        Example:
275            `props.updateFont(newFont)`
276        """
277        self.fontData = value
278        return self
279
280    def updateFontSize(self, value: int) -> "DFontProps":
281        """
282        Update fontSize and return self.
283
284        Args:
285            value: The new font size.
286
287        Example:
288            `props.updateFontSize(12)`
289        """
290        self.fontSize = value
291        return self
292
293    def makeParagraph(
294        self,
295        paragraphTopSpacing: float = 0,
296        paragraphBottomSpacing: float = 0,
297        indent: float = 0,
298        tailIndent: float = 0,
299    ):
300        """Convert to DParagraphProps with paragraph-level formatting.
301
302        Creates a new DParagraphProps instance with all current font properties
303        plus the specified paragraph formatting properties.
304
305        Args:
306            paragraphTopSpacing: Space before the paragraph in points.
307            paragraphBottomSpacing: Space after the paragraph in points.
308            indent: First-line indent in points.
309            tailIndent: Right margin indent in points.
310
311        Returns:
312            DParagraphProps instance with font and paragraph properties.
313
314        Example:
315            ```python
316            from datatypes import DFontProps
317
318            font_props = DFontProps(fontSize=16, leading=1.5)
319            para_props = font_props.makeParagraph(paragraphTopSpacing=20, indent=10)
320
321            # Chain with app helper:
322            props = app.getStyleProps('tiny').makeParagraph(paragraphTopSpacing=15)
323            ```
324        """
325        from .data_paragraphprops import DParagraphProps
326
327        return DParagraphProps(
328            fontSize=self.fontSize,
329            leading=self.leading,
330            letterSpacing=self.letterSpacing,
331            trackingStrategy=self.trackingStrategy,
332            fontData=self.fontData,
333            align=self.align,
334            openType=self.openType,
335            paragraphTopSpacing=paragraphTopSpacing,
336            paragraphBottomSpacing=paragraphBottomSpacing,
337            indent=indent,
338            tailIndent=tailIndent,
339        )
TrackingStrategy = typing.Literal['relative', 'absolute']

How letterSpacing input is interpreted: "relative" (default) scales by font size like Adobe tracking, "absolute" uses letterSpacing as absolute points like Sketch and CSS.

@dataclass
class DFontProps:
 22@dataclass
 23class DFontProps:
 24    """Data structure for font properties used in DrawBot.
 25
 26    Args:
 27        fontSize: The font size in points.
 28        leading: The leading (line spacing) multiplier.
 29        letterSpacing: Tracking input value (relative by default, absolute if trackingStrategy="absolute").
 30        trackingStrategy: Tracking strategy: "relative" for Adobe-style, "absolute" for Sketch-style.
 31        fontData: The font data or list of font data.
 32        align: Text alignment option.
 33        openType: OpenType font features.
 34    """
 35
 36    fontSize: int = 24
 37    leading: float = 1.25
 38    letterSpacing: int = 0
 39    trackingStrategy: TrackingStrategy = "relative"
 40    fontData: "KFont | DFontList | list[KFont]" = None
 41    align: fonts.TextAlign = None
 42    openType: fonts.FeatureId | list[fonts.FeatureId] | dict[fonts.FeatureId, bool] = (
 43        None
 44    )
 45
 46    @property
 47    def lineHeight(self):
 48        """The calculated line height.
 49
 50        Returns:
 51            The product of fontSize and leading, or None if fontSize undefined.
 52        """
 53        try:
 54            return self.fontSize * self.leading
 55        except Exception:
 56            return
 57
 58    @cached_property
 59    def blockHeight(self) -> float:
 60        """The height of one line of text."""
 61        with drawBot.savedState():
 62            self.apply()
 63            _, h = drawBot.textSize("H")
 64        return h
 65
 66    @property
 67    def tracking(self) -> float:
 68        """Tracking value in DrawBot points.
 69
 70        Default behavior matches Adobe-style relative tracking where `letterSpacing` units scale by font size. Set `trackingStrategy="absolute"` to use Sketch-style absolute tracking in points.
 71        """
 72        try:
 73            if self.trackingStrategy == "absolute":
 74                return self.letterSpacing
 75            return self.letterSpacing * (self.fontSize / 1000)
 76        except Exception:
 77            return 0
 78
 79    @property
 80    def fontPath(self) -> str:
 81        """The file path of the font.
 82
 83        Returns:
 84            The path to the font file, or the fontData if path is unavailable.
 85        """
 86        try:
 87            return self.fontData.path
 88        except Exception:
 89            return self.fontData
 90
 91    @property
 92    def isSingleFont(self) -> bool:
 93        """Whether fontData is a single font.
 94
 95        Returns:
 96            True if fontData is a string or KFont instance, False otherwise.
 97        """
 98        # Local import to avoid circular dependency at module level
 99        from classes import KFont
100
101        return isinstance(self.fontData, (str, KFont))
102
103    @property
104    def openTypeFeatures(self):
105        """OpenType features as a dictionary.
106
107        Returns:
108            Dictionary of OpenType features with bool values, or None if unavailable.
109            When openType is a dict it is returned as-is (supports explicit False).
110            When openType is a tag or list of tags every entry is set to True.
111        """
112        if isinstance(self.openType, dict):
113            return dict(self.openType)
114        try:
115            return {key: True for key in helpers.coerceList(self.openType)}
116        except Exception:
117            return None
118
119    def getArgs(self, clean=False, pick: list[str] = None) -> dict:
120        """
121        Get properties as a dictionary.
122
123        Args:
124            clean: If True, remove falsy properties.
125            pick: List of property keys to include.
126
127        Returns:
128            Dictionary of font properties.
129        """
130        asDict = asdict(self)
131        asDict["lineHeight"] = self.lineHeight
132        asDict["tracking"] = self.tracking
133        asDict["openTypeFeatures"] = self.openTypeFeatures
134
135        if self.align:
136            asDict["align"] = self.align
137
138        if self.isSingleFont:
139            asDict["font"] = self.fontPath
140
141        if pick:
142            asDict = helpers.pick(asDict, pick)
143
144        if clean:
145            return helpers.omitBy(asDict)
146        else:
147            return asDict
148
149    def calc(self, clean=True) -> dict:
150        """
151        Calculate keyword arguments for FormattedString().
152
153        Args:
154            clean: If True, remove falsy properties.
155
156        Returns:
157            Dictionary of keyword arguments for FormattedString().
158        """
159
160        return self.getArgs(
161            clean=clean,
162            pick=[
163                "font",
164                "fontSize",
165                "lineHeight",
166                "tracking",
167                "align",
168                "openTypeFeatures",
169            ],
170        )
171
172    def activateFont(self):
173        """
174        Set the font for DrawBot.
175
176        Returns:
177            The current instance.
178        """
179        if self.isSingleFont:
180            drawBot.font(self.fontPath)
181        elif helpers.isSequence(self.fontData):
182            # If it's a list of fonts, activate the first one (for fallback)
183            drawBot.font(self.fontData[0].path)
184
185        return self
186
187    def unwrapFont(self) -> "KFont":
188        """Return a single KFont from fontData.
189
190        Raises:
191            ValueError: If fontData is None or empty.
192            TypeError: If the resolved item is not a KFont.
193        """
194        from classes import KFont
195
196        fontItems = helpers.coerceList(self.fontData)
197        if not fontItems:
198            raise ValueError("fontData is empty; cannot unwrap a KFont.")
199
200        firstItem = fontItems[0]
201        if not isinstance(firstItem, KFont):
202            raise TypeError(
203                f"Expected KFont as first font item, got {type(firstItem).__name__}."
204            )
205
206        return firstItem
207
208    def apply(self, activateFont: bool = True):
209        """
210        Set DrawBot properties for the current font settings.
211
212        Args:
213            activateFont: Whether to activate the font (default True).
214
215        Returns:
216            The current instance.
217        """
218        drawBot.fontSize(self.fontSize)
219        drawBot.lineHeight(self.lineHeight)
220        drawBot.tracking(self.tracking)
221
222        if activateFont:
223            self.activateFont()
224
225        if self.openTypeFeatures:
226            drawBot.openTypeFeatures(**self.openTypeFeatures)
227        else:
228            drawBot.openTypeFeatures(resetFeatures=True)
229
230        return self
231
232    def describe(self, format: Literal["string", "list"] = "string"):
233        """
234        Return human-friendly font properties.
235
236        Args:
237            format: Return as 'str' or 'list[str]'.
238
239        Returns:
240            Human-readable font properties.
241        """
242
243        props: List[float | int] = [self.fontSize]
244        lh, t = self.lineHeight, self.letterSpacing
245
246        if lh and lh != self.fontSize:
247            props.append(lh)
248        if t:
249            props.append(t)
250
251        rounded = [str(round(p)) for p in props]
252        return rounded if format == "list" else " ".join(rounded)
253
254    def describeFonts(self, includeFamily: bool = True) -> str:
255        """Human-friendly label for the font or font list."""
256        fontItems: list[KFont] = helpers.coerceList(self.fontData)
257        fontExtremes: tuple[KFont] = helpers.pickExtremes(fontItems)
258        fontNames = [font.styleName for font in fontExtremes]
259        separator = "–" if len(fontItems) > 2 else ", "
260
261        styleNames = separator.join(fontNames)
262        if includeFamily:
263            familyName = fontItems[0].familyName
264            return f"{familyName} {styleNames}"
265        else:
266            return styleNames
267
268    def updateFont(self, value: Union["KFont", list["KFont"]]) -> "DFontProps":
269        """
270        Update font and return self.
271
272        Args:
273            value: The new font or list of fonts.
274
275        Example:
276            `props.updateFont(newFont)`
277        """
278        self.fontData = value
279        return self
280
281    def updateFontSize(self, value: int) -> "DFontProps":
282        """
283        Update fontSize and return self.
284
285        Args:
286            value: The new font size.
287
288        Example:
289            `props.updateFontSize(12)`
290        """
291        self.fontSize = value
292        return self
293
294    def makeParagraph(
295        self,
296        paragraphTopSpacing: float = 0,
297        paragraphBottomSpacing: float = 0,
298        indent: float = 0,
299        tailIndent: float = 0,
300    ):
301        """Convert to DParagraphProps with paragraph-level formatting.
302
303        Creates a new DParagraphProps instance with all current font properties
304        plus the specified paragraph formatting properties.
305
306        Args:
307            paragraphTopSpacing: Space before the paragraph in points.
308            paragraphBottomSpacing: Space after the paragraph in points.
309            indent: First-line indent in points.
310            tailIndent: Right margin indent in points.
311
312        Returns:
313            DParagraphProps instance with font and paragraph properties.
314
315        Example:
316            ```python
317            from datatypes import DFontProps
318
319            font_props = DFontProps(fontSize=16, leading=1.5)
320            para_props = font_props.makeParagraph(paragraphTopSpacing=20, indent=10)
321
322            # Chain with app helper:
323            props = app.getStyleProps('tiny').makeParagraph(paragraphTopSpacing=15)
324            ```
325        """
326        from .data_paragraphprops import DParagraphProps
327
328        return DParagraphProps(
329            fontSize=self.fontSize,
330            leading=self.leading,
331            letterSpacing=self.letterSpacing,
332            trackingStrategy=self.trackingStrategy,
333            fontData=self.fontData,
334            align=self.align,
335            openType=self.openType,
336            paragraphTopSpacing=paragraphTopSpacing,
337            paragraphBottomSpacing=paragraphBottomSpacing,
338            indent=indent,
339            tailIndent=tailIndent,
340        )

Data structure for font properties used in DrawBot.

Arguments:
  • fontSize: The font size in points.
  • leading: The leading (line spacing) multiplier.
  • letterSpacing: Tracking input value (relative by default, absolute if trackingStrategy="absolute").
  • trackingStrategy: Tracking strategy: "relative" for Adobe-style, "absolute" for Sketch-style.
  • fontData: The font data or list of font data.
  • align: Text alignment option.
  • openType: OpenType font features.
DFontProps( fontSize: int = 24, leading: float = 1.25, letterSpacing: int = 0, trackingStrategy: Literal['relative', 'absolute'] = 'relative', fontData: classes.c10_font.KFont | datatypes.data_fontlist.DFontList | list[classes.c10_font.KFont] = None, align: Literal['left', 'center', 'right'] = None, openType: Union[Literal['liga', 'dlig', 'calt', 'locl', 'titl', 'case', 'pnum', 'lnum', 'onum', 'tnum', 'zero', 'subs', 'sups', 'numr', 'dnom', 'frac', 'ordn', 'kern', 'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'ss08', 'ss09', 'ss10'], list[Literal['liga', 'dlig', 'calt', 'locl', 'titl', 'case', 'pnum', 'lnum', 'onum', 'tnum', 'zero', 'subs', 'sups', 'numr', 'dnom', 'frac', 'ordn', 'kern', 'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'ss08', 'ss09', 'ss10']], dict[Literal['liga', 'dlig', 'calt', 'locl', 'titl', 'case', 'pnum', 'lnum', 'onum', 'tnum', 'zero', 'subs', 'sups', 'numr', 'dnom', 'frac', 'ordn', 'kern', 'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'ss08', 'ss09', 'ss10'], bool]] = None)
fontSize: int = 24
leading: float = 1.25
letterSpacing: int = 0
trackingStrategy: Literal['relative', 'absolute'] = 'relative'
align: Literal['left', 'center', 'right'] = None
openType: Union[Literal['liga', 'dlig', 'calt', 'locl', 'titl', 'case', 'pnum', 'lnum', 'onum', 'tnum', 'zero', 'subs', 'sups', 'numr', 'dnom', 'frac', 'ordn', 'kern', 'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'ss08', 'ss09', 'ss10'], list[Literal['liga', 'dlig', 'calt', 'locl', 'titl', 'case', 'pnum', 'lnum', 'onum', 'tnum', 'zero', 'subs', 'sups', 'numr', 'dnom', 'frac', 'ordn', 'kern', 'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'ss08', 'ss09', 'ss10']], dict[Literal['liga', 'dlig', 'calt', 'locl', 'titl', 'case', 'pnum', 'lnum', 'onum', 'tnum', 'zero', 'subs', 'sups', 'numr', 'dnom', 'frac', 'ordn', 'kern', 'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'ss08', 'ss09', 'ss10'], bool]] = None
lineHeight
46    @property
47    def lineHeight(self):
48        """The calculated line height.
49
50        Returns:
51            The product of fontSize and leading, or None if fontSize undefined.
52        """
53        try:
54            return self.fontSize * self.leading
55        except Exception:
56            return

The calculated line height.

Returns:

The product of fontSize and leading, or None if fontSize undefined.

blockHeight: float
58    @cached_property
59    def blockHeight(self) -> float:
60        """The height of one line of text."""
61        with drawBot.savedState():
62            self.apply()
63            _, h = drawBot.textSize("H")
64        return h

The height of one line of text.

tracking: float
66    @property
67    def tracking(self) -> float:
68        """Tracking value in DrawBot points.
69
70        Default behavior matches Adobe-style relative tracking where `letterSpacing` units scale by font size. Set `trackingStrategy="absolute"` to use Sketch-style absolute tracking in points.
71        """
72        try:
73            if self.trackingStrategy == "absolute":
74                return self.letterSpacing
75            return self.letterSpacing * (self.fontSize / 1000)
76        except Exception:
77            return 0

Tracking value in DrawBot points.

Default behavior matches Adobe-style relative tracking where letterSpacing units scale by font size. Set trackingStrategy="absolute" to use Sketch-style absolute tracking in points.

fontPath: str
79    @property
80    def fontPath(self) -> str:
81        """The file path of the font.
82
83        Returns:
84            The path to the font file, or the fontData if path is unavailable.
85        """
86        try:
87            return self.fontData.path
88        except Exception:
89            return self.fontData

The file path of the font.

Returns:

The path to the font file, or the fontData if path is unavailable.

isSingleFont: bool
 91    @property
 92    def isSingleFont(self) -> bool:
 93        """Whether fontData is a single font.
 94
 95        Returns:
 96            True if fontData is a string or KFont instance, False otherwise.
 97        """
 98        # Local import to avoid circular dependency at module level
 99        from classes import KFont
100
101        return isinstance(self.fontData, (str, KFont))

Whether fontData is a single font.

Returns:

True if fontData is a string or KFont instance, False otherwise.

openTypeFeatures
103    @property
104    def openTypeFeatures(self):
105        """OpenType features as a dictionary.
106
107        Returns:
108            Dictionary of OpenType features with bool values, or None if unavailable.
109            When openType is a dict it is returned as-is (supports explicit False).
110            When openType is a tag or list of tags every entry is set to True.
111        """
112        if isinstance(self.openType, dict):
113            return dict(self.openType)
114        try:
115            return {key: True for key in helpers.coerceList(self.openType)}
116        except Exception:
117            return None

OpenType features as a dictionary.

Returns:

Dictionary of OpenType features with bool values, or None if unavailable. When openType is a dict it is returned as-is (supports explicit False). When openType is a tag or list of tags every entry is set to True.

def getArgs(self, clean=False, pick: list[str] = None) -> dict:
119    def getArgs(self, clean=False, pick: list[str] = None) -> dict:
120        """
121        Get properties as a dictionary.
122
123        Args:
124            clean: If True, remove falsy properties.
125            pick: List of property keys to include.
126
127        Returns:
128            Dictionary of font properties.
129        """
130        asDict = asdict(self)
131        asDict["lineHeight"] = self.lineHeight
132        asDict["tracking"] = self.tracking
133        asDict["openTypeFeatures"] = self.openTypeFeatures
134
135        if self.align:
136            asDict["align"] = self.align
137
138        if self.isSingleFont:
139            asDict["font"] = self.fontPath
140
141        if pick:
142            asDict = helpers.pick(asDict, pick)
143
144        if clean:
145            return helpers.omitBy(asDict)
146        else:
147            return asDict

Get properties as a dictionary.

Arguments:
  • clean: If True, remove falsy properties.
  • pick: List of property keys to include.
Returns:

Dictionary of font properties.

def calc(self, clean=True) -> dict:
149    def calc(self, clean=True) -> dict:
150        """
151        Calculate keyword arguments for FormattedString().
152
153        Args:
154            clean: If True, remove falsy properties.
155
156        Returns:
157            Dictionary of keyword arguments for FormattedString().
158        """
159
160        return self.getArgs(
161            clean=clean,
162            pick=[
163                "font",
164                "fontSize",
165                "lineHeight",
166                "tracking",
167                "align",
168                "openTypeFeatures",
169            ],
170        )

Calculate keyword arguments for FormattedString().

Arguments:
  • clean: If True, remove falsy properties.
Returns:

Dictionary of keyword arguments for FormattedString().

def activateFont(self):
172    def activateFont(self):
173        """
174        Set the font for DrawBot.
175
176        Returns:
177            The current instance.
178        """
179        if self.isSingleFont:
180            drawBot.font(self.fontPath)
181        elif helpers.isSequence(self.fontData):
182            # If it's a list of fonts, activate the first one (for fallback)
183            drawBot.font(self.fontData[0].path)
184
185        return self

Set the font for DrawBot.

Returns:

The current instance.

def unwrapFont(self) -> classes.c10_font.KFont:
187    def unwrapFont(self) -> "KFont":
188        """Return a single KFont from fontData.
189
190        Raises:
191            ValueError: If fontData is None or empty.
192            TypeError: If the resolved item is not a KFont.
193        """
194        from classes import KFont
195
196        fontItems = helpers.coerceList(self.fontData)
197        if not fontItems:
198            raise ValueError("fontData is empty; cannot unwrap a KFont.")
199
200        firstItem = fontItems[0]
201        if not isinstance(firstItem, KFont):
202            raise TypeError(
203                f"Expected KFont as first font item, got {type(firstItem).__name__}."
204            )
205
206        return firstItem

Return a single KFont from fontData.

Raises:
  • ValueError: If fontData is None or empty.
  • TypeError: If the resolved item is not a KFont.
def apply(self, activateFont: bool = True):
208    def apply(self, activateFont: bool = True):
209        """
210        Set DrawBot properties for the current font settings.
211
212        Args:
213            activateFont: Whether to activate the font (default True).
214
215        Returns:
216            The current instance.
217        """
218        drawBot.fontSize(self.fontSize)
219        drawBot.lineHeight(self.lineHeight)
220        drawBot.tracking(self.tracking)
221
222        if activateFont:
223            self.activateFont()
224
225        if self.openTypeFeatures:
226            drawBot.openTypeFeatures(**self.openTypeFeatures)
227        else:
228            drawBot.openTypeFeatures(resetFeatures=True)
229
230        return self

Set DrawBot properties for the current font settings.

Arguments:
  • activateFont: Whether to activate the font (default True).
Returns:

The current instance.

def describe(self, format: Literal['string', 'list'] = 'string'):
232    def describe(self, format: Literal["string", "list"] = "string"):
233        """
234        Return human-friendly font properties.
235
236        Args:
237            format: Return as 'str' or 'list[str]'.
238
239        Returns:
240            Human-readable font properties.
241        """
242
243        props: List[float | int] = [self.fontSize]
244        lh, t = self.lineHeight, self.letterSpacing
245
246        if lh and lh != self.fontSize:
247            props.append(lh)
248        if t:
249            props.append(t)
250
251        rounded = [str(round(p)) for p in props]
252        return rounded if format == "list" else " ".join(rounded)

Return human-friendly font properties.

Arguments:
  • format: Return as 'str' or 'list[str]'.
Returns:

Human-readable font properties.

def describeFonts(self, includeFamily: bool = True) -> str:
254    def describeFonts(self, includeFamily: bool = True) -> str:
255        """Human-friendly label for the font or font list."""
256        fontItems: list[KFont] = helpers.coerceList(self.fontData)
257        fontExtremes: tuple[KFont] = helpers.pickExtremes(fontItems)
258        fontNames = [font.styleName for font in fontExtremes]
259        separator = "–" if len(fontItems) > 2 else ", "
260
261        styleNames = separator.join(fontNames)
262        if includeFamily:
263            familyName = fontItems[0].familyName
264            return f"{familyName} {styleNames}"
265        else:
266            return styleNames

Human-friendly label for the font or font list.

def updateFont( self, value: Union[classes.c10_font.KFont, list[classes.c10_font.KFont]]) -> DFontProps:
268    def updateFont(self, value: Union["KFont", list["KFont"]]) -> "DFontProps":
269        """
270        Update font and return self.
271
272        Args:
273            value: The new font or list of fonts.
274
275        Example:
276            `props.updateFont(newFont)`
277        """
278        self.fontData = value
279        return self

Update font and return self.

Arguments:
  • value: The new font or list of fonts.
Example:

props.updateFont(newFont)

def updateFontSize(self, value: int) -> DFontProps:
281    def updateFontSize(self, value: int) -> "DFontProps":
282        """
283        Update fontSize and return self.
284
285        Args:
286            value: The new font size.
287
288        Example:
289            `props.updateFontSize(12)`
290        """
291        self.fontSize = value
292        return self

Update fontSize and return self.

Arguments:
  • value: The new font size.
Example:

props.updateFontSize(12)

def makeParagraph( self, paragraphTopSpacing: float = 0, paragraphBottomSpacing: float = 0, indent: float = 0, tailIndent: float = 0):
294    def makeParagraph(
295        self,
296        paragraphTopSpacing: float = 0,
297        paragraphBottomSpacing: float = 0,
298        indent: float = 0,
299        tailIndent: float = 0,
300    ):
301        """Convert to DParagraphProps with paragraph-level formatting.
302
303        Creates a new DParagraphProps instance with all current font properties
304        plus the specified paragraph formatting properties.
305
306        Args:
307            paragraphTopSpacing: Space before the paragraph in points.
308            paragraphBottomSpacing: Space after the paragraph in points.
309            indent: First-line indent in points.
310            tailIndent: Right margin indent in points.
311
312        Returns:
313            DParagraphProps instance with font and paragraph properties.
314
315        Example:
316            ```python
317            from datatypes import DFontProps
318
319            font_props = DFontProps(fontSize=16, leading=1.5)
320            para_props = font_props.makeParagraph(paragraphTopSpacing=20, indent=10)
321
322            # Chain with app helper:
323            props = app.getStyleProps('tiny').makeParagraph(paragraphTopSpacing=15)
324            ```
325        """
326        from .data_paragraphprops import DParagraphProps
327
328        return DParagraphProps(
329            fontSize=self.fontSize,
330            leading=self.leading,
331            letterSpacing=self.letterSpacing,
332            trackingStrategy=self.trackingStrategy,
333            fontData=self.fontData,
334            align=self.align,
335            openType=self.openType,
336            paragraphTopSpacing=paragraphTopSpacing,
337            paragraphBottomSpacing=paragraphBottomSpacing,
338            indent=indent,
339            tailIndent=tailIndent,
340        )

Convert to DParagraphProps with paragraph-level formatting.

Creates a new DParagraphProps instance with all current font properties plus the specified paragraph formatting properties.

Arguments:
  • paragraphTopSpacing: Space before the paragraph in points.
  • paragraphBottomSpacing: Space after the paragraph in points.
  • indent: First-line indent in points.
  • tailIndent: Right margin indent in points.
Returns:

DParagraphProps instance with font and paragraph properties.

Example:
from datatypes import DFontProps

font_props = DFontProps(fontSize=16, leading=1.5)
para_props = font_props.makeParagraph(paragraphTopSpacing=20, indent=10)

# Chain with app helper:
props = app.getStyleProps('tiny').makeParagraph(paragraphTopSpacing=15)