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