1# Copyright 2014 Adobe. All rights reserved.
2
3"""
4Utilities for converting between T2 charstrings and the bez data format.
5"""
6
7import copy
8import logging
9import os
10import re
11import subprocess
12import tempfile
13import itertools
14
15from fontTools.misc.psCharStrings import (T2OutlineExtractor,
16                                          SimpleT2Decompiler)
17from fontTools.ttLib import TTFont, newTable
18from fontTools.misc.fixedTools import otRound
19from fontTools.varLib.varStore import VarStoreInstancer
20from fontTools.varLib.cff import CFF2CharStringMergePen, MergeOutlineExtractor
21# import subset.cff is needed to load the implementation for
22# CFF.desubroutinize: the module adds this class method to the CFF and CFF2
23# classes.
24import fontTools.subset.cff
25
26from . import fdTools, FontParseError
27
28# keep linting tools quiet about unused import
29assert fontTools.subset.cff is not None
30
31log = logging.getLogger(__name__)
32
33kStackLimit = 46
34kStemLimit = 96
35
36
37class SEACError(Exception):
38    pass
39
40
41def _add_method(*clazzes):
42    """Returns a decorator function that adds a new method to one or
43    more classes."""
44    def wrapper(method):
45        done = []
46        for clazz in clazzes:
47            if clazz in done:
48                continue  # Support multiple names of a clazz
49            done.append(clazz)
50            assert clazz.__name__ != 'DefaultTable', \
51                'Oops, table class not found.'
52            assert not hasattr(clazz, method.__name__), \
53                "Oops, class '%s' has method '%s'." % (clazz.__name__,
54                                                       method.__name__)
55            setattr(clazz, method.__name__, method)
56        return None
57    return wrapper
58
59
60def hintOn(i, hintMaskBytes):
61    # used to add the active hints to the bez string,
62    # when a T2 hintmask operator is encountered.
63    byteIndex = int(i / 8)
64    byteValue = hintMaskBytes[byteIndex]
65    offset = 7 - (i % 8)
66    return ((2**offset) & byteValue) > 0
67
68
69class T2ToBezExtractor(T2OutlineExtractor):
70    # The T2OutlineExtractor class calls a class method as the handler for each
71    # T2 operator.
72    # I use this to convert the T2 operands and arguments to bez operators.
73    # Note: flex is converted to regular rrcurveto's.
74    # cntrmasks just map to hint replacement blocks with the specified stems.
75    def __init__(self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX,
76                 read_hints=True, round_coords=True):
77        T2OutlineExtractor.__init__(self, None, localSubrs, globalSubrs,
78                                    nominalWidthX, defaultWidthX)
79        self.vhints = []
80        self.hhints = []
81        self.bezProgram = []
82        self.read_hints = read_hints
83        self.firstMarkingOpSeen = False
84        self.closePathSeen = False
85        self.subrLevel = 0
86        self.round_coords = round_coords
87        self.hintMaskBytes = None
88
89    def execute(self, charString):
90        self.subrLevel += 1
91        SimpleT2Decompiler.execute(self, charString)
92        self.subrLevel -= 1
93        if (not self.closePathSeen) and (self.subrLevel == 0):
94            self.closePath()
95
96    def _point(self, point):
97        if self.round_coords:
98            return " ".join("%d" % round(pt) for pt in point)
99        return " ".join("%3f" % pt for pt in point)
100
101    def rMoveTo(self, point):
102        point = self._nextPoint(point)
103        if not self.firstMarkingOpSeen:
104            self.firstMarkingOpSeen = True
105            self.bezProgram.append("sc\n")
106        log.debug("moveto %s, curpos %s", point, self.currentPoint)
107        self.bezProgram.append("%s mt\n" % self._point(point))
108        self.sawMoveTo = True
109
110    def rLineTo(self, point):
111        if not self.sawMoveTo:
112            self.rMoveTo((0, 0))
113        point = self._nextPoint(point)
114        log.debug("lineto %s, curpos %s", point, self.currentPoint)
115        self.bezProgram.append("%s dt\n" % self._point(point))
116
117    def rCurveTo(self, pt1, pt2, pt3):
118        if not self.sawMoveTo:
119            self.rMoveTo((0, 0))
120        pt1 = list(self._nextPoint(pt1))
121        pt2 = list(self._nextPoint(pt2))
122        pt3 = list(self._nextPoint(pt3))
123        log.debug("curveto %s %s %s, curpos %s", pt1, pt2, pt3,
124                  self.currentPoint)
125        self.bezProgram.append("%s ct\n" % self._point(pt1 + pt2 + pt3))
126
127    def op_endchar(self, index):
128        self.endPath()
129        args = self.popallWidth()
130        if args:  # It is a 'seac' composite character. Don't process
131            raise SEACError
132
133    def endPath(self):
134        # In T2 there are no open paths, so always do a closePath when
135        # finishing a sub path.
136        if self.sawMoveTo:
137            log.debug("endPath")
138            self.bezProgram.append("cp\n")
139        self.sawMoveTo = False
140
141    def closePath(self):
142        self.closePathSeen = True
143        log.debug("closePath")
144        if self.bezProgram and self.bezProgram[-1] != "cp\n":
145            self.bezProgram.append("cp\n")
146        self.bezProgram.append("ed\n")
147
148    def updateHints(self, args, hint_list, bezCommand):
149        self.countHints(args)
150
151        # first hint value is absolute hint coordinate, second is hint width
152        if not self.read_hints:
153            return
154
155        lastval = args[0]
156        arg = str(lastval)
157        hint_list.append(arg)
158        self.bezProgram.append(arg + " ")
159
160        for i in range(len(args))[1:]:
161            val = args[i]
162            lastval += val
163
164            if i % 2:
165                arg = str(val)
166                hint_list.append(arg)
167                self.bezProgram.append("%s %s\n" % (arg, bezCommand))
168            else:
169                arg = str(lastval)
170                hint_list.append(arg)
171                self.bezProgram.append(arg + " ")
172
173    def op_hstem(self, index):
174        args = self.popallWidth()
175        self.hhints = []
176        self.updateHints(args, self.hhints, "rb")
177        log.debug("hstem %s", self.hhints)
178
179    def op_vstem(self, index):
180        args = self.popallWidth()
181        self.vhints = []
182        self.updateHints(args, self.vhints, "ry")
183        log.debug("vstem %s", self.vhints)
184
185    def op_hstemhm(self, index):
186        args = self.popallWidth()
187        self.hhints = []
188        self.updateHints(args, self.hhints, "rb")
189        log.debug("stemhm %s %s", self.hhints, args)
190
191    def op_vstemhm(self, index):
192        args = self.popallWidth()
193        self.vhints = []
194        self.updateHints(args, self.vhints, "ry")
195        log.debug("vstemhm %s %s", self.vhints, args)
196
197    def getCurHints(self, hintMaskBytes):
198        curhhints = []
199        curvhints = []
200        numhhints = len(self.hhints)
201
202        for i in range(int(numhhints / 2)):
203            if hintOn(i, hintMaskBytes):
204                curhhints.extend(self.hhints[2 * i:2 * i + 2])
205        numvhints = len(self.vhints)
206        for i in range(int(numvhints / 2)):
207            if hintOn(i + int(numhhints / 2), hintMaskBytes):
208                curvhints.extend(self.vhints[2 * i:2 * i + 2])
209        return curhhints, curvhints
210
211    def doMask(self, index, bezCommand):
212        args = []
213        if not self.hintMaskBytes:
214            args = self.popallWidth()
215            if args:
216                self.vhints = []
217                self.updateHints(args, self.vhints, "ry")
218            self.hintMaskBytes = int((self.hintCount + 7) / 8)
219
220        self.hintMaskString, index = self.callingStack[-1].getBytes(
221            index, self.hintMaskBytes)
222
223        if self.read_hints:
224            curhhints, curvhints = self.getCurHints(self.hintMaskString)
225            strout = ""
226            mask = [strout + hex(ch) for ch in self.hintMaskString]
227            log.debug("%s %s %s %s %s", bezCommand, mask, curhhints, curvhints,
228                      args)
229
230            self.bezProgram.append("beginsubr snc\n")
231            for i, hint in enumerate(curhhints):
232                self.bezProgram.append("%s " % hint)
233                if i % 2:
234                    self.bezProgram.append("rb\n")
235            for i, hint in enumerate(curvhints):
236                self.bezProgram.append("%s " % hint)
237                if i % 2:
238                    self.bezProgram.append("ry\n")
239            self.bezProgram.extend(["endsubr enc\n", "newcolors\n"])
240        return self.hintMaskString, index
241
242    def op_hintmask(self, index):
243        hintMaskString, index = self.doMask(index, "hintmask")
244        return hintMaskString, index
245
246    def op_cntrmask(self, index):
247        hintMaskString, index = self.doMask(index, "cntrmask")
248        return hintMaskString, index
249
250    def countHints(self, args):
251        self.hintCount = self.hintCount + int(len(args) / 2)
252
253
254def convertT2GlyphToBez(t2CharString, read_hints=True, round_coords=True):
255    # wrapper for T2ToBezExtractor which
256    # applies it to the supplied T2 charstring
257    subrs = getattr(t2CharString.private, "Subrs", [])
258    extractor = T2ToBezExtractor(subrs,
259                                 t2CharString.globalSubrs,
260                                 t2CharString.private.nominalWidthX,
261                                 t2CharString.private.defaultWidthX,
262                                 read_hints,
263                                 round_coords)
264    extractor.execute(t2CharString)
265    t2_width_arg = None
266    if extractor.gotWidth and (extractor.width is not None):
267        t2_width_arg = extractor.width - t2CharString.private.nominalWidthX
268    return "".join(extractor.bezProgram), t2_width_arg
269
270
271class HintMask:
272    # class used to collect hints for the current
273    # hint mask when converting bez to T2.
274    def __init__(self, listPos):
275        # The index into the t2list is kept so we can quickly find them later.
276        # Note that t2list has one item per operator, and does not include the
277        # initial hint operators - first op is always [rhv]moveto or endchar.
278        self.listPos = listPos
279        # These contain the actual hint values.
280        self.h_list = []
281        self.v_list = []
282        self.mask = None
283
284    def maskByte(self, hHints, vHints):
285        # return hintmask bytes for known hints.
286        num_hhints = len(hHints)
287        num_vhints = len(vHints)
288        self.byteLength = byteLength = int((7 + num_hhints + num_vhints) / 8)
289        maskVal = 0
290        byteIndex = 0
291        mask = b""
292        if self.h_list:
293            mask, maskVal, byteIndex = self.addMaskBits(
294                hHints, self.h_list, 0, mask, maskVal, byteIndex)
295        if self.v_list:
296            mask, maskVal, byteIndex = self.addMaskBits(
297                vHints, self.v_list, num_hhints, mask, maskVal, byteIndex)
298
299        if maskVal:
300            mask += bytes([maskVal])
301
302        if len(mask) < byteLength:
303            mask += b"\0" * (byteLength - len(mask))
304        self.mask = mask
305        return mask
306
307    @staticmethod
308    def addMaskBits(allHints, maskHints, numPriorHints, mask, maskVal,
309                    byteIndex):
310        # sort in allhints order.
311        sort_list = [[allHints.index(hint) + numPriorHints, hint] for hint in
312                     maskHints if hint in allHints]
313        if not sort_list:
314            # we get here if some hints have been dropped # because of
315            # the stack limit, so that none of the items in maskHints are
316            # not in allHints
317            return mask, maskVal, byteIndex
318
319        sort_list.sort()
320        (idx_list, maskHints) = zip(*sort_list)
321        for i in idx_list:
322            newbyteIndex = int(i / 8)
323            if newbyteIndex != byteIndex:
324                mask += bytes([maskVal])
325                byteIndex += 1
326                while byteIndex < newbyteIndex:
327                    mask += b"\0"
328                    byteIndex += 1
329                maskVal = 0
330            maskVal += 2**(7 - (i % 8))
331        return mask, maskVal, byteIndex
332
333    @property
334    def num_bits(self):
335        count = sum(
336            [bin(mask_byte).count('1') for mask_byte in bytearray(self.mask)])
337        return count
338
339
340def make_hint_list(hints, need_hint_masks, is_h):
341    # Add the list of T2 tokens that make up the initial hint operators
342    hint_list = []
343    lastPos = 0
344    # In bez terms, the first coordinate in each pair is absolute,
345    # second is relative.
346    # In T2, each term is relative to the previous one.
347    for hint in hints:
348        if not hint:
349            continue
350        pos1 = hint[0]
351        pos = pos1 - lastPos
352        if pos % 1 == 0:
353            pos = int(pos)
354        hint_list.append(pos)
355        pos2 = hint[1]
356        if pos2 % 1 == 0:
357            pos2 = int(pos2)
358        lastPos = pos1 + pos2
359        hint_list.append(pos2)
360
361    if need_hint_masks:
362        if is_h:
363            op = "hstemhm"
364            hint_list.append(op)
365        # never need to append vstemhm: if we are using it, it is followed
366        # by a mask command and vstemhm is inferred.
367    else:
368        if is_h:
369            op = "hstem"
370        else:
371            op = "vstem"
372        hint_list.append(op)
373    return hint_list
374
375
376bezToT2 = {
377    "mt": 'rmoveto',
378    "rmt": 'rmoveto',
379    "dt": 'rlineto',
380    "ct": 'rrcurveto',
381    "cp": '',
382    "ed": 'endchar'
383}
384
385
386kHintArgsNoOverlap = 0
387kHintArgsOverLap = 1
388kHintArgsMatch = 2
389
390
391def checkStem3ArgsOverlap(arg_list, hint_list):
392    status = kHintArgsNoOverlap
393    for x0, x1 in arg_list:
394        x1 = x0 + x1
395        for y0, y1 in hint_list:
396            y1 = y0 + y1
397            if x0 == y0:
398                if x1 == y1:
399                    status = kHintArgsMatch
400                else:
401                    return kHintArgsOverLap
402            elif x1 == y1:
403                return kHintArgsOverLap
404            else:
405                if (x0 > y0) and (x0 < y1):
406                    return kHintArgsOverLap
407                if (x1 > y0) and (x1 < y1):
408                    return kHintArgsOverLap
409    return status
410
411
412def _add_cntr_maskHints(counter_mask_list, src_hints, is_h):
413    for arg_list in src_hints:
414        for mask in counter_mask_list:
415            dst_hints = mask.h_list if is_h else mask.v_list
416            if not dst_hints:
417                dst_hints.extend(arg_list)
418                overlap_status = kHintArgsMatch
419                break
420            overlap_status = checkStem3ArgsOverlap(arg_list, dst_hints)
421            # The args match args in this control mask.
422            if overlap_status == kHintArgsMatch:
423                break
424        if overlap_status != kHintArgsMatch:
425            mask = HintMask(0)
426            counter_mask_list.append(mask)
427            dst_hints.extend(arg_list)
428
429
430def build_counter_mask_list(h_stem3_list, v_stem3_list):
431
432    v_counter_mask = HintMask(0)
433    h_counter_mask = v_counter_mask
434    counter_mask_list = [h_counter_mask]
435    _add_cntr_maskHints(counter_mask_list, h_stem3_list, is_h=True)
436    _add_cntr_maskHints(counter_mask_list, v_stem3_list, is_h=False)
437
438    return counter_mask_list
439
440
441def makeRelativeCTArgs(arg_list, curX, curY):
442    newCurX = arg_list[4]
443    newCurY = arg_list[5]
444    arg_list[5] -= arg_list[3]
445    arg_list[4] -= arg_list[2]
446
447    arg_list[3] -= arg_list[1]
448    arg_list[2] -= arg_list[0]
449
450    arg_list[0] -= curX
451    arg_list[1] -= curY
452    return arg_list, newCurX, newCurY
453
454
455def build_hint_order(hints):
456    # MM hints have duplicate hints. We want to return a list of indices into
457    # the original unsorted and unfiltered list. The list should be sorted, and
458    # should filter out duplicates
459
460    num_hints = len(hints)
461    index_list = list(range(num_hints))
462    hint_list = list(zip(hints, index_list))
463    hint_list.sort()
464    new_hints = [hint_list[i] for i in range(1, num_hints)
465                 if hint_list[i][0] != hint_list[i - 1][0]]
466    new_hints = [hint_list[0]] + new_hints
467    hints, hint_order = list(zip(*new_hints))
468    # hints is now a list of hint pairs, sorted by increasing bottom edge.
469    # hint_order is now a list of the hint indices from the bez file, but
470    # sorted in the order of the hint pairs.
471    return hints, hint_order
472
473
474def make_abs(hint_pair):
475    bottom_edge, delta = hint_pair
476    new_hint_pair = [bottom_edge, delta]
477    if delta in [-20, -21]:  # It is a ghost hint!
478        # We use this only in comparing overlap and order:
479        # pretend the delta is 0, as it isn't a real value.
480        new_hint_pair[1] = bottom_edge
481    else:
482        new_hint_pair[1] = bottom_edge + delta
483    return new_hint_pair
484
485
486def check_hint_overlap(hint_list, last_idx, bad_hint_idxs):
487    # return True if there is an overlap.
488    prev = hint_list[0]
489    for i, hint_pair in enumerate(hint_list[1:], 1):
490        if prev[1] >= hint_pair[0]:
491            bad_hint_idxs.add(i + last_idx - 1)
492        prev = hint_pair
493
494
495def check_hint_pairs(hint_pairs, mm_hint_info, last_idx=0):
496    # pairs must be in ascending order by bottom (or left) edge,
497    # and pairs in a hint group must not overlap.
498
499    # check order first
500    bad_hint_idxs = set()
501    prev = hint_pairs[0]
502    for i, hint_pair in enumerate(hint_pairs[1:], 1):
503        if prev[0] > hint_pair[0]:
504            # If there is a conflict, we drop the previous hint
505            bad_hint_idxs.add(i + last_idx - 1)
506        prev = hint_pair
507
508    # check for overlap in hint groups.
509    if mm_hint_info.hint_masks:
510        for hint_mask in mm_hint_info.hint_masks:
511            if last_idx == 0:
512                hint_list = hint_mask.h_list
513            else:
514                hint_list = hint_mask.v_list
515            hint_list = [make_abs(hint_pair) for hint_pair in hint_list]
516            check_hint_overlap(hint_list, last_idx, bad_hint_idxs)
517    else:
518        hint_list = [make_abs(hint_pair) for hint_pair in hint_pairs]
519        check_hint_overlap(hint_list, last_idx, bad_hint_idxs)
520
521    if bad_hint_idxs:
522        mm_hint_info.bad_hint_idxs |= bad_hint_idxs
523
524
525def update_hints(in_mm_hints, arg_list, hints, hint_mask, is_v=False):
526    if in_mm_hints:
527        hints.append(arg_list)
528        i = len(hints) - 1
529    else:
530        try:
531            i = hints.index(arg_list)
532        except ValueError:
533            i = len(hints)
534            hints.append(arg_list)
535    if hint_mask:
536        hint_list = hint_mask.v_list if is_v else hint_mask.h_list
537        if hints[i] not in hint_list:
538            hint_list.append(hints[i])
539    return i
540
541
542def convertBezToT2(bezString, mm_hint_info=None):
543    # convert bez data to a T2 outline program, a list of operator tokens.
544    #
545    # Convert all bez ops to simplest T2 equivalent.
546    # Add all hints to vertical and horizontal hint lists as encountered.
547    # Insert a HintMask class whenever a new set of hints is encountered.
548    # Add all hints as prefix to t2Program
549    # After all operators have been processed, convert HintMask items into
550    # hintmask ops and hintmask bytes.
551    # Review operator list to optimize T2 operators.
552    #
553    # If doing MM-hinting, extra work is needed to maintain merge
554    # compatibility between the reference font and the region fonts.
555    # Although hints are generated for exactly the same outline features
556    # in all fonts, they will have different values. Consequently, the
557    # hints in a region font may not sort to the same order as in the
558    # reference font. In addition, they may be filtered differently. Only
559    # unique hints are added from the bez file to the hint list. Two hint
560    # pairs may differ in one font, but not in another.
561    # We work around these problems by first not filtering the hint
562    # pairs for uniqueness when accumulating the hint lists. For the
563    # reference font, once we have collected all the hints, we remove any
564    # duplicate pairs, but keep a list of the retained hint pair indices
565    # into the unfiltered hint pair list. For the region fonts, we
566    # select hints from the unfiltered hint pair lists by using the selected
567    # index list from the reference font.
568    # Note that this breaks the CFF spec for snapshotted instances of the
569    # CFF2 VF variable font, as hints may not be in ascending order, and the
570    # hint list may contain duplicate hints.
571
572    in_mm_hints = mm_hint_info is not None
573    bezString = re.sub(r"%.+?\n", "", bezString)  # suppress comments
574    bezList = re.findall(r"(\S+)", bezString)
575    if not bezList:
576        return ""
577    hhints = []
578    vhints = []
579    # Always assume a hint mask exists until proven
580    # otherwise - make an initial HintMask.
581    hint_mask = HintMask(0)
582    hintMaskList = [hint_mask]
583    vStem3Args = []
584    hStem3Args = []
585    v_stem3_list = []
586    h_stem3_list = []
587    arg_list = []
588    t2List = []
589
590    lastPathOp = None
591    curX = 0
592    curY = 0
593    for token in bezList:
594        try:
595            val1 = round(float(token), 2)
596            try:
597                val2 = int(token)
598                if int(val1) == val2:
599                    arg_list.append(val2)
600                else:
601                    arg_list.append("%s 100 div" % int(val1 * 100))
602            except ValueError:
603                arg_list.append(val1)
604            continue
605        except ValueError:
606            pass
607
608        if token == "newcolors":
609            lastPathOp = token
610        elif token in ["beginsubr", "endsubr"]:
611            lastPathOp = token
612        elif token == "snc":
613            lastPathOp = token
614            # The index into the t2list is kept
615            # so we can quickly find them later.
616            hint_mask = HintMask(len(t2List))
617            t2List.append([hint_mask])
618            hintMaskList.append(hint_mask)
619        elif token == "enc":
620            lastPathOp = token
621        elif token == "rb":
622            update_hints(in_mm_hints, arg_list, hhints, hint_mask, False)
623            arg_list = []
624            lastPathOp = token
625        elif token == "ry":
626            update_hints(in_mm_hints, arg_list, vhints, hint_mask, True)
627            arg_list = []
628            lastPathOp = token
629        elif token == "rm":  # vstem3 hints are vhints
630            update_hints(in_mm_hints, arg_list, vhints, hint_mask, True)
631            if (lastPathOp != token) and vStem3Args:
632                # first rm, must be start of a new vstem3
633                # if we already have a set of vstems in vStem3Args, save them,
634                # and then clear the vStem3Args so we can add the new set.
635                v_stem3_list.append(vStem3Args)
636                vStem3Args = []
637
638            vStem3Args.append(arg_list)
639            arg_list = []
640            lastPathOp = token
641        elif token == "rv":  # hstem3 are hhints
642            update_hints(in_mm_hints, arg_list, hhints, hint_mask, False)
643
644            if (lastPathOp != token) and hStem3Args:
645                # first rv, must be start of a new h countermask
646                h_stem3_list.append(hStem3Args)
647                hStem3Args = []
648
649            hStem3Args.append(arg_list)
650            arg_list = []
651            lastPathOp = token
652        elif token == "preflx1":
653            # The preflx1/preflx2a sequence provides the same 'i' as the flex
654            # sequence. The difference is that the preflx1/preflx2a sequence
655            # provides the argument values needed for building a Type1 string
656            # while the flex sequence is simply the 6 rrcurveto points.
657            # Both sequences are always provided.
658            lastPathOp = token
659            arg_list = []
660        elif token == "preflx2a":
661            lastPathOp = token
662            del t2List[-1]
663            arg_list = []
664        elif token == "flxa":
665            lastPathOp = token
666            argList1, curX, curY = makeRelativeCTArgs(arg_list[:6], curX, curY)
667            argList2, curX, curY = makeRelativeCTArgs(arg_list[6:], curX, curY)
668            arg_list = argList1 + argList2
669            t2List.append([arg_list[:12] + [50], "flex"])
670            arg_list = []
671        elif token == "sc":
672            lastPathOp = token
673        else:
674            if token in ["rmt", "mt", "dt", "ct"]:
675                lastPathOp = token
676            t2Op = bezToT2.get(token, None)
677            if token in ["mt", "dt"]:
678                newList = [arg_list[0] - curX, arg_list[1] - curY]
679                curX = arg_list[0]
680                curY = arg_list[1]
681                arg_list = newList
682            elif token == "ct":
683                arg_list, curX, curY = makeRelativeCTArgs(arg_list, curX, curY)
684            if t2Op:
685                t2List.append([arg_list, t2Op])
686            elif t2Op is None:
687                raise KeyError("Unhandled operation %s %s" % (arg_list, token))
688            arg_list = []
689
690    # Add hints, if any. Must be done at the end of op processing to make sure
691    # we have seen all the hints in the bez string. Note that the hintmask are
692    # identified in the t2List by an index into the list; be careful NOT to
693    # change the t2List length until the hintmasks have been converted.
694    need_hint_masks = len(hintMaskList) > 1
695    if vStem3Args:
696        v_stem3_list.append(vStem3Args)
697    if hStem3Args:
698        h_stem3_list.append(hStem3Args)
699
700    t2Program = []
701
702    if hhints or vhints:
703        if mm_hint_info is None:
704            hhints.sort()
705            vhints.sort()
706        elif mm_hint_info.defined:
707            # Apply hint order from reference font in MM hinting
708            hhints = [hhints[j] for j in mm_hint_info.h_order]
709            vhints = [vhints[j] for j in mm_hint_info.v_order]
710        else:
711            # Define hint order from reference font in MM hinting
712            hhints, mm_hint_info.h_order = build_hint_order(hhints)
713            vhints, mm_hint_info.v_order = build_hint_order(vhints)
714
715        num_hhints = len(hhints)
716        num_vhints = len(vhints)
717        hint_limit = int((kStackLimit - 2) / 2)
718        if num_hhints >= hint_limit:
719            hhints = hhints[:hint_limit]
720        if num_vhints >= hint_limit:
721            vhints = vhints[:hint_limit]
722
723        if mm_hint_info and mm_hint_info.defined:
724            check_hint_pairs(hhints, mm_hint_info)
725            last_idx = len(hhints)
726            check_hint_pairs(vhints, mm_hint_info, last_idx)
727
728        if hhints:
729            t2Program = make_hint_list(hhints, need_hint_masks, is_h=True)
730        if vhints:
731            t2Program += make_hint_list(vhints, need_hint_masks, is_h=False)
732
733        cntrmask_progam = None
734        if mm_hint_info is None:
735            if v_stem3_list or h_stem3_list:
736                counter_mask_list = build_counter_mask_list(h_stem3_list,
737                                                            v_stem3_list)
738                cntrmask_progam = [['cntrmask', cMask.maskByte(hhints,
739                                                               vhints)] for
740                                   cMask in counter_mask_list]
741        elif (not mm_hint_info.defined):
742            if v_stem3_list or h_stem3_list:
743                # this is the reference font - we need to build the list.
744                counter_mask_list = build_counter_mask_list(h_stem3_list,
745                                                            v_stem3_list)
746                cntrmask_progam = [['cntrmask', cMask.maskByte(hhints,
747                                                               vhints)] for
748                                   cMask in counter_mask_list]
749                mm_hint_info.cntr_masks = counter_mask_list
750        else:
751            # This is a region font - we need to used the reference font list.
752            counter_mask_list = mm_hint_info.cntr_masks
753            cntrmask_progam = [['cntrmask', cMask.mask] for
754                               cMask in counter_mask_list]
755
756        if cntrmask_progam:
757            cntrmask_progam = itertools.chain(*cntrmask_progam)
758            t2Program.extend(cntrmask_progam)
759
760        if need_hint_masks:
761            # If there is not a hintsub before any drawing operators, then
762            # add an initial first hint mask to the t2Program.
763            if (mm_hint_info is None) or (not mm_hint_info.defined):
764                # a single font and a reference font for mm hinting are
765                # processed the same way
766                if hintMaskList[1].listPos != 0:
767                    hBytes = hintMaskList[0].maskByte(hhints, vhints)
768                    t2Program.extend(["hintmask", hBytes])
769                    if in_mm_hints:
770                        mm_hint_info.hint_masks.append(hintMaskList[0])
771
772                # Convert the rest of the hint masks
773                # to a hintmask op and hintmask bytes.
774                for hint_mask in hintMaskList[1:]:
775                    pos = hint_mask.listPos
776                    hBytes = hint_mask.maskByte(hhints, vhints)
777                    t2List[pos] = [["hintmask"], hBytes]
778                    if in_mm_hints:
779                        mm_hint_info.hint_masks.append(hint_mask)
780            elif (mm_hint_info is not None):
781                # This is a MM region font:
782                # apply hint masks from reference font.
783                try:
784                    hm0_mask = mm_hint_info.hint_masks[0].mask
785                except IndexError:
786                    import pdb
787                    pdb.set_trace()
788                if isinstance(t2List[0][0], HintMask):
789                    t2List[0] = [["hintmask"], hm0_mask]
790                else:
791                    t2Program.extend(["hintmask", hm0_mask])
792
793                for hm in mm_hint_info.hint_masks[1:]:
794                    t2List[hm.listPos] = [["hintmask"], hm.mask]
795
796    for entry in t2List:
797        try:
798            t2Program.extend(entry[0])
799            t2Program.append(entry[1])
800        except Exception:
801            raise KeyError("Failed to extend t2Program with entry %s" % entry)
802
803    if in_mm_hints:
804        mm_hint_info.defined = True
805    return t2Program
806
807
808def _run_tx(args):
809    try:
810        subprocess.check_call(["tx"] + args)
811    except (subprocess.CalledProcessError, OSError) as e:
812        raise FontParseError(e)
813
814
815class FixHintWidthDecompiler(SimpleT2Decompiler):
816    # If we are using this class, we know the charstring has hints.
817    def __init__(self, localSubrs, globalSubrs, private=None):
818        self.hintMaskBytes = 0  # to silence false Codacy error.
819        SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)
820        self.has_explicit_width = None
821        self.h_hint_args = self.v_hint_args = None
822        self.last_stem_index = None
823
824    def op_hstem(self, index):
825        self.countHints(is_vert=False)
826        self.last_stem_index = index
827    op_hstemhm = op_hstem
828
829    def op_vstem(self, index):
830        self.countHints(is_vert=True)
831        self.last_stem_index = index
832    op_vstemhm = op_vstem
833
834    def op_hintmask(self, index):
835        if not self.hintMaskBytes:
836            # Note that I am assuming that there is never an op_vstemhm
837            # followed by an op_hintmask. Since this is applied after saving
838            # the font with fontTools, this is safe.
839            self.countHints(is_vert=True)
840            self.hintMaskBytes = (self.hintCount + 7) // 8
841        cs = self.callingStack[-1]
842        hintMaskBytes, index = cs.getBytes(index, self.hintMaskBytes)
843        return hintMaskBytes, index
844    op_cntrmask = op_hintmask
845
846    def countHints(self, is_vert):
847        args = self.popall()
848        if self.has_explicit_width is None:
849            if (len(args) % 2) == 0:
850                self.has_explicit_width = False
851            else:
852                self.has_explicit_width = True
853                self.width_arg = args[0]
854                args = args[1:]
855        self.hintCount = self.hintCount + len(args) // 2
856        if is_vert:
857            self.v_hint_args = args
858        else:
859            self.h_hint_args = args
860
861
862class CFFFontData:
863    def __init__(self, path, font_format):
864        self.inputPath = path
865        self.font_format = font_format
866        self.mm_hint_info_dict = {}
867        self.t2_widths = {}
868        self.is_cff2 = False
869        self.is_vf = False
870        self.vs_data_models = None
871        if font_format == "OTF":
872            # It is an OTF font, we can process it directly.
873            font = TTFont(path)
874            if "CFF "in font:
875                cff_format = "CFF "
876            elif "CFF2" in font:
877                cff_format = "CFF2"
878                self.is_cff2 = True
879            else:
880                raise FontParseError("OTF font has no CFF table <%s>." % path)
881        else:
882            # Else, package it in an OTF font.
883            cff_format = "CFF "
884            if font_format == "CFF":
885                with open(path, "rb") as fp:
886                    data = fp.read()
887            else:
888                fd, temp_path = tempfile.mkstemp()
889                os.close(fd)
890                try:
891                    _run_tx(["-cff", "+b", "-std", path, temp_path])
892                    with open(temp_path, "rb") as fp:
893                        data = fp.read()
894                finally:
895                    os.remove(temp_path)
896
897            font = TTFont()
898            font['CFF '] = newTable('CFF ')
899            font['CFF '].decompile(data, font)
900
901        self.ttFont = font
902        self.cffTable = font[cff_format]
903
904        # for identifier in glyph-list:
905        # Get charstring.
906        self.topDict = self.cffTable.cff.topDictIndex[0]
907        self.charStrings = self.topDict.CharStrings
908        if 'fvar' in self.ttFont:
909            self.is_vf = True
910            fvar = self.ttFont['fvar']
911            CFF2 = self.cffTable
912            CFF2.desubroutinize()
913            topDict = CFF2.cff.topDictIndex[0]
914            self.temp_cs = copy.deepcopy(self.charStrings['.notdef'])
915            self.vs_data_models = self.get_vs_data_models(topDict,
916                                                          fvar)
917
918    def getGlyphList(self):
919        return self.ttFont.getGlyphOrder()
920
921    def getPSName(self):
922        if self.is_cff2 and 'name' in self.ttFont:
923            psName = next((name_rec.string for name_rec in self.ttFont[
924                'name'].names if (name_rec.nameID == 6) and (
925                    name_rec.platformID == 3)))
926            psName = psName.decode('utf-16be')
927        else:
928            psName = self.cffTable.cff.fontNames[0]
929        return psName
930
931    def get_min_max(self, pTopDict, upm):
932        if self.is_cff2 and 'hhea' in self.ttFont:
933            font_max = self.ttFont['hhea'].ascent
934            font_min = self.ttFont['hhea'].descent
935        elif hasattr(pTopDict, 'FontBBox'):
936            font_max = pTopDict.FontBBox[3]
937            font_min = pTopDict.FontBBox[1]
938        else:
939            font_max = upm * 1.25
940            font_min = -upm * 0.25
941        alignment_min = min(-upm * 0.25, font_min)
942        alignment_max = max(upm * 1.25, font_max)
943        return alignment_min, alignment_max
944
945    def convertToBez(self, glyphName, read_hints, round_coords, doAll=False):
946        t2Wdth = None
947        t2CharString = self.charStrings[glyphName]
948        try:
949            bezString, t2Wdth = convertT2GlyphToBez(t2CharString,
950                                                    read_hints, round_coords)
951            # Note: the glyph name is important, as it is used by autohintexe
952            # for various heuristics, including [hv]stem3 derivation.
953            bezString = "% " + glyphName + "\n" + bezString
954        except SEACError:
955            log.warning("Skipping %s: can't process SEAC composite glyphs.",
956                        glyphName)
957            bezString = None
958        self.t2_widths[glyphName] = t2Wdth
959        return bezString
960
961    def updateFromBez(self, bezData, glyphName, mm_hint_info=None):
962        t2Program = convertBezToT2(bezData, mm_hint_info)
963        if not self.is_cff2:
964            t2_width_arg = self.t2_widths[glyphName]
965            if t2_width_arg is not None:
966                t2Program = [t2_width_arg] + t2Program
967        if self.vs_data_models is not None:
968            # It is a variable font. Accumulate the charstrings.
969            self.glyph_programs.append(t2Program)
970        else:
971            # This is an MM source font. Update the font's charstring directly.
972            t2CharString = self.charStrings[glyphName]
973            t2CharString.program = t2Program
974
975    def save(self, path):
976        if path is None:
977            path = self.inputPath
978
979        if self.font_format == "OTF":
980            self.ttFont.save(path)
981            self.ttFont.close()
982        else:
983            data = self.ttFont["CFF "].compile(self.ttFont)
984            if self.font_format == "CFF":
985                with open(path, "wb") as fp:
986                    fp.write(data)
987            else:
988                fd, temp_path = tempfile.mkstemp()
989                os.write(fd, data)
990                os.close(fd)
991
992                try:
993                    args = ["-t1", "-std"]
994                    if self.font_format == "PFB":
995                        args.append("-pfb")
996                    _run_tx(args + [temp_path, path])
997                finally:
998                    os.remove(temp_path)
999
1000    def close(self):
1001        self.ttFont.close()
1002
1003    def isCID(self):
1004        return hasattr(self.topDict, "FDSelect")
1005
1006    def hasFDArray(self):
1007        return self.is_cff2 or hasattr(self.topDict, "FDSelect")
1008
1009    def flattenBlends(self, blendList):
1010        if type(blendList[0]) is list:
1011            flatList = [blendList[i][0] for i in range(len(blendList))]
1012        else:
1013            flatList = blendList
1014        return flatList
1015
1016    def getFontInfo(self, allow_no_blues, noFlex,
1017                    vCounterGlyphs, hCounterGlyphs, fdIndex=0):
1018        # The psautohint library needs the global font hint zones
1019        # and standard stem widths.
1020        # Format them into a single text string.
1021        # The text format is arbitrary, inherited from very old software,
1022        # but there is no real need to change it.
1023        pTopDict = self.topDict
1024        if hasattr(pTopDict, "FDArray"):
1025            pDict = pTopDict.FDArray[fdIndex]
1026        else:
1027            pDict = pTopDict
1028        privateDict = pDict.Private
1029
1030        fdDict = fdTools.FDDict()
1031        fdDict.LanguageGroup = getattr(privateDict, "LanguageGroup", "0")
1032
1033        if hasattr(pDict, "FontMatrix"):
1034            fdDict.FontMatrix = pDict.FontMatrix
1035        else:
1036            fdDict.FontMatrix = pTopDict.FontMatrix
1037        upm = int(1 / fdDict.FontMatrix[0])
1038        fdDict.OrigEmSqUnits = str(upm)
1039
1040        fdDict.FontName = getattr(pTopDict, "FontName", self.getPSName())
1041
1042        blueValues = getattr(privateDict, "BlueValues", [])[:]
1043        numBlueValues = len(blueValues)
1044        if numBlueValues < 4:
1045            low, high = self.get_min_max(pTopDict, upm)
1046            # Make a set of inactive alignment zones: zones outside of the
1047            # font BBox so as not to affect hinting. Used when source font has
1048            # no BlueValues or has invalid BlueValues. Some fonts have bad BBox
1049            # values, so I don't let this be smaller than -upm*0.25, upm*1.25.
1050            inactiveAlignmentValues = [low, low, high, high]
1051            if allow_no_blues:
1052                blueValues = inactiveAlignmentValues
1053                numBlueValues = len(blueValues)
1054            else:
1055                raise FontParseError("Font must have at least four values in "
1056                                     "its BlueValues array for PSAutoHint to "
1057                                     "work!")
1058        blueValues.sort()
1059
1060        # The first pair only is a bottom zone, where the first value is the
1061        # overshoot position. The rest are top zones, and second value of the
1062        # pair is the overshoot position.
1063        blueValues = self.flattenBlends(blueValues)
1064        blueValues[0] = blueValues[0] - blueValues[1]
1065        for i in range(3, numBlueValues, 2):
1066            blueValues[i] = blueValues[i] - blueValues[i - 1]
1067
1068        blueValues = [str(v) for v in blueValues]
1069        numBlueValues = min(numBlueValues, len(fdTools.kBlueValueKeys))
1070        for i in range(numBlueValues):
1071            key = fdTools.kBlueValueKeys[i]
1072            value = blueValues[i]
1073            setattr(fdDict, key, value)
1074
1075        if hasattr(privateDict, "OtherBlues"):
1076            # For all OtherBlues, the pairs are bottom zones, and
1077            # the first value of each pair is the overshoot position.
1078            i = 0
1079            numBlueValues = len(privateDict.OtherBlues)
1080            blueValues = privateDict.OtherBlues[:]
1081            blueValues.sort()
1082            blueValues = self.flattenBlends(blueValues)
1083            for i in range(0, numBlueValues, 2):
1084                blueValues[i] = blueValues[i] - blueValues[i + 1]
1085            blueValues = [str(v) for v in blueValues]
1086            numBlueValues = min(numBlueValues,
1087                                len(fdTools.kOtherBlueValueKeys))
1088            for i in range(numBlueValues):
1089                key = fdTools.kOtherBlueValueKeys[i]
1090                value = blueValues[i]
1091                setattr(fdDict, key, value)
1092
1093        if hasattr(privateDict, "StemSnapV"):
1094            vstems = privateDict.StemSnapV
1095        elif hasattr(privateDict, "StdVW"):
1096            vstems = [privateDict.StdVW]
1097        else:
1098            if allow_no_blues:
1099                # dummy value. Needs to be larger than any hint will likely be,
1100                # as the autohint program strips out any hint wider than twice
1101                # the largest global stem width.
1102                vstems = [upm]
1103            else:
1104                raise FontParseError("Font has neither StemSnapV nor StdVW!")
1105        vstems.sort()
1106        vstems = self.flattenBlends(vstems)
1107        if (len(vstems) == 0) or ((len(vstems) == 1) and (vstems[0] < 1)):
1108            vstems = [upm]  # dummy value that will allow PyAC to run
1109            log.warning("There is no value or 0 value for DominantV.")
1110        fdDict.DominantV = "[" + " ".join([str(v) for v in vstems]) + "]"
1111
1112        if hasattr(privateDict, "StemSnapH"):
1113            hstems = privateDict.StemSnapH
1114        elif hasattr(privateDict, "StdHW"):
1115            hstems = [privateDict.StdHW]
1116        else:
1117            if allow_no_blues:
1118                # dummy value. Needs to be larger than any hint will likely be,
1119                # as the autohint program strips out any hint wider than twice
1120                # the largest global stem width.
1121                hstems = [upm]
1122            else:
1123                raise FontParseError("Font has neither StemSnapH nor StdHW!")
1124        hstems.sort()
1125        hstems = self.flattenBlends(hstems)
1126        if (len(hstems) == 0) or ((len(hstems) == 1) and (hstems[0] < 1)):
1127            hstems = [upm]  # dummy value that will allow PyAC to run
1128            log.warning("There is no value or 0 value for DominantH.")
1129        fdDict.DominantH = "[" + " ".join([str(v) for v in hstems]) + "]"
1130
1131        if noFlex:
1132            fdDict.FlexOK = "false"
1133        else:
1134            fdDict.FlexOK = "true"
1135
1136        # Add candidate lists for counter hints, if any.
1137        if vCounterGlyphs:
1138            temp = " ".join(vCounterGlyphs)
1139            fdDict.VCounterChars = "( %s )" % (temp)
1140        if hCounterGlyphs:
1141            temp = " ".join(hCounterGlyphs)
1142            fdDict.HCounterChars = "( %s )" % (temp)
1143
1144        fdDict.BlueFuzz = getattr(privateDict, "BlueFuzz", 1)
1145
1146        return fdDict
1147
1148    def getfdIndex(self, name):
1149        gid = self.ttFont.getGlyphID(name)
1150        if hasattr(self.topDict, "FDSelect"):
1151            fdIndex = self.topDict.FDSelect[gid]
1152        else:
1153            fdIndex = 0
1154        return fdIndex
1155
1156    def getfdInfo(self, allow_no_blues, noFlex, vCounterGlyphs, hCounterGlyphs,
1157                  glyphList, fdIndex=0):
1158        topDict = self.topDict
1159        fdGlyphDict = None
1160
1161        # Get the default fontinfo from the font's top dict.
1162        fdDict = self.getFontInfo(
1163            allow_no_blues, noFlex, vCounterGlyphs, hCounterGlyphs, fdIndex)
1164        fontDictList = [fdDict]
1165
1166        # Check the fontinfo file, and add any other font dicts
1167        srcFontInfo = os.path.dirname(self.inputPath)
1168        srcFontInfo = os.path.join(srcFontInfo, "fontinfo")
1169        if os.path.exists(srcFontInfo):
1170            with open(srcFontInfo, "r", encoding="utf-8") as fi:
1171                fontInfoData = fi.read()
1172            fontInfoData = re.sub(r"#[^\r\n]+", "", fontInfoData)
1173        else:
1174            return fdGlyphDict, fontDictList
1175
1176        if "FDDict" in fontInfoData:
1177            maxY = topDict.FontBBox[3]
1178            minY = topDict.FontBBox[1]
1179            fdGlyphDict, fontDictList, finalFDict = fdTools.parseFontInfoFile(
1180                fontDictList, fontInfoData, glyphList, maxY, minY,
1181                self.getPSName())
1182            if hasattr(topDict, "FDArray"):
1183                private = topDict.FDArray[fdIndex].Private
1184            else:
1185                private = topDict.Private
1186            if finalFDict is None:
1187                # If a font dict was not explicitly specified for the
1188                # output font, use the first user-specified font dict.
1189                fdTools.mergeFDDicts(fontDictList[1:], private)
1190            else:
1191                fdTools.mergeFDDicts([finalFDict], private)
1192        return fdGlyphDict, fontDictList
1193
1194    @staticmethod
1195    def args_to_hints(hint_args):
1196        hints = [hint_args[0:2]]
1197        prev = hints[0]
1198        for i in range(2, len(hint_args), 2):
1199            bottom = hint_args[i] + prev[0] + prev[1]
1200            hints.append([bottom, hint_args[i + 1]])
1201            prev = hints[-1]
1202        return hints
1203
1204    @staticmethod
1205    def extract_hint_args(program):
1206        width = None
1207        h_hint_args = []
1208        v_hint_args = []
1209        for i, token in enumerate(program):
1210            if type(token) is str:
1211                if i % 2 != 0:
1212                    width = program[0]
1213                    del program[0]
1214                    idx = i - 1
1215                else:
1216                    idx = i
1217
1218                if (token[:4] == 'vstem') or token[-3:] == 'mask':
1219                    h_hint_args = []
1220                    v_hint_args = program[:idx]
1221
1222                elif token[:5] == 'hstem':
1223                    h_hint_args = program[:idx]
1224                    v_program = program[idx+1:]
1225
1226                    for j, vtoken in enumerate(v_program):
1227                        if type(vtoken) is str:
1228                            if (vtoken[:5] == 'vstem') or vtoken[-4:] == \
1229                                    'mask':
1230                                v_hint_args = v_program[:j]
1231                                break
1232                break
1233
1234        return width, h_hint_args, v_hint_args
1235
1236    def fix_t2_program_hints(self, program, mm_hint_info, is_reference_font):
1237
1238        width_arg, h_hint_args, v_hint_args = self.extract_hint_args(program)
1239
1240        # 1. Build list of good [vh]hints.
1241        bad_hint_idxs = list(mm_hint_info.bad_hint_idxs)
1242        bad_hint_idxs.sort()
1243        num_hhint_pairs = len(h_hint_args) // 2
1244        for idx in reversed(bad_hint_idxs):
1245            if idx < num_hhint_pairs:
1246                hint_args = h_hint_args
1247                bottom_idx = idx * 2
1248            else:
1249                hint_args = v_hint_args
1250                bottom_idx = (idx - num_hhint_pairs) * 2
1251            delta = hint_args[bottom_idx] + hint_args[bottom_idx + 1]
1252            del hint_args[bottom_idx:bottom_idx + 2]
1253            if len(hint_args) > bottom_idx:
1254                hint_args[bottom_idx] += delta
1255
1256        # delete old hints from program
1257        if mm_hint_info.cntr_masks:
1258            last_hint_idx = program.index('cntrmask')
1259        elif mm_hint_info.hint_masks:
1260            last_hint_idx = program.index('hintmask')
1261        else:
1262            for op in ['vstem', 'hstem']:
1263                try:
1264                    last_hint_idx = program.index(op)
1265                    break
1266                except IndexError:
1267                    last_hint_idx = None
1268        if last_hint_idx is not None:
1269            del program[:last_hint_idx]
1270
1271        # If there were v_hint_args, but they have now all been
1272        # deleted, the first token will still be 'vstem[hm]'. Delete it.
1273        if ((not v_hint_args) and program[0].startswith('vstem')):
1274            del program[0]
1275
1276        # Add width and updated hints back.
1277        if width_arg is not None:
1278            hint_program = [width_arg]
1279        else:
1280            hint_program = []
1281        if h_hint_args:
1282            op_hstem = 'hstemhm' if mm_hint_info.hint_masks else 'hstem'
1283            hint_program.extend(h_hint_args)
1284            hint_program.append(op_hstem)
1285        if v_hint_args:
1286            hint_program.extend(v_hint_args)
1287            # Don't need to append op_vstem, as this is still in hint_program.
1288            program = hint_program + program
1289
1290        # Re-calculate the hint masks.
1291        if is_reference_font:
1292            hhints = self.args_to_hints(h_hint_args)
1293            vhints = self.args_to_hints(v_hint_args)
1294            for hm in mm_hint_info.hint_masks:
1295                hm.maskByte(hhints, vhints)
1296
1297        # Apply fixed hint masks
1298        if mm_hint_info.hint_masks:
1299            hm_pos_list = [i for i, token in enumerate(program)
1300                           if token == 'hintmask']
1301            for i, hm in enumerate(mm_hint_info.hint_masks):
1302                pos = hm_pos_list[i]
1303                program[pos + 1] = hm.mask
1304
1305        # Now fix the control masks. We will weed out a control mask
1306        # if it ends up with fewer than 3 hints.
1307        cntr_masks = mm_hint_info.cntr_masks
1308        if is_reference_font and cntr_masks:
1309            # Update mask bytes,
1310            # and remove control masks with fewer than 3 bits.
1311            mask_byte_list = [cm.mask for cm in cntr_masks]
1312            for cm in cntr_masks:
1313                cm.maskByte(hhints, vhints)
1314            new_cm_list = [cm for cm in cntr_masks if cm.num_bits >= 3]
1315            new_mask_byte_list = [cm.mask for cm in new_cm_list]
1316            if new_mask_byte_list != mask_byte_list:
1317                mm_hint_info.new_cntr_masks = new_cm_list
1318        if mm_hint_info.new_cntr_masks:
1319            # Remove all the old cntrmask ops
1320            num_old_cm = len(cntr_masks)
1321            idx = program.index('cntrmask')
1322            del program[idx:idx + num_old_cm * 2]
1323            cm_progam = [['cntrmask', cm.mask] for cm in
1324                         mm_hint_info.new_cntr_masks]
1325            cm_progam = list(itertools.chain(*cm_progam))
1326            program[idx:idx] = cm_progam
1327        return program
1328
1329    def fix_glyph_hints(self, glyph_name, mm_hint_info,
1330                        is_reference_font=None):
1331        # 1. Delete any bad hints.
1332        # 2. If reference font, recalculate the hint mask byte strings
1333        # 3. Replace hint masks.
1334        # 3. Fix cntr masks.
1335        if self.is_vf:
1336            # We get called once, and fix all the charstring programs.
1337            for i, t2_program in enumerate(self.glyph_programs):
1338                self.glyph_programs[i] = self.fix_t2_program_hints(
1339                    t2_program, mm_hint_info, is_reference_font=(i == 0))
1340        else:
1341            # we are called for each font in turn
1342            try:
1343                t2CharString = self.charStrings[glyph_name]
1344            except KeyError:
1345                return  # Happens with sparse sources - just skip the glyph.
1346
1347            program = self.fix_t2_program_hints(t2CharString.program,
1348                                                mm_hint_info,
1349                                                is_reference_font)
1350            t2CharString.program = program
1351
1352    def get_vf_bez_glyphs(self, glyph_name):
1353
1354        charstring = self.charStrings[glyph_name]
1355        if 'fvar' in self.ttFont:
1356            # have not yet collected VF global data.
1357            self.is_vf = True
1358            fvar = self.ttFont['fvar']
1359            CFF2 = self.cffTable
1360            CFF2.desubroutinize()
1361            topDict = CFF2.cff.topDictIndex[0]
1362            # We need a new charstring object into which we can save the
1363            # hinted CFF2 program data. Copying an existing charstring is a
1364            # little easier than creating a new one and making sure that all
1365            # properties are set correctly.
1366            self.temp_cs = copy.deepcopy(self.charStrings['.notdef'])
1367            self.vs_data_models = self.get_vs_data_models(topDict,
1368                                                          fvar)
1369
1370        if 'vsindex' in charstring.program:
1371            op_index = charstring.program.index('vsindex')
1372            vsindex = charstring.program[op_index - 1]
1373        else:
1374            vsindex = 0
1375        self.vsindex = vsindex
1376        self.glyph_programs = []
1377        vs_data_model = self.vs_data_model = self.vs_data_models[vsindex]
1378
1379        bez_list = []
1380        for vsi in vs_data_model.master_vsi_list:
1381            t2_program = interpolate_cff2_charstring(charstring, glyph_name,
1382                                                     vsi.interpolateFromDeltas,
1383                                                     vsindex)
1384            self.temp_cs.program = t2_program
1385            bezString, _ = convertT2GlyphToBez(self.temp_cs, True, True)
1386            #  DBG Adding glyph name is useful only for debugging.
1387            bezString = "% {}\n".format(glyph_name) + bezString
1388            bez_list.append(bezString)
1389        return bez_list
1390
1391    @staticmethod
1392    def get_vs_data_models(topDict, fvar):
1393        otvs = topDict.VarStore.otVarStore
1394        region_list = otvs.VarRegionList.Region
1395        axis_tags = [axis_entry.axisTag for axis_entry in fvar.axes]
1396        vs_data_models = []
1397        for vsindex, var_data in enumerate(otvs.VarData):
1398            vsi = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, {})
1399            master_vsi_list = [vsi]
1400            for region_idx in var_data.VarRegionIndex:
1401                region = region_list[region_idx]
1402                loc = {}
1403                for i, axis in enumerate(region.VarRegionAxis):
1404                    loc[axis_tags[i]] = axis.PeakCoord
1405                vsi = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes,
1406                                        loc)
1407                master_vsi_list.append(vsi)
1408            vdm = VarDataModel(var_data, vsindex, master_vsi_list)
1409            vs_data_models.append(vdm)
1410        return vs_data_models
1411
1412    def merge_hinted_glyphs(self, name):
1413        new_t2cs = merge_hinted_programs(self.temp_cs, self.glyph_programs,
1414                                         name, self.vs_data_model)
1415        if self.vsindex:
1416            new_t2cs.program = [self.vsindex, 'vsindex'] + new_t2cs.program
1417        self.charStrings[name] = new_t2cs
1418
1419
1420def interpolate_cff2_charstring(charstring, gname, interpolateFromDeltas,
1421                                vsindex):
1422    # Interpolate charstring
1423    # e.g replace blend op args with regular args,
1424    # and discard vsindex op.
1425    new_program = []
1426    last_i = 0
1427    program = charstring.program
1428    for i, token in enumerate(program):
1429        if token == 'vsindex':
1430            if last_i != 0:
1431                new_program.extend(program[last_i:i - 1])
1432            last_i = i + 1
1433        elif token == 'blend':
1434            num_regions = charstring.getNumRegions(vsindex)
1435            numMasters = 1 + num_regions
1436            num_args = program[i - 1]
1437            # The program list starting at program[i] is now:
1438            # ..args for following operations
1439            # num_args values  from the default font
1440            # num_args tuples, each with numMasters-1 delta values
1441            # num_blend_args
1442            # 'blend'
1443            argi = i - (num_args * numMasters + 1)
1444            if last_i != argi:
1445                new_program.extend(program[last_i:argi])
1446            end_args = tuplei = argi + num_args
1447            master_args = []
1448            while argi < end_args:
1449                next_ti = tuplei + num_regions
1450                deltas = program[tuplei:next_ti]
1451                val = interpolateFromDeltas(vsindex, deltas)
1452                master_val = program[argi]
1453                master_val += otRound(val)
1454                master_args.append(master_val)
1455                tuplei = next_ti
1456                argi += 1
1457            new_program.extend(master_args)
1458            last_i = i + 1
1459    if last_i != 0:
1460        new_program.extend(program[last_i:])
1461    return new_program
1462
1463
1464def merge_hinted_programs(charstring, t2_programs, gname, vs_data_model):
1465    num_masters = vs_data_model.num_masters
1466    var_pen = CFF2CharStringMergePen([], gname, num_masters, 0)
1467    charstring.outlineExtractor = MergeOutlineExtractor
1468
1469    for i, t2_program in enumerate(t2_programs):
1470        var_pen.restart(i)
1471        charstring.program = t2_program
1472        charstring.draw(var_pen)
1473
1474    new_charstring = var_pen.getCharString(
1475        private=charstring.private,
1476        globalSubrs=charstring.globalSubrs,
1477        var_model=vs_data_model, optimize=True)
1478    return new_charstring
1479
1480
1481@_add_method(VarStoreInstancer)
1482def get_scalars(self, vsindex, region_idx):
1483    varData = self._varData
1484    # The index key needs to be the master value index, which includes
1485    # the default font value. VarRegionIndex provides the region indices.
1486    scalars = {0: 1.0}  # The default font always has a weight of 1.0
1487    region_index = varData[vsindex].VarRegionIndex
1488    for idx in range(region_idx):  # omit the scalar for the region.
1489        scalar = self._getScalar(region_index[idx])
1490        if scalar:
1491            scalars[idx+1] = scalar
1492    return scalars
1493
1494
1495class VarDataModel(object):
1496
1497    def __init__(self, var_data, vsindex, master_vsi_list):
1498        self.master_vsi_list = master_vsi_list
1499        self.var_data = var_data
1500        self._num_masters = len(master_vsi_list)
1501        self.delta_weights = [{}]  # for default font value
1502        for region_idx, vsi in enumerate(master_vsi_list[1:]):
1503            scalars = vsi.get_scalars(vsindex, region_idx)
1504            self.delta_weights.append(scalars)
1505
1506    @property
1507    def num_masters(self):
1508        return self._num_masters
1509
1510    def getDeltas(self, master_values):
1511        assert len(master_values) == len(self.delta_weights)
1512        out = []
1513        for i, scalars in enumerate(self.delta_weights):
1514            delta = master_values[i]
1515            for j, scalar in scalars.items():
1516                if scalar:
1517                    delta -= out[j] * scalar
1518            out.append(delta)
1519        return out
1520