1from enum import IntEnum 2from typing import Any, List, Optional, Sequence, Type, TypeVar, Union 3 4import attr 5from fontTools.ufoLib import UFOReader 6 7from ufoLib2.objects.guideline import Guideline 8from ufoLib2.objects.misc import AttrDictMixin 9 10__all__ = ("Info", "GaspRangeRecord", "NameRecord", "WidthClass") 11 12 13def _positive(instance: Any, attribute: Any, value: int) -> None: 14 if value < 0: 15 raise ValueError( 16 "'{name}' must be at least 0 (got {value!r})".format( 17 name=attribute.name, value=value 18 ) 19 ) 20 21 22_optional_positive = attr.validators.optional(_positive) 23 24 25# or maybe use IntFlag? 26class GaspBehavior(IntEnum): 27 GRIDFIT = 0 28 DOGRAY = 1 29 SYMMETRIC_GRIDFIT = 2 30 SYMMETRIC_SMOOTHING = 3 31 32 33def _convert_GaspBehavior( 34 seq: Sequence[Union[GaspBehavior, int]] 35) -> List[GaspBehavior]: 36 return [v if isinstance(v, GaspBehavior) else GaspBehavior(v) for v in seq] 37 38 39@attr.s(auto_attribs=True, slots=True) 40class GaspRangeRecord(AttrDictMixin): 41 rangeMaxPPEM: int = attr.ib(validator=_positive) 42 # Use Set[GaspBehavior] instead of List? 43 rangeGaspBehavior: List[GaspBehavior] = attr.ib(converter=_convert_GaspBehavior) 44 45 46@attr.s(auto_attribs=True, slots=True) 47class NameRecord(AttrDictMixin): 48 nameID: int = attr.ib(validator=_positive) 49 platformID: int = attr.ib(validator=_positive) 50 encodingID: int = attr.ib(validator=_positive) 51 languageID: int = attr.ib(validator=_positive) 52 string: str = "" 53 54 55class WidthClass(IntEnum): 56 ULTRA_CONDENSED = 1 57 EXTRA_CONDESED = 2 58 CONDENSED = 3 59 SEMI_CONDENSED = 4 60 NORMAL = 5 # alias for WidthClass.MEDIUM 61 MEDIUM = 5 62 SEMI_EXPANDED = 6 63 EXPANDED = 7 64 EXTRA_EXPANDED = 8 65 ULTRA_EXPANDED = 9 66 67 68Tc = TypeVar("Tc", Guideline, GaspRangeRecord, NameRecord) 69 70 71def _convert_optional_list( 72 lst: Optional[Sequence[Any]], klass: Type[Tc] 73) -> Optional[List[Tc]]: 74 if lst is None: 75 return None 76 result = [] 77 for d in lst: 78 if isinstance(d, klass): 79 result.append(d) 80 else: 81 result.append(klass(**d)) 82 return result 83 84 85def _convert_guidelines( 86 values: Optional[Sequence[Union[Guideline, Any]]] 87) -> Optional[List[Guideline]]: 88 return _convert_optional_list(values, Guideline) 89 90 91def _convert_gasp_range_records( 92 values: Optional[Sequence[Union[GaspRangeRecord, Any]]] 93) -> Optional[List[GaspRangeRecord]]: 94 return _convert_optional_list(values, GaspRangeRecord) 95 96 97def _convert_name_records( 98 values: Optional[Sequence[Union[NameRecord, Any]]] 99) -> Optional[List[NameRecord]]: 100 return _convert_optional_list(values, NameRecord) 101 102 103def _convert_WidthClass(value: Optional[int]) -> Optional[WidthClass]: 104 return None if value is None else WidthClass(value) 105 106 107@attr.s(auto_attribs=True, slots=True) 108class Info: 109 """A data class representing the contents of fontinfo.plist. 110 111 The attributes are formally specified at 112 http://unifiedfontobject.org/versions/ufo3/fontinfo.plist/. Value validation is 113 mostly done during saving and loading. 114 """ 115 116 familyName: Optional[str] = None 117 styleName: Optional[str] = None 118 styleMapFamilyName: Optional[str] = None 119 styleMapStyleName: Optional[str] = None 120 versionMajor: Optional[int] = attr.ib(default=None, validator=_optional_positive) 121 versionMinor: Optional[int] = attr.ib(default=None, validator=_optional_positive) 122 123 copyright: Optional[str] = None 124 trademark: Optional[str] = None 125 126 unitsPerEm: Optional[float] = attr.ib(default=None, validator=_optional_positive) 127 descender: Optional[float] = None 128 xHeight: Optional[float] = None 129 capHeight: Optional[float] = None 130 ascender: Optional[float] = None 131 italicAngle: Optional[float] = None 132 133 note: Optional[str] = None 134 135 _guidelines: Optional[List[Guideline]] = attr.ib( 136 default=None, converter=_convert_guidelines 137 ) 138 139 @property 140 def guidelines(self) -> Optional[List[Guideline]]: 141 return self._guidelines 142 143 @guidelines.setter 144 def guidelines(self, value: Optional[List[Guideline]]) -> None: 145 self._guidelines = _convert_guidelines(value) 146 147 _openTypeGaspRangeRecords: Optional[List[GaspRangeRecord]] = attr.ib( 148 default=None, converter=_convert_gasp_range_records 149 ) 150 151 @property 152 def openTypeGaspRangeRecords(self) -> Optional[List[GaspRangeRecord]]: 153 return self._openTypeGaspRangeRecords 154 155 @openTypeGaspRangeRecords.setter 156 def openTypeGaspRangeRecords(self, value: Optional[List[GaspRangeRecord]]) -> None: 157 self._openTypeGaspRangeRecords = _convert_gasp_range_records(value) 158 159 openTypeHeadCreated: Optional[str] = None 160 openTypeHeadLowestRecPPEM: Optional[int] = attr.ib( 161 default=None, validator=_optional_positive 162 ) 163 openTypeHeadFlags: Optional[List[int]] = None 164 165 openTypeHheaAscender: Optional[int] = None 166 openTypeHheaDescender: Optional[int] = None 167 openTypeHheaLineGap: Optional[int] = None 168 openTypeHheaCaretSlopeRise: Optional[int] = None 169 openTypeHheaCaretSlopeRun: Optional[int] = None 170 openTypeHheaCaretOffset: Optional[int] = None 171 172 openTypeNameDesigner: Optional[str] = None 173 openTypeNameDesignerURL: Optional[str] = None 174 openTypeNameManufacturer: Optional[str] = None 175 openTypeNameManufacturerURL: Optional[str] = None 176 openTypeNameLicense: Optional[str] = None 177 openTypeNameLicenseURL: Optional[str] = None 178 openTypeNameVersion: Optional[str] = None 179 openTypeNameUniqueID: Optional[str] = None 180 openTypeNameDescription: Optional[str] = None 181 openTypeNamePreferredFamilyName: Optional[str] = None 182 openTypeNamePreferredSubfamilyName: Optional[str] = None 183 openTypeNameCompatibleFullName: Optional[str] = None 184 openTypeNameSampleText: Optional[str] = None 185 openTypeNameWWSFamilyName: Optional[str] = None 186 openTypeNameWWSSubfamilyName: Optional[str] = None 187 188 _openTypeNameRecords: Optional[List[NameRecord]] = attr.ib( 189 default=None, converter=_convert_name_records 190 ) 191 192 @property 193 def openTypeNameRecords(self) -> Optional[List[NameRecord]]: 194 return self._openTypeNameRecords 195 196 @openTypeNameRecords.setter 197 def openTypeNameRecords(self, value: Optional[List[NameRecord]]) -> None: 198 self._openTypeNameRecords = _convert_name_records(value) 199 200 _openTypeOS2WidthClass: Optional[WidthClass] = attr.ib( 201 default=None, converter=_convert_WidthClass 202 ) 203 204 @property 205 def openTypeOS2WidthClass(self) -> Optional[WidthClass]: 206 return self._openTypeOS2WidthClass 207 208 @openTypeOS2WidthClass.setter 209 def openTypeOS2WidthClass(self, value: Optional[WidthClass]) -> None: 210 self._openTypeOS2WidthClass = value if value is None else WidthClass(value) 211 212 openTypeOS2WeightClass: Optional[int] = attr.ib(default=None) 213 214 @openTypeOS2WeightClass.validator 215 def _validate_weight_class(self, attribute: Any, value: Optional[int]) -> None: 216 if value is not None and (value < 1 or value > 1000): 217 raise ValueError("'openTypeOS2WeightClass' must be between 1 and 1000") 218 219 openTypeOS2Selection: Optional[List[int]] = None 220 openTypeOS2VendorID: Optional[str] = None 221 openTypeOS2Panose: Optional[List[int]] = None 222 openTypeOS2FamilyClass: Optional[List[int]] = None 223 openTypeOS2UnicodeRanges: Optional[List[int]] = None 224 openTypeOS2CodePageRanges: Optional[List[int]] = None 225 openTypeOS2TypoAscender: Optional[int] = None 226 openTypeOS2TypoDescender: Optional[int] = None 227 openTypeOS2TypoLineGap: Optional[int] = None 228 openTypeOS2WinAscent: Optional[int] = attr.ib( 229 default=None, validator=_optional_positive 230 ) 231 openTypeOS2WinDescent: Optional[int] = attr.ib( 232 default=None, validator=_optional_positive 233 ) 234 openTypeOS2Type: Optional[List[int]] = None 235 openTypeOS2SubscriptXSize: Optional[int] = None 236 openTypeOS2SubscriptYSize: Optional[int] = None 237 openTypeOS2SubscriptXOffset: Optional[int] = None 238 openTypeOS2SubscriptYOffset: Optional[int] = None 239 openTypeOS2SuperscriptXSize: Optional[int] = None 240 openTypeOS2SuperscriptYSize: Optional[int] = None 241 openTypeOS2SuperscriptXOffset: Optional[int] = None 242 openTypeOS2SuperscriptYOffset: Optional[int] = None 243 openTypeOS2StrikeoutSize: Optional[int] = None 244 openTypeOS2StrikeoutPosition: Optional[int] = None 245 246 openTypeVheaVertTypoAscender: Optional[int] = None 247 openTypeVheaVertTypoDescender: Optional[int] = None 248 openTypeVheaVertTypoLineGap: Optional[int] = None 249 openTypeVheaCaretSlopeRise: Optional[int] = None 250 openTypeVheaCaretSlopeRun: Optional[int] = None 251 openTypeVheaCaretOffset: Optional[int] = None 252 253 postscriptFontName: Optional[str] = None 254 postscriptFullName: Optional[str] = None 255 postscriptSlantAngle: Optional[float] = None 256 postscriptUniqueID: Optional[int] = None 257 postscriptUnderlineThickness: Optional[float] = None 258 postscriptUnderlinePosition: Optional[float] = None 259 postscriptIsFixedPitch: Optional[bool] = None 260 postscriptBlueValues: Optional[List[float]] = None 261 postscriptOtherBlues: Optional[List[float]] = None 262 postscriptFamilyBlues: Optional[List[float]] = None 263 postscriptFamilyOtherBlues: Optional[List[float]] = None 264 postscriptStemSnapH: Optional[List[float]] = None 265 postscriptStemSnapV: Optional[List[float]] = None 266 postscriptBlueFuzz: Optional[float] = None 267 postscriptBlueShift: Optional[float] = None 268 postscriptBlueScale: Optional[float] = None 269 postscriptForceBold: Optional[bool] = None 270 postscriptDefaultWidthX: Optional[float] = None 271 postscriptNominalWidthX: Optional[float] = None 272 postscriptWeightName: Optional[str] = None 273 postscriptDefaultCharacter: Optional[str] = None 274 postscriptWindowsCharacterSet: Optional[str] = None 275 276 # old stuff 277 macintoshFONDName: Optional[str] = None 278 macintoshFONDFamilyID: Optional[int] = None 279 year: Optional[int] = None 280 281 @classmethod 282 def read(cls, reader: UFOReader) -> "Info": 283 """Instantiates a Info object from a 284 :class:`fontTools.ufoLib.UFOReader`.""" 285 self = cls() 286 reader.readInfo(self) 287 return self 288