xref: /aosp_15_r20/external/fonttools/Lib/fontTools/varLib/mutator.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""
2*e1fe3e4aSElliott HughesInstantiate a variation font.  Run, eg:
3*e1fe3e4aSElliott Hughes
4*e1fe3e4aSElliott Hughes$ fonttools varLib.mutator ./NotoSansArabic-VF.ttf wght=140 wdth=85
5*e1fe3e4aSElliott Hughes"""
6*e1fe3e4aSElliott Hughes
7*e1fe3e4aSElliott Hughesfrom fontTools.misc.fixedTools import floatToFixedToFloat, floatToFixed
8*e1fe3e4aSElliott Hughesfrom fontTools.misc.roundTools import otRound
9*e1fe3e4aSElliott Hughesfrom fontTools.pens.boundsPen import BoundsPen
10*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import TTFont, newTable
11*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables import ttProgram
12*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._g_l_y_f import (
13*e1fe3e4aSElliott Hughes    GlyphCoordinates,
14*e1fe3e4aSElliott Hughes    flagOverlapSimple,
15*e1fe3e4aSElliott Hughes    OVERLAP_COMPOUND,
16*e1fe3e4aSElliott Hughes)
17*e1fe3e4aSElliott Hughesfrom fontTools.varLib.models import (
18*e1fe3e4aSElliott Hughes    supportScalar,
19*e1fe3e4aSElliott Hughes    normalizeLocation,
20*e1fe3e4aSElliott Hughes    piecewiseLinearMap,
21*e1fe3e4aSElliott Hughes)
22*e1fe3e4aSElliott Hughesfrom fontTools.varLib.merger import MutatorMerger
23*e1fe3e4aSElliott Hughesfrom fontTools.varLib.varStore import VarStoreInstancer
24*e1fe3e4aSElliott Hughesfrom fontTools.varLib.mvar import MVAR_ENTRIES
25*e1fe3e4aSElliott Hughesfrom fontTools.varLib.iup import iup_delta
26*e1fe3e4aSElliott Hughesimport fontTools.subset.cff
27*e1fe3e4aSElliott Hughesimport os.path
28*e1fe3e4aSElliott Hughesimport logging
29*e1fe3e4aSElliott Hughesfrom io import BytesIO
30*e1fe3e4aSElliott Hughes
31*e1fe3e4aSElliott Hughes
32*e1fe3e4aSElliott Hugheslog = logging.getLogger("fontTools.varlib.mutator")
33*e1fe3e4aSElliott Hughes
34*e1fe3e4aSElliott Hughes# map 'wdth' axis (1..200) to OS/2.usWidthClass (1..9), rounding to closest
35*e1fe3e4aSElliott HughesOS2_WIDTH_CLASS_VALUES = {}
36*e1fe3e4aSElliott Hughespercents = [50.0, 62.5, 75.0, 87.5, 100.0, 112.5, 125.0, 150.0, 200.0]
37*e1fe3e4aSElliott Hughesfor i, (prev, curr) in enumerate(zip(percents[:-1], percents[1:]), start=1):
38*e1fe3e4aSElliott Hughes    half = (prev + curr) / 2
39*e1fe3e4aSElliott Hughes    OS2_WIDTH_CLASS_VALUES[half] = i
40*e1fe3e4aSElliott Hughes
41*e1fe3e4aSElliott Hughes
42*e1fe3e4aSElliott Hughesdef interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas):
43*e1fe3e4aSElliott Hughes    pd_blend_lists = (
44*e1fe3e4aSElliott Hughes        "BlueValues",
45*e1fe3e4aSElliott Hughes        "OtherBlues",
46*e1fe3e4aSElliott Hughes        "FamilyBlues",
47*e1fe3e4aSElliott Hughes        "FamilyOtherBlues",
48*e1fe3e4aSElliott Hughes        "StemSnapH",
49*e1fe3e4aSElliott Hughes        "StemSnapV",
50*e1fe3e4aSElliott Hughes    )
51*e1fe3e4aSElliott Hughes    pd_blend_values = ("BlueScale", "BlueShift", "BlueFuzz", "StdHW", "StdVW")
52*e1fe3e4aSElliott Hughes    for fontDict in topDict.FDArray:
53*e1fe3e4aSElliott Hughes        pd = fontDict.Private
54*e1fe3e4aSElliott Hughes        vsindex = pd.vsindex if (hasattr(pd, "vsindex")) else 0
55*e1fe3e4aSElliott Hughes        for key, value in pd.rawDict.items():
56*e1fe3e4aSElliott Hughes            if (key in pd_blend_values) and isinstance(value, list):
57*e1fe3e4aSElliott Hughes                delta = interpolateFromDeltas(vsindex, value[1:])
58*e1fe3e4aSElliott Hughes                pd.rawDict[key] = otRound(value[0] + delta)
59*e1fe3e4aSElliott Hughes            elif (key in pd_blend_lists) and isinstance(value[0], list):
60*e1fe3e4aSElliott Hughes                """If any argument in a BlueValues list is a blend list,
61*e1fe3e4aSElliott Hughes                then they all are. The first value of each list is an
62*e1fe3e4aSElliott Hughes                absolute value. The delta tuples are calculated from
63*e1fe3e4aSElliott Hughes                relative master values, hence we need to append all the
64*e1fe3e4aSElliott Hughes                deltas to date to each successive absolute value."""
65*e1fe3e4aSElliott Hughes                delta = 0
66*e1fe3e4aSElliott Hughes                for i, val_list in enumerate(value):
67*e1fe3e4aSElliott Hughes                    delta += otRound(interpolateFromDeltas(vsindex, val_list[1:]))
68*e1fe3e4aSElliott Hughes                    value[i] = val_list[0] + delta
69*e1fe3e4aSElliott Hughes
70*e1fe3e4aSElliott Hughes
71*e1fe3e4aSElliott Hughesdef interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder):
72*e1fe3e4aSElliott Hughes    charstrings = topDict.CharStrings
73*e1fe3e4aSElliott Hughes    for gname in glyphOrder:
74*e1fe3e4aSElliott Hughes        # Interpolate charstring
75*e1fe3e4aSElliott Hughes        # e.g replace blend op args with regular args,
76*e1fe3e4aSElliott Hughes        # and use and discard vsindex op.
77*e1fe3e4aSElliott Hughes        charstring = charstrings[gname]
78*e1fe3e4aSElliott Hughes        new_program = []
79*e1fe3e4aSElliott Hughes        vsindex = 0
80*e1fe3e4aSElliott Hughes        last_i = 0
81*e1fe3e4aSElliott Hughes        for i, token in enumerate(charstring.program):
82*e1fe3e4aSElliott Hughes            if token == "vsindex":
83*e1fe3e4aSElliott Hughes                vsindex = charstring.program[i - 1]
84*e1fe3e4aSElliott Hughes                if last_i != 0:
85*e1fe3e4aSElliott Hughes                    new_program.extend(charstring.program[last_i : i - 1])
86*e1fe3e4aSElliott Hughes                last_i = i + 1
87*e1fe3e4aSElliott Hughes            elif token == "blend":
88*e1fe3e4aSElliott Hughes                num_regions = charstring.getNumRegions(vsindex)
89*e1fe3e4aSElliott Hughes                numMasters = 1 + num_regions
90*e1fe3e4aSElliott Hughes                num_args = charstring.program[i - 1]
91*e1fe3e4aSElliott Hughes                # The program list starting at program[i] is now:
92*e1fe3e4aSElliott Hughes                # ..args for following operations
93*e1fe3e4aSElliott Hughes                # num_args values  from the default font
94*e1fe3e4aSElliott Hughes                # num_args tuples, each with numMasters-1 delta values
95*e1fe3e4aSElliott Hughes                # num_blend_args
96*e1fe3e4aSElliott Hughes                # 'blend'
97*e1fe3e4aSElliott Hughes                argi = i - (num_args * numMasters + 1)
98*e1fe3e4aSElliott Hughes                end_args = tuplei = argi + num_args
99*e1fe3e4aSElliott Hughes                while argi < end_args:
100*e1fe3e4aSElliott Hughes                    next_ti = tuplei + num_regions
101*e1fe3e4aSElliott Hughes                    deltas = charstring.program[tuplei:next_ti]
102*e1fe3e4aSElliott Hughes                    delta = interpolateFromDeltas(vsindex, deltas)
103*e1fe3e4aSElliott Hughes                    charstring.program[argi] += otRound(delta)
104*e1fe3e4aSElliott Hughes                    tuplei = next_ti
105*e1fe3e4aSElliott Hughes                    argi += 1
106*e1fe3e4aSElliott Hughes                new_program.extend(charstring.program[last_i:end_args])
107*e1fe3e4aSElliott Hughes                last_i = i + 1
108*e1fe3e4aSElliott Hughes        if last_i != 0:
109*e1fe3e4aSElliott Hughes            new_program.extend(charstring.program[last_i:])
110*e1fe3e4aSElliott Hughes            charstring.program = new_program
111*e1fe3e4aSElliott Hughes
112*e1fe3e4aSElliott Hughes
113*e1fe3e4aSElliott Hughesdef interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc):
114*e1fe3e4aSElliott Hughes    """Unlike TrueType glyphs, neither advance width nor bounding box
115*e1fe3e4aSElliott Hughes    info is stored in a CFF2 charstring. The width data exists only in
116*e1fe3e4aSElliott Hughes    the hmtx and HVAR tables. Since LSB data cannot be interpolated
117*e1fe3e4aSElliott Hughes    reliably from the master LSB values in the hmtx table, we traverse
118*e1fe3e4aSElliott Hughes    the charstring to determine the actual bound box."""
119*e1fe3e4aSElliott Hughes
120*e1fe3e4aSElliott Hughes    charstrings = topDict.CharStrings
121*e1fe3e4aSElliott Hughes    boundsPen = BoundsPen(glyphOrder)
122*e1fe3e4aSElliott Hughes    hmtx = varfont["hmtx"]
123*e1fe3e4aSElliott Hughes    hvar_table = None
124*e1fe3e4aSElliott Hughes    if "HVAR" in varfont:
125*e1fe3e4aSElliott Hughes        hvar_table = varfont["HVAR"].table
126*e1fe3e4aSElliott Hughes        fvar = varfont["fvar"]
127*e1fe3e4aSElliott Hughes        varStoreInstancer = VarStoreInstancer(hvar_table.VarStore, fvar.axes, loc)
128*e1fe3e4aSElliott Hughes
129*e1fe3e4aSElliott Hughes    for gid, gname in enumerate(glyphOrder):
130*e1fe3e4aSElliott Hughes        entry = list(hmtx[gname])
131*e1fe3e4aSElliott Hughes        # get width delta.
132*e1fe3e4aSElliott Hughes        if hvar_table:
133*e1fe3e4aSElliott Hughes            if hvar_table.AdvWidthMap:
134*e1fe3e4aSElliott Hughes                width_idx = hvar_table.AdvWidthMap.mapping[gname]
135*e1fe3e4aSElliott Hughes            else:
136*e1fe3e4aSElliott Hughes                width_idx = gid
137*e1fe3e4aSElliott Hughes            width_delta = otRound(varStoreInstancer[width_idx])
138*e1fe3e4aSElliott Hughes        else:
139*e1fe3e4aSElliott Hughes            width_delta = 0
140*e1fe3e4aSElliott Hughes
141*e1fe3e4aSElliott Hughes        # get LSB.
142*e1fe3e4aSElliott Hughes        boundsPen.init()
143*e1fe3e4aSElliott Hughes        charstring = charstrings[gname]
144*e1fe3e4aSElliott Hughes        charstring.draw(boundsPen)
145*e1fe3e4aSElliott Hughes        if boundsPen.bounds is None:
146*e1fe3e4aSElliott Hughes            # Happens with non-marking glyphs
147*e1fe3e4aSElliott Hughes            lsb_delta = 0
148*e1fe3e4aSElliott Hughes        else:
149*e1fe3e4aSElliott Hughes            lsb = otRound(boundsPen.bounds[0])
150*e1fe3e4aSElliott Hughes            lsb_delta = entry[1] - lsb
151*e1fe3e4aSElliott Hughes
152*e1fe3e4aSElliott Hughes        if lsb_delta or width_delta:
153*e1fe3e4aSElliott Hughes            if width_delta:
154*e1fe3e4aSElliott Hughes                entry[0] = max(0, entry[0] + width_delta)
155*e1fe3e4aSElliott Hughes            if lsb_delta:
156*e1fe3e4aSElliott Hughes                entry[1] = lsb
157*e1fe3e4aSElliott Hughes            hmtx[gname] = tuple(entry)
158*e1fe3e4aSElliott Hughes
159*e1fe3e4aSElliott Hughes
160*e1fe3e4aSElliott Hughesdef instantiateVariableFont(varfont, location, inplace=False, overlap=True):
161*e1fe3e4aSElliott Hughes    """Generate a static instance from a variable TTFont and a dictionary
162*e1fe3e4aSElliott Hughes    defining the desired location along the variable font's axes.
163*e1fe3e4aSElliott Hughes    The location values must be specified as user-space coordinates, e.g.:
164*e1fe3e4aSElliott Hughes
165*e1fe3e4aSElliott Hughes            {'wght': 400, 'wdth': 100}
166*e1fe3e4aSElliott Hughes
167*e1fe3e4aSElliott Hughes    By default, a new TTFont object is returned. If ``inplace`` is True, the
168*e1fe3e4aSElliott Hughes    input varfont is modified and reduced to a static font.
169*e1fe3e4aSElliott Hughes
170*e1fe3e4aSElliott Hughes    When the overlap parameter is defined as True,
171*e1fe3e4aSElliott Hughes    OVERLAP_SIMPLE and OVERLAP_COMPOUND bits are set to 1.  See
172*e1fe3e4aSElliott Hughes    https://docs.microsoft.com/en-us/typography/opentype/spec/glyf
173*e1fe3e4aSElliott Hughes    """
174*e1fe3e4aSElliott Hughes    if not inplace:
175*e1fe3e4aSElliott Hughes        # make a copy to leave input varfont unmodified
176*e1fe3e4aSElliott Hughes        stream = BytesIO()
177*e1fe3e4aSElliott Hughes        varfont.save(stream)
178*e1fe3e4aSElliott Hughes        stream.seek(0)
179*e1fe3e4aSElliott Hughes        varfont = TTFont(stream)
180*e1fe3e4aSElliott Hughes
181*e1fe3e4aSElliott Hughes    fvar = varfont["fvar"]
182*e1fe3e4aSElliott Hughes    axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes}
183*e1fe3e4aSElliott Hughes    loc = normalizeLocation(location, axes)
184*e1fe3e4aSElliott Hughes    if "avar" in varfont:
185*e1fe3e4aSElliott Hughes        maps = varfont["avar"].segments
186*e1fe3e4aSElliott Hughes        loc = {k: piecewiseLinearMap(v, maps[k]) for k, v in loc.items()}
187*e1fe3e4aSElliott Hughes    # Quantize to F2Dot14, to avoid surprise interpolations.
188*e1fe3e4aSElliott Hughes    loc = {k: floatToFixedToFloat(v, 14) for k, v in loc.items()}
189*e1fe3e4aSElliott Hughes    # Location is normalized now
190*e1fe3e4aSElliott Hughes    log.info("Normalized location: %s", loc)
191*e1fe3e4aSElliott Hughes
192*e1fe3e4aSElliott Hughes    if "gvar" in varfont:
193*e1fe3e4aSElliott Hughes        log.info("Mutating glyf/gvar tables")
194*e1fe3e4aSElliott Hughes        gvar = varfont["gvar"]
195*e1fe3e4aSElliott Hughes        glyf = varfont["glyf"]
196*e1fe3e4aSElliott Hughes        hMetrics = varfont["hmtx"].metrics
197*e1fe3e4aSElliott Hughes        vMetrics = getattr(varfont.get("vmtx"), "metrics", None)
198*e1fe3e4aSElliott Hughes        # get list of glyph names in gvar sorted by component depth
199*e1fe3e4aSElliott Hughes        glyphnames = sorted(
200*e1fe3e4aSElliott Hughes            gvar.variations.keys(),
201*e1fe3e4aSElliott Hughes            key=lambda name: (
202*e1fe3e4aSElliott Hughes                (
203*e1fe3e4aSElliott Hughes                    glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth
204*e1fe3e4aSElliott Hughes                    if glyf[name].isComposite() or glyf[name].isVarComposite()
205*e1fe3e4aSElliott Hughes                    else 0
206*e1fe3e4aSElliott Hughes                ),
207*e1fe3e4aSElliott Hughes                name,
208*e1fe3e4aSElliott Hughes            ),
209*e1fe3e4aSElliott Hughes        )
210*e1fe3e4aSElliott Hughes        for glyphname in glyphnames:
211*e1fe3e4aSElliott Hughes            variations = gvar.variations[glyphname]
212*e1fe3e4aSElliott Hughes            coordinates, _ = glyf._getCoordinatesAndControls(
213*e1fe3e4aSElliott Hughes                glyphname, hMetrics, vMetrics
214*e1fe3e4aSElliott Hughes            )
215*e1fe3e4aSElliott Hughes            origCoords, endPts = None, None
216*e1fe3e4aSElliott Hughes            for var in variations:
217*e1fe3e4aSElliott Hughes                scalar = supportScalar(loc, var.axes)
218*e1fe3e4aSElliott Hughes                if not scalar:
219*e1fe3e4aSElliott Hughes                    continue
220*e1fe3e4aSElliott Hughes                delta = var.coordinates
221*e1fe3e4aSElliott Hughes                if None in delta:
222*e1fe3e4aSElliott Hughes                    if origCoords is None:
223*e1fe3e4aSElliott Hughes                        origCoords, g = glyf._getCoordinatesAndControls(
224*e1fe3e4aSElliott Hughes                            glyphname, hMetrics, vMetrics
225*e1fe3e4aSElliott Hughes                        )
226*e1fe3e4aSElliott Hughes                    delta = iup_delta(delta, origCoords, g.endPts)
227*e1fe3e4aSElliott Hughes                coordinates += GlyphCoordinates(delta) * scalar
228*e1fe3e4aSElliott Hughes            glyf._setCoordinates(glyphname, coordinates, hMetrics, vMetrics)
229*e1fe3e4aSElliott Hughes    else:
230*e1fe3e4aSElliott Hughes        glyf = None
231*e1fe3e4aSElliott Hughes
232*e1fe3e4aSElliott Hughes    if "DSIG" in varfont:
233*e1fe3e4aSElliott Hughes        del varfont["DSIG"]
234*e1fe3e4aSElliott Hughes
235*e1fe3e4aSElliott Hughes    if "cvar" in varfont:
236*e1fe3e4aSElliott Hughes        log.info("Mutating cvt/cvar tables")
237*e1fe3e4aSElliott Hughes        cvar = varfont["cvar"]
238*e1fe3e4aSElliott Hughes        cvt = varfont["cvt "]
239*e1fe3e4aSElliott Hughes        deltas = {}
240*e1fe3e4aSElliott Hughes        for var in cvar.variations:
241*e1fe3e4aSElliott Hughes            scalar = supportScalar(loc, var.axes)
242*e1fe3e4aSElliott Hughes            if not scalar:
243*e1fe3e4aSElliott Hughes                continue
244*e1fe3e4aSElliott Hughes            for i, c in enumerate(var.coordinates):
245*e1fe3e4aSElliott Hughes                if c is not None:
246*e1fe3e4aSElliott Hughes                    deltas[i] = deltas.get(i, 0) + scalar * c
247*e1fe3e4aSElliott Hughes        for i, delta in deltas.items():
248*e1fe3e4aSElliott Hughes            cvt[i] += otRound(delta)
249*e1fe3e4aSElliott Hughes
250*e1fe3e4aSElliott Hughes    if "CFF2" in varfont:
251*e1fe3e4aSElliott Hughes        log.info("Mutating CFF2 table")
252*e1fe3e4aSElliott Hughes        glyphOrder = varfont.getGlyphOrder()
253*e1fe3e4aSElliott Hughes        CFF2 = varfont["CFF2"]
254*e1fe3e4aSElliott Hughes        topDict = CFF2.cff.topDictIndex[0]
255*e1fe3e4aSElliott Hughes        vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc)
256*e1fe3e4aSElliott Hughes        interpolateFromDeltas = vsInstancer.interpolateFromDeltas
257*e1fe3e4aSElliott Hughes        interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas)
258*e1fe3e4aSElliott Hughes        CFF2.desubroutinize()
259*e1fe3e4aSElliott Hughes        interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder)
260*e1fe3e4aSElliott Hughes        interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc)
261*e1fe3e4aSElliott Hughes        del topDict.rawDict["VarStore"]
262*e1fe3e4aSElliott Hughes        del topDict.VarStore
263*e1fe3e4aSElliott Hughes
264*e1fe3e4aSElliott Hughes    if "MVAR" in varfont:
265*e1fe3e4aSElliott Hughes        log.info("Mutating MVAR table")
266*e1fe3e4aSElliott Hughes        mvar = varfont["MVAR"].table
267*e1fe3e4aSElliott Hughes        varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc)
268*e1fe3e4aSElliott Hughes        records = mvar.ValueRecord
269*e1fe3e4aSElliott Hughes        for rec in records:
270*e1fe3e4aSElliott Hughes            mvarTag = rec.ValueTag
271*e1fe3e4aSElliott Hughes            if mvarTag not in MVAR_ENTRIES:
272*e1fe3e4aSElliott Hughes                continue
273*e1fe3e4aSElliott Hughes            tableTag, itemName = MVAR_ENTRIES[mvarTag]
274*e1fe3e4aSElliott Hughes            delta = otRound(varStoreInstancer[rec.VarIdx])
275*e1fe3e4aSElliott Hughes            if not delta:
276*e1fe3e4aSElliott Hughes                continue
277*e1fe3e4aSElliott Hughes            setattr(
278*e1fe3e4aSElliott Hughes                varfont[tableTag],
279*e1fe3e4aSElliott Hughes                itemName,
280*e1fe3e4aSElliott Hughes                getattr(varfont[tableTag], itemName) + delta,
281*e1fe3e4aSElliott Hughes            )
282*e1fe3e4aSElliott Hughes
283*e1fe3e4aSElliott Hughes    log.info("Mutating FeatureVariations")
284*e1fe3e4aSElliott Hughes    for tableTag in "GSUB", "GPOS":
285*e1fe3e4aSElliott Hughes        if not tableTag in varfont:
286*e1fe3e4aSElliott Hughes            continue
287*e1fe3e4aSElliott Hughes        table = varfont[tableTag].table
288*e1fe3e4aSElliott Hughes        if not getattr(table, "FeatureVariations", None):
289*e1fe3e4aSElliott Hughes            continue
290*e1fe3e4aSElliott Hughes        variations = table.FeatureVariations
291*e1fe3e4aSElliott Hughes        for record in variations.FeatureVariationRecord:
292*e1fe3e4aSElliott Hughes            applies = True
293*e1fe3e4aSElliott Hughes            for condition in record.ConditionSet.ConditionTable:
294*e1fe3e4aSElliott Hughes                if condition.Format == 1:
295*e1fe3e4aSElliott Hughes                    axisIdx = condition.AxisIndex
296*e1fe3e4aSElliott Hughes                    axisTag = fvar.axes[axisIdx].axisTag
297*e1fe3e4aSElliott Hughes                    Min = condition.FilterRangeMinValue
298*e1fe3e4aSElliott Hughes                    Max = condition.FilterRangeMaxValue
299*e1fe3e4aSElliott Hughes                    v = loc[axisTag]
300*e1fe3e4aSElliott Hughes                    if not (Min <= v <= Max):
301*e1fe3e4aSElliott Hughes                        applies = False
302*e1fe3e4aSElliott Hughes                else:
303*e1fe3e4aSElliott Hughes                    applies = False
304*e1fe3e4aSElliott Hughes                if not applies:
305*e1fe3e4aSElliott Hughes                    break
306*e1fe3e4aSElliott Hughes
307*e1fe3e4aSElliott Hughes            if applies:
308*e1fe3e4aSElliott Hughes                assert record.FeatureTableSubstitution.Version == 0x00010000
309*e1fe3e4aSElliott Hughes                for rec in record.FeatureTableSubstitution.SubstitutionRecord:
310*e1fe3e4aSElliott Hughes                    table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = (
311*e1fe3e4aSElliott Hughes                        rec.Feature
312*e1fe3e4aSElliott Hughes                    )
313*e1fe3e4aSElliott Hughes                break
314*e1fe3e4aSElliott Hughes        del table.FeatureVariations
315*e1fe3e4aSElliott Hughes
316*e1fe3e4aSElliott Hughes    if "GDEF" in varfont and varfont["GDEF"].table.Version >= 0x00010003:
317*e1fe3e4aSElliott Hughes        log.info("Mutating GDEF/GPOS/GSUB tables")
318*e1fe3e4aSElliott Hughes        gdef = varfont["GDEF"].table
319*e1fe3e4aSElliott Hughes        instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc)
320*e1fe3e4aSElliott Hughes
321*e1fe3e4aSElliott Hughes        merger = MutatorMerger(varfont, instancer)
322*e1fe3e4aSElliott Hughes        merger.mergeTables(varfont, [varfont], ["GDEF", "GPOS"])
323*e1fe3e4aSElliott Hughes
324*e1fe3e4aSElliott Hughes        # Downgrade GDEF.
325*e1fe3e4aSElliott Hughes        del gdef.VarStore
326*e1fe3e4aSElliott Hughes        gdef.Version = 0x00010002
327*e1fe3e4aSElliott Hughes        if gdef.MarkGlyphSetsDef is None:
328*e1fe3e4aSElliott Hughes            del gdef.MarkGlyphSetsDef
329*e1fe3e4aSElliott Hughes            gdef.Version = 0x00010000
330*e1fe3e4aSElliott Hughes
331*e1fe3e4aSElliott Hughes        if not (
332*e1fe3e4aSElliott Hughes            gdef.LigCaretList
333*e1fe3e4aSElliott Hughes            or gdef.MarkAttachClassDef
334*e1fe3e4aSElliott Hughes            or gdef.GlyphClassDef
335*e1fe3e4aSElliott Hughes            or gdef.AttachList
336*e1fe3e4aSElliott Hughes            or (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)
337*e1fe3e4aSElliott Hughes        ):
338*e1fe3e4aSElliott Hughes            del varfont["GDEF"]
339*e1fe3e4aSElliott Hughes
340*e1fe3e4aSElliott Hughes    addidef = False
341*e1fe3e4aSElliott Hughes    if glyf:
342*e1fe3e4aSElliott Hughes        for glyph in glyf.glyphs.values():
343*e1fe3e4aSElliott Hughes            if hasattr(glyph, "program"):
344*e1fe3e4aSElliott Hughes                instructions = glyph.program.getAssembly()
345*e1fe3e4aSElliott Hughes                # If GETVARIATION opcode is used in bytecode of any glyph add IDEF
346*e1fe3e4aSElliott Hughes                addidef = any(op.startswith("GETVARIATION") for op in instructions)
347*e1fe3e4aSElliott Hughes                if addidef:
348*e1fe3e4aSElliott Hughes                    break
349*e1fe3e4aSElliott Hughes        if overlap:
350*e1fe3e4aSElliott Hughes            for glyph_name in glyf.keys():
351*e1fe3e4aSElliott Hughes                glyph = glyf[glyph_name]
352*e1fe3e4aSElliott Hughes                # Set OVERLAP_COMPOUND bit for compound glyphs
353*e1fe3e4aSElliott Hughes                if glyph.isComposite():
354*e1fe3e4aSElliott Hughes                    glyph.components[0].flags |= OVERLAP_COMPOUND
355*e1fe3e4aSElliott Hughes                # Set OVERLAP_SIMPLE bit for simple glyphs
356*e1fe3e4aSElliott Hughes                elif glyph.numberOfContours > 0:
357*e1fe3e4aSElliott Hughes                    glyph.flags[0] |= flagOverlapSimple
358*e1fe3e4aSElliott Hughes    if addidef:
359*e1fe3e4aSElliott Hughes        log.info("Adding IDEF to fpgm table for GETVARIATION opcode")
360*e1fe3e4aSElliott Hughes        asm = []
361*e1fe3e4aSElliott Hughes        if "fpgm" in varfont:
362*e1fe3e4aSElliott Hughes            fpgm = varfont["fpgm"]
363*e1fe3e4aSElliott Hughes            asm = fpgm.program.getAssembly()
364*e1fe3e4aSElliott Hughes        else:
365*e1fe3e4aSElliott Hughes            fpgm = newTable("fpgm")
366*e1fe3e4aSElliott Hughes            fpgm.program = ttProgram.Program()
367*e1fe3e4aSElliott Hughes            varfont["fpgm"] = fpgm
368*e1fe3e4aSElliott Hughes        asm.append("PUSHB[000] 145")
369*e1fe3e4aSElliott Hughes        asm.append("IDEF[ ]")
370*e1fe3e4aSElliott Hughes        args = [str(len(loc))]
371*e1fe3e4aSElliott Hughes        for a in fvar.axes:
372*e1fe3e4aSElliott Hughes            args.append(str(floatToFixed(loc[a.axisTag], 14)))
373*e1fe3e4aSElliott Hughes        asm.append("NPUSHW[ ] " + " ".join(args))
374*e1fe3e4aSElliott Hughes        asm.append("ENDF[ ]")
375*e1fe3e4aSElliott Hughes        fpgm.program.fromAssembly(asm)
376*e1fe3e4aSElliott Hughes
377*e1fe3e4aSElliott Hughes        # Change maxp attributes as IDEF is added
378*e1fe3e4aSElliott Hughes        if "maxp" in varfont:
379*e1fe3e4aSElliott Hughes            maxp = varfont["maxp"]
380*e1fe3e4aSElliott Hughes            setattr(
381*e1fe3e4aSElliott Hughes                maxp, "maxInstructionDefs", 1 + getattr(maxp, "maxInstructionDefs", 0)
382*e1fe3e4aSElliott Hughes            )
383*e1fe3e4aSElliott Hughes            setattr(
384*e1fe3e4aSElliott Hughes                maxp,
385*e1fe3e4aSElliott Hughes                "maxStackElements",
386*e1fe3e4aSElliott Hughes                max(len(loc), getattr(maxp, "maxStackElements", 0)),
387*e1fe3e4aSElliott Hughes            )
388*e1fe3e4aSElliott Hughes
389*e1fe3e4aSElliott Hughes    if "name" in varfont:
390*e1fe3e4aSElliott Hughes        log.info("Pruning name table")
391*e1fe3e4aSElliott Hughes        exclude = {a.axisNameID for a in fvar.axes}
392*e1fe3e4aSElliott Hughes        for i in fvar.instances:
393*e1fe3e4aSElliott Hughes            exclude.add(i.subfamilyNameID)
394*e1fe3e4aSElliott Hughes            exclude.add(i.postscriptNameID)
395*e1fe3e4aSElliott Hughes        if "ltag" in varfont:
396*e1fe3e4aSElliott Hughes            # Drop the whole 'ltag' table if all its language tags are referenced by
397*e1fe3e4aSElliott Hughes            # name records to be pruned.
398*e1fe3e4aSElliott Hughes            # TODO: prune unused ltag tags and re-enumerate langIDs accordingly
399*e1fe3e4aSElliott Hughes            excludedUnicodeLangIDs = [
400*e1fe3e4aSElliott Hughes                n.langID
401*e1fe3e4aSElliott Hughes                for n in varfont["name"].names
402*e1fe3e4aSElliott Hughes                if n.nameID in exclude and n.platformID == 0 and n.langID != 0xFFFF
403*e1fe3e4aSElliott Hughes            ]
404*e1fe3e4aSElliott Hughes            if set(excludedUnicodeLangIDs) == set(range(len((varfont["ltag"].tags)))):
405*e1fe3e4aSElliott Hughes                del varfont["ltag"]
406*e1fe3e4aSElliott Hughes        varfont["name"].names[:] = [
407*e1fe3e4aSElliott Hughes            n for n in varfont["name"].names if n.nameID not in exclude
408*e1fe3e4aSElliott Hughes        ]
409*e1fe3e4aSElliott Hughes
410*e1fe3e4aSElliott Hughes    if "wght" in location and "OS/2" in varfont:
411*e1fe3e4aSElliott Hughes        varfont["OS/2"].usWeightClass = otRound(max(1, min(location["wght"], 1000)))
412*e1fe3e4aSElliott Hughes    if "wdth" in location:
413*e1fe3e4aSElliott Hughes        wdth = location["wdth"]
414*e1fe3e4aSElliott Hughes        for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()):
415*e1fe3e4aSElliott Hughes            if wdth < percent:
416*e1fe3e4aSElliott Hughes                varfont["OS/2"].usWidthClass = widthClass
417*e1fe3e4aSElliott Hughes                break
418*e1fe3e4aSElliott Hughes        else:
419*e1fe3e4aSElliott Hughes            varfont["OS/2"].usWidthClass = 9
420*e1fe3e4aSElliott Hughes    if "slnt" in location and "post" in varfont:
421*e1fe3e4aSElliott Hughes        varfont["post"].italicAngle = max(-90, min(location["slnt"], 90))
422*e1fe3e4aSElliott Hughes
423*e1fe3e4aSElliott Hughes    log.info("Removing variable tables")
424*e1fe3e4aSElliott Hughes    for tag in ("avar", "cvar", "fvar", "gvar", "HVAR", "MVAR", "VVAR", "STAT"):
425*e1fe3e4aSElliott Hughes        if tag in varfont:
426*e1fe3e4aSElliott Hughes            del varfont[tag]
427*e1fe3e4aSElliott Hughes
428*e1fe3e4aSElliott Hughes    return varfont
429*e1fe3e4aSElliott Hughes
430*e1fe3e4aSElliott Hughes
431*e1fe3e4aSElliott Hughesdef main(args=None):
432*e1fe3e4aSElliott Hughes    """Instantiate a variation font"""
433*e1fe3e4aSElliott Hughes    from fontTools import configLogger
434*e1fe3e4aSElliott Hughes    import argparse
435*e1fe3e4aSElliott Hughes
436*e1fe3e4aSElliott Hughes    parser = argparse.ArgumentParser(
437*e1fe3e4aSElliott Hughes        "fonttools varLib.mutator", description="Instantiate a variable font"
438*e1fe3e4aSElliott Hughes    )
439*e1fe3e4aSElliott Hughes    parser.add_argument("input", metavar="INPUT.ttf", help="Input variable TTF file.")
440*e1fe3e4aSElliott Hughes    parser.add_argument(
441*e1fe3e4aSElliott Hughes        "locargs",
442*e1fe3e4aSElliott Hughes        metavar="AXIS=LOC",
443*e1fe3e4aSElliott Hughes        nargs="*",
444*e1fe3e4aSElliott Hughes        help="List of space separated locations. A location consist in "
445*e1fe3e4aSElliott Hughes        "the name of a variation axis, followed by '=' and a number. E.g.: "
446*e1fe3e4aSElliott Hughes        " wght=700 wdth=80. The default is the location of the base master.",
447*e1fe3e4aSElliott Hughes    )
448*e1fe3e4aSElliott Hughes    parser.add_argument(
449*e1fe3e4aSElliott Hughes        "-o",
450*e1fe3e4aSElliott Hughes        "--output",
451*e1fe3e4aSElliott Hughes        metavar="OUTPUT.ttf",
452*e1fe3e4aSElliott Hughes        default=None,
453*e1fe3e4aSElliott Hughes        help="Output instance TTF file (default: INPUT-instance.ttf).",
454*e1fe3e4aSElliott Hughes    )
455*e1fe3e4aSElliott Hughes    parser.add_argument(
456*e1fe3e4aSElliott Hughes        "--no-recalc-timestamp",
457*e1fe3e4aSElliott Hughes        dest="recalc_timestamp",
458*e1fe3e4aSElliott Hughes        action="store_false",
459*e1fe3e4aSElliott Hughes        help="Don't set the output font's timestamp to the current time.",
460*e1fe3e4aSElliott Hughes    )
461*e1fe3e4aSElliott Hughes    logging_group = parser.add_mutually_exclusive_group(required=False)
462*e1fe3e4aSElliott Hughes    logging_group.add_argument(
463*e1fe3e4aSElliott Hughes        "-v", "--verbose", action="store_true", help="Run more verbosely."
464*e1fe3e4aSElliott Hughes    )
465*e1fe3e4aSElliott Hughes    logging_group.add_argument(
466*e1fe3e4aSElliott Hughes        "-q", "--quiet", action="store_true", help="Turn verbosity off."
467*e1fe3e4aSElliott Hughes    )
468*e1fe3e4aSElliott Hughes    parser.add_argument(
469*e1fe3e4aSElliott Hughes        "--no-overlap",
470*e1fe3e4aSElliott Hughes        dest="overlap",
471*e1fe3e4aSElliott Hughes        action="store_false",
472*e1fe3e4aSElliott Hughes        help="Don't set OVERLAP_SIMPLE/OVERLAP_COMPOUND glyf flags.",
473*e1fe3e4aSElliott Hughes    )
474*e1fe3e4aSElliott Hughes    options = parser.parse_args(args)
475*e1fe3e4aSElliott Hughes
476*e1fe3e4aSElliott Hughes    varfilename = options.input
477*e1fe3e4aSElliott Hughes    outfile = (
478*e1fe3e4aSElliott Hughes        os.path.splitext(varfilename)[0] + "-instance.ttf"
479*e1fe3e4aSElliott Hughes        if not options.output
480*e1fe3e4aSElliott Hughes        else options.output
481*e1fe3e4aSElliott Hughes    )
482*e1fe3e4aSElliott Hughes    configLogger(
483*e1fe3e4aSElliott Hughes        level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
484*e1fe3e4aSElliott Hughes    )
485*e1fe3e4aSElliott Hughes
486*e1fe3e4aSElliott Hughes    loc = {}
487*e1fe3e4aSElliott Hughes    for arg in options.locargs:
488*e1fe3e4aSElliott Hughes        try:
489*e1fe3e4aSElliott Hughes            tag, val = arg.split("=")
490*e1fe3e4aSElliott Hughes            assert len(tag) <= 4
491*e1fe3e4aSElliott Hughes            loc[tag.ljust(4)] = float(val)
492*e1fe3e4aSElliott Hughes        except (ValueError, AssertionError):
493*e1fe3e4aSElliott Hughes            parser.error("invalid location argument format: %r" % arg)
494*e1fe3e4aSElliott Hughes    log.info("Location: %s", loc)
495*e1fe3e4aSElliott Hughes
496*e1fe3e4aSElliott Hughes    log.info("Loading variable font")
497*e1fe3e4aSElliott Hughes    varfont = TTFont(varfilename, recalcTimestamp=options.recalc_timestamp)
498*e1fe3e4aSElliott Hughes
499*e1fe3e4aSElliott Hughes    instantiateVariableFont(varfont, loc, inplace=True, overlap=options.overlap)
500*e1fe3e4aSElliott Hughes
501*e1fe3e4aSElliott Hughes    log.info("Saving instance font %s", outfile)
502*e1fe3e4aSElliott Hughes    varfont.save(outfile)
503*e1fe3e4aSElliott Hughes
504*e1fe3e4aSElliott Hughes
505*e1fe3e4aSElliott Hughesif __name__ == "__main__":
506*e1fe3e4aSElliott Hughes    import sys
507*e1fe3e4aSElliott Hughes
508*e1fe3e4aSElliott Hughes    if len(sys.argv) > 1:
509*e1fe3e4aSElliott Hughes        sys.exit(main())
510*e1fe3e4aSElliott Hughes    import doctest
511*e1fe3e4aSElliott Hughes
512*e1fe3e4aSElliott Hughes    sys.exit(doctest.testmod().failed)
513