xref: /aosp_15_r20/external/fonttools/Lib/fontTools/varLib/errors.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1import textwrap
2
3
4class VarLibError(Exception):
5    """Base exception for the varLib module."""
6
7
8class VarLibValidationError(VarLibError):
9    """Raised when input data is invalid from varLib's point of view."""
10
11
12class VarLibMergeError(VarLibError):
13    """Raised when input data cannot be merged into a variable font."""
14
15    def __init__(self, merger=None, **kwargs):
16        self.merger = merger
17        if not kwargs:
18            kwargs = {}
19        if "stack" in kwargs:
20            self.stack = kwargs["stack"]
21            del kwargs["stack"]
22        else:
23            self.stack = []
24        self.cause = kwargs
25
26    @property
27    def reason(self):
28        return self.__doc__
29
30    def _master_name(self, ix):
31        if self.merger is not None:
32            ttf = self.merger.ttfs[ix]
33            if "name" in ttf and ttf["name"].getBestFullName():
34                return ttf["name"].getBestFullName()
35            elif hasattr(ttf.reader, "file") and hasattr(ttf.reader.file, "name"):
36                return ttf.reader.file.name
37        return f"master number {ix}"
38
39    @property
40    def offender(self):
41        if "expected" in self.cause and "got" in self.cause:
42            index = [x == self.cause["expected"] for x in self.cause["got"]].index(
43                False
44            )
45            master_name = self._master_name(index)
46            if "location" in self.cause:
47                master_name = f"{master_name} ({self.cause['location']})"
48            return index, master_name
49        return None, None
50
51    @property
52    def details(self):
53        if "expected" in self.cause and "got" in self.cause:
54            offender_index, offender = self.offender
55            got = self.cause["got"][offender_index]
56            return f"Expected to see {self.stack[0]}=={self.cause['expected']!r}, instead saw {got!r}\n"
57        return ""
58
59    def __str__(self):
60        offender_index, offender = self.offender
61        location = ""
62        if offender:
63            location = f"\n\nThe problem is likely to be in {offender}:\n"
64        context = "".join(reversed(self.stack))
65        basic = textwrap.fill(
66            f"Couldn't merge the fonts, because {self.reason}. "
67            f"This happened while performing the following operation: {context}",
68            width=78,
69        )
70        return "\n\n" + basic + location + self.details
71
72
73class ShouldBeConstant(VarLibMergeError):
74    """some values were different, but should have been the same"""
75
76    @property
77    def details(self):
78        basic_message = super().details
79
80        if self.stack[0] != ".FeatureCount" or self.merger is None:
81            return basic_message
82
83        assert self.stack[0] == ".FeatureCount"
84        offender_index, _ = self.offender
85        bad_ttf = self.merger.ttfs[offender_index]
86        good_ttf = next(
87            ttf
88            for ttf in self.merger.ttfs
89            if self.stack[-1] in ttf
90            and ttf[self.stack[-1]].table.FeatureList.FeatureCount
91            == self.cause["expected"]
92        )
93
94        good_features = [
95            x.FeatureTag
96            for x in good_ttf[self.stack[-1]].table.FeatureList.FeatureRecord
97        ]
98        bad_features = [
99            x.FeatureTag
100            for x in bad_ttf[self.stack[-1]].table.FeatureList.FeatureRecord
101        ]
102        return basic_message + (
103            "\nIncompatible features between masters.\n"
104            f"Expected: {', '.join(good_features)}.\n"
105            f"Got: {', '.join(bad_features)}.\n"
106        )
107
108
109class FoundANone(VarLibMergeError):
110    """one of the values in a list was empty when it shouldn't have been"""
111
112    @property
113    def offender(self):
114        index = [x is None for x in self.cause["got"]].index(True)
115        return index, self._master_name(index)
116
117    @property
118    def details(self):
119        cause, stack = self.cause, self.stack
120        return f"{stack[0]}=={cause['got']}\n"
121
122
123class NotANone(VarLibMergeError):
124    """one of the values in a list was not empty when it should have been"""
125
126    @property
127    def offender(self):
128        index = [x is not None for x in self.cause["got"]].index(True)
129        return index, self._master_name(index)
130
131    @property
132    def details(self):
133        cause, stack = self.cause, self.stack
134        return f"{stack[0]}=={cause['got']}\n"
135
136
137class MismatchedTypes(VarLibMergeError):
138    """data had inconsistent types"""
139
140
141class LengthsDiffer(VarLibMergeError):
142    """a list of objects had inconsistent lengths"""
143
144
145class KeysDiffer(VarLibMergeError):
146    """a list of objects had different keys"""
147
148
149class InconsistentGlyphOrder(VarLibMergeError):
150    """the glyph order was inconsistent between masters"""
151
152
153class InconsistentExtensions(VarLibMergeError):
154    """the masters use extension lookups in inconsistent ways"""
155
156
157class UnsupportedFormat(VarLibMergeError):
158    """an OpenType subtable (%s) had a format I didn't expect"""
159
160    def __init__(self, merger=None, **kwargs):
161        super().__init__(merger, **kwargs)
162        if not self.stack:
163            self.stack = [".Format"]
164
165    @property
166    def reason(self):
167        s = self.__doc__ % self.cause["subtable"]
168        if "value" in self.cause:
169            s += f" ({self.cause['value']!r})"
170        return s
171
172
173class InconsistentFormats(UnsupportedFormat):
174    """an OpenType subtable (%s) had inconsistent formats between masters"""
175
176
177class VarLibCFFMergeError(VarLibError):
178    pass
179
180
181class VarLibCFFDictMergeError(VarLibCFFMergeError):
182    """Raised when a CFF PrivateDict cannot be merged."""
183
184    def __init__(self, key, value, values):
185        error_msg = (
186            f"For the Private Dict key '{key}', the default font value list:"
187            f"\n\t{value}\nhad a different number of values than a region font:"
188        )
189        for region_value in values:
190            error_msg += f"\n\t{region_value}"
191        self.args = (error_msg,)
192
193
194class VarLibCFFPointTypeMergeError(VarLibCFFMergeError):
195    """Raised when a CFF glyph cannot be merged because of point type differences."""
196
197    def __init__(self, point_type, pt_index, m_index, default_type, glyph_name):
198        error_msg = (
199            f"Glyph '{glyph_name}': '{point_type}' at point index {pt_index} in "
200            f"master index {m_index} differs from the default font point type "
201            f"'{default_type}'"
202        )
203        self.args = (error_msg,)
204
205
206class VarLibCFFHintTypeMergeError(VarLibCFFMergeError):
207    """Raised when a CFF glyph cannot be merged because of hint type differences."""
208
209    def __init__(self, hint_type, cmd_index, m_index, default_type, glyph_name):
210        error_msg = (
211            f"Glyph '{glyph_name}': '{hint_type}' at index {cmd_index} in "
212            f"master index {m_index} differs from the default font hint type "
213            f"'{default_type}'"
214        )
215        self.args = (error_msg,)
216
217
218class VariationModelError(VarLibError):
219    """Raised when a variation model is faulty."""
220