• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from . import DefaultTable
2from fontTools.misc import sstruct
3from fontTools.misc.textTools import safeEval
4import struct
5
6VDMX_HeaderFmt = """
7	>                 # big endian
8	version:     H    # Version number (0 or 1)
9	numRecs:     H    # Number of VDMX groups present
10	numRatios:   H    # Number of aspect ratio groupings
11"""
12# the VMDX header is followed by an array of RatRange[numRatios] (i.e. aspect
13# ratio ranges);
14VDMX_RatRangeFmt = """
15	>                 # big endian
16	bCharSet:    B    # Character set
17	xRatio:      B    # Value to use for x-Ratio
18	yStartRatio: B    # Starting y-Ratio value
19	yEndRatio:   B    # Ending y-Ratio value
20"""
21# followed by an array of offset[numRatios] from start of VDMX table to the
22# VDMX Group for this ratio range (offsets will be re-calculated on compile);
23# followed by an array of Group[numRecs] records;
24VDMX_GroupFmt = """
25	>                 # big endian
26	recs:        H    # Number of height records in this group
27	startsz:     B    # Starting yPelHeight
28	endsz:       B    # Ending yPelHeight
29"""
30# followed by an array of vTable[recs] records.
31VDMX_vTableFmt = """
32	>                 # big endian
33	yPelHeight:  H    # yPelHeight to which values apply
34	yMax:        h    # Maximum value (in pels) for this yPelHeight
35	yMin:        h    # Minimum value (in pels) for this yPelHeight
36"""
37
38
39class table_V_D_M_X_(DefaultTable.DefaultTable):
40    def decompile(self, data, ttFont):
41        pos = 0  # track current position from to start of VDMX table
42        dummy, data = sstruct.unpack2(VDMX_HeaderFmt, data, self)
43        pos += sstruct.calcsize(VDMX_HeaderFmt)
44        self.ratRanges = []
45        for i in range(self.numRatios):
46            ratio, data = sstruct.unpack2(VDMX_RatRangeFmt, data)
47            pos += sstruct.calcsize(VDMX_RatRangeFmt)
48            # the mapping between a ratio and a group is defined further below
49            ratio["groupIndex"] = None
50            self.ratRanges.append(ratio)
51        lenOffset = struct.calcsize(">H")
52        _offsets = []  # temporarily store offsets to groups
53        for i in range(self.numRatios):
54            offset = struct.unpack(">H", data[0:lenOffset])[0]
55            data = data[lenOffset:]
56            pos += lenOffset
57            _offsets.append(offset)
58        self.groups = []
59        for groupIndex in range(self.numRecs):
60            # the offset to this group from beginning of the VDMX table
61            currOffset = pos
62            group, data = sstruct.unpack2(VDMX_GroupFmt, data)
63            # the group lenght and bounding sizes are re-calculated on compile
64            recs = group.pop("recs")
65            startsz = group.pop("startsz")
66            endsz = group.pop("endsz")
67            pos += sstruct.calcsize(VDMX_GroupFmt)
68            for j in range(recs):
69                vTable, data = sstruct.unpack2(VDMX_vTableFmt, data)
70                vTableLength = sstruct.calcsize(VDMX_vTableFmt)
71                pos += vTableLength
72                # group is a dict of (yMax, yMin) tuples keyed by yPelHeight
73                group[vTable["yPelHeight"]] = (vTable["yMax"], vTable["yMin"])
74            # make sure startsz and endsz match the calculated values
75            minSize = min(group.keys())
76            maxSize = max(group.keys())
77            assert (
78                startsz == minSize
79            ), "startsz (%s) must equal min yPelHeight (%s): group %d" % (
80                group.startsz,
81                minSize,
82                groupIndex,
83            )
84            assert (
85                endsz == maxSize
86            ), "endsz (%s) must equal max yPelHeight (%s): group %d" % (
87                group.endsz,
88                maxSize,
89                groupIndex,
90            )
91            self.groups.append(group)
92            # match the defined offsets with the current group's offset
93            for offsetIndex, offsetValue in enumerate(_offsets):
94                # when numRecs < numRatios there can more than one ratio range
95                # sharing the same VDMX group
96                if currOffset == offsetValue:
97                    # map the group with the ratio range thas has the same
98                    # index as the offset to that group (it took me a while..)
99                    self.ratRanges[offsetIndex]["groupIndex"] = groupIndex
100        # check that all ratio ranges have a group
101        for i in range(self.numRatios):
102            ratio = self.ratRanges[i]
103            if ratio["groupIndex"] is None:
104                from fontTools import ttLib
105
106                raise ttLib.TTLibError("no group defined for ratRange %d" % i)
107
108    def _getOffsets(self):
109        """
110        Calculate offsets to VDMX_Group records.
111        For each ratRange return a list of offset values from the beginning of
112        the VDMX table to a VDMX_Group.
113        """
114        lenHeader = sstruct.calcsize(VDMX_HeaderFmt)
115        lenRatRange = sstruct.calcsize(VDMX_RatRangeFmt)
116        lenOffset = struct.calcsize(">H")
117        lenGroupHeader = sstruct.calcsize(VDMX_GroupFmt)
118        lenVTable = sstruct.calcsize(VDMX_vTableFmt)
119        # offset to the first group
120        pos = lenHeader + self.numRatios * lenRatRange + self.numRatios * lenOffset
121        groupOffsets = []
122        for group in self.groups:
123            groupOffsets.append(pos)
124            lenGroup = lenGroupHeader + len(group) * lenVTable
125            pos += lenGroup  # offset to next group
126        offsets = []
127        for ratio in self.ratRanges:
128            groupIndex = ratio["groupIndex"]
129            offsets.append(groupOffsets[groupIndex])
130        return offsets
131
132    def compile(self, ttFont):
133        if not (self.version == 0 or self.version == 1):
134            from fontTools import ttLib
135
136            raise ttLib.TTLibError(
137                "unknown format for VDMX table: version %s" % self.version
138            )
139        data = sstruct.pack(VDMX_HeaderFmt, self)
140        for ratio in self.ratRanges:
141            data += sstruct.pack(VDMX_RatRangeFmt, ratio)
142        # recalculate offsets to VDMX groups
143        for offset in self._getOffsets():
144            data += struct.pack(">H", offset)
145        for group in self.groups:
146            recs = len(group)
147            startsz = min(group.keys())
148            endsz = max(group.keys())
149            gHeader = {"recs": recs, "startsz": startsz, "endsz": endsz}
150            data += sstruct.pack(VDMX_GroupFmt, gHeader)
151            for yPelHeight, (yMax, yMin) in sorted(group.items()):
152                vTable = {"yPelHeight": yPelHeight, "yMax": yMax, "yMin": yMin}
153                data += sstruct.pack(VDMX_vTableFmt, vTable)
154        return data
155
156    def toXML(self, writer, ttFont):
157        writer.simpletag("version", value=self.version)
158        writer.newline()
159        writer.begintag("ratRanges")
160        writer.newline()
161        for ratio in self.ratRanges:
162            groupIndex = ratio["groupIndex"]
163            writer.simpletag(
164                "ratRange",
165                bCharSet=ratio["bCharSet"],
166                xRatio=ratio["xRatio"],
167                yStartRatio=ratio["yStartRatio"],
168                yEndRatio=ratio["yEndRatio"],
169                groupIndex=groupIndex,
170            )
171            writer.newline()
172        writer.endtag("ratRanges")
173        writer.newline()
174        writer.begintag("groups")
175        writer.newline()
176        for groupIndex in range(self.numRecs):
177            group = self.groups[groupIndex]
178            recs = len(group)
179            startsz = min(group.keys())
180            endsz = max(group.keys())
181            writer.begintag("group", index=groupIndex)
182            writer.newline()
183            writer.comment("recs=%d, startsz=%d, endsz=%d" % (recs, startsz, endsz))
184            writer.newline()
185            for yPelHeight, (yMax, yMin) in sorted(group.items()):
186                writer.simpletag(
187                    "record",
188                    [("yPelHeight", yPelHeight), ("yMax", yMax), ("yMin", yMin)],
189                )
190                writer.newline()
191            writer.endtag("group")
192            writer.newline()
193        writer.endtag("groups")
194        writer.newline()
195
196    def fromXML(self, name, attrs, content, ttFont):
197        if name == "version":
198            self.version = safeEval(attrs["value"])
199        elif name == "ratRanges":
200            if not hasattr(self, "ratRanges"):
201                self.ratRanges = []
202            for element in content:
203                if not isinstance(element, tuple):
204                    continue
205                name, attrs, content = element
206                if name == "ratRange":
207                    if not hasattr(self, "numRatios"):
208                        self.numRatios = 1
209                    else:
210                        self.numRatios += 1
211                    ratio = {
212                        "bCharSet": safeEval(attrs["bCharSet"]),
213                        "xRatio": safeEval(attrs["xRatio"]),
214                        "yStartRatio": safeEval(attrs["yStartRatio"]),
215                        "yEndRatio": safeEval(attrs["yEndRatio"]),
216                        "groupIndex": safeEval(attrs["groupIndex"]),
217                    }
218                    self.ratRanges.append(ratio)
219        elif name == "groups":
220            if not hasattr(self, "groups"):
221                self.groups = []
222            for element in content:
223                if not isinstance(element, tuple):
224                    continue
225                name, attrs, content = element
226                if name == "group":
227                    if not hasattr(self, "numRecs"):
228                        self.numRecs = 1
229                    else:
230                        self.numRecs += 1
231                    group = {}
232                    for element in content:
233                        if not isinstance(element, tuple):
234                            continue
235                        name, attrs, content = element
236                        if name == "record":
237                            yPelHeight = safeEval(attrs["yPelHeight"])
238                            yMax = safeEval(attrs["yMax"])
239                            yMin = safeEval(attrs["yMin"])
240                            group[yPelHeight] = (yMax, yMin)
241                    self.groups.append(group)
242