1*e1fe3e4aSElliott Hughes"""Module to build FeatureVariation tables: 2*e1fe3e4aSElliott Hugheshttps://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#featurevariations-table 3*e1fe3e4aSElliott Hughes 4*e1fe3e4aSElliott HughesNOTE: The API is experimental and subject to change. 5*e1fe3e4aSElliott Hughes""" 6*e1fe3e4aSElliott Hughes 7*e1fe3e4aSElliott Hughesfrom fontTools.misc.dictTools import hashdict 8*e1fe3e4aSElliott Hughesfrom fontTools.misc.intTools import bit_count 9*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import newTable 10*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables import otTables as ot 11*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.ttVisitor import TTVisitor 12*e1fe3e4aSElliott Hughesfrom fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable 13*e1fe3e4aSElliott Hughesfrom collections import OrderedDict 14*e1fe3e4aSElliott Hughes 15*e1fe3e4aSElliott Hughesfrom .errors import VarLibError, VarLibValidationError 16*e1fe3e4aSElliott Hughes 17*e1fe3e4aSElliott Hughes 18*e1fe3e4aSElliott Hughesdef addFeatureVariations(font, conditionalSubstitutions, featureTag="rvrn"): 19*e1fe3e4aSElliott Hughes """Add conditional substitutions to a Variable Font. 20*e1fe3e4aSElliott Hughes 21*e1fe3e4aSElliott Hughes The `conditionalSubstitutions` argument is a list of (Region, Substitutions) 22*e1fe3e4aSElliott Hughes tuples. 23*e1fe3e4aSElliott Hughes 24*e1fe3e4aSElliott Hughes A Region is a list of Boxes. A Box is a dict mapping axisTags to 25*e1fe3e4aSElliott Hughes (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are 26*e1fe3e4aSElliott Hughes interpretted as extending to end of axis in each direction. A Box represents 27*e1fe3e4aSElliott Hughes an orthogonal 'rectangular' subset of an N-dimensional design space. 28*e1fe3e4aSElliott Hughes A Region represents a more complex subset of an N-dimensional design space, 29*e1fe3e4aSElliott Hughes ie. the union of all the Boxes in the Region. 30*e1fe3e4aSElliott Hughes For efficiency, Boxes within a Region should ideally not overlap, but 31*e1fe3e4aSElliott Hughes functionality is not compromised if they do. 32*e1fe3e4aSElliott Hughes 33*e1fe3e4aSElliott Hughes The minimum and maximum values are expressed in normalized coordinates. 34*e1fe3e4aSElliott Hughes 35*e1fe3e4aSElliott Hughes A Substitution is a dict mapping source glyph names to substitute glyph names. 36*e1fe3e4aSElliott Hughes 37*e1fe3e4aSElliott Hughes Example: 38*e1fe3e4aSElliott Hughes 39*e1fe3e4aSElliott Hughes # >>> f = TTFont(srcPath) 40*e1fe3e4aSElliott Hughes # >>> condSubst = [ 41*e1fe3e4aSElliott Hughes # ... # A list of (Region, Substitution) tuples. 42*e1fe3e4aSElliott Hughes # ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}), 43*e1fe3e4aSElliott Hughes # ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}), 44*e1fe3e4aSElliott Hughes # ... ] 45*e1fe3e4aSElliott Hughes # >>> addFeatureVariations(f, condSubst) 46*e1fe3e4aSElliott Hughes # >>> f.save(dstPath) 47*e1fe3e4aSElliott Hughes 48*e1fe3e4aSElliott Hughes The `featureTag` parameter takes either a str or a iterable of str (the single str 49*e1fe3e4aSElliott Hughes is kept for backwards compatibility), and defines which feature(s) will be 50*e1fe3e4aSElliott Hughes associated with the feature variations. 51*e1fe3e4aSElliott Hughes Note, if this is "rvrn", then the substitution lookup will be inserted at the 52*e1fe3e4aSElliott Hughes beginning of the lookup list so that it is processed before others, otherwise 53*e1fe3e4aSElliott Hughes for any other feature tags it will be appended last. 54*e1fe3e4aSElliott Hughes """ 55*e1fe3e4aSElliott Hughes 56*e1fe3e4aSElliott Hughes # process first when "rvrn" is the only listed tag 57*e1fe3e4aSElliott Hughes featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag) 58*e1fe3e4aSElliott Hughes processLast = "rvrn" not in featureTags or len(featureTags) > 1 59*e1fe3e4aSElliott Hughes 60*e1fe3e4aSElliott Hughes _checkSubstitutionGlyphsExist( 61*e1fe3e4aSElliott Hughes glyphNames=set(font.getGlyphOrder()), 62*e1fe3e4aSElliott Hughes substitutions=conditionalSubstitutions, 63*e1fe3e4aSElliott Hughes ) 64*e1fe3e4aSElliott Hughes 65*e1fe3e4aSElliott Hughes substitutions = overlayFeatureVariations(conditionalSubstitutions) 66*e1fe3e4aSElliott Hughes 67*e1fe3e4aSElliott Hughes # turn substitution dicts into tuples of tuples, so they are hashable 68*e1fe3e4aSElliott Hughes conditionalSubstitutions, allSubstitutions = makeSubstitutionsHashable( 69*e1fe3e4aSElliott Hughes substitutions 70*e1fe3e4aSElliott Hughes ) 71*e1fe3e4aSElliott Hughes if "GSUB" not in font: 72*e1fe3e4aSElliott Hughes font["GSUB"] = buildGSUB() 73*e1fe3e4aSElliott Hughes else: 74*e1fe3e4aSElliott Hughes existingTags = _existingVariableFeatures(font["GSUB"].table).intersection( 75*e1fe3e4aSElliott Hughes featureTags 76*e1fe3e4aSElliott Hughes ) 77*e1fe3e4aSElliott Hughes if existingTags: 78*e1fe3e4aSElliott Hughes raise VarLibError( 79*e1fe3e4aSElliott Hughes f"FeatureVariations already exist for feature tag(s): {existingTags}" 80*e1fe3e4aSElliott Hughes ) 81*e1fe3e4aSElliott Hughes 82*e1fe3e4aSElliott Hughes # setup lookups 83*e1fe3e4aSElliott Hughes lookupMap = buildSubstitutionLookups( 84*e1fe3e4aSElliott Hughes font["GSUB"].table, allSubstitutions, processLast 85*e1fe3e4aSElliott Hughes ) 86*e1fe3e4aSElliott Hughes 87*e1fe3e4aSElliott Hughes # addFeatureVariationsRaw takes a list of 88*e1fe3e4aSElliott Hughes # ( {condition}, [ lookup indices ] ) 89*e1fe3e4aSElliott Hughes # so rearrange our lookups to match 90*e1fe3e4aSElliott Hughes conditionsAndLookups = [] 91*e1fe3e4aSElliott Hughes for conditionSet, substitutions in conditionalSubstitutions: 92*e1fe3e4aSElliott Hughes conditionsAndLookups.append( 93*e1fe3e4aSElliott Hughes (conditionSet, [lookupMap[s] for s in substitutions]) 94*e1fe3e4aSElliott Hughes ) 95*e1fe3e4aSElliott Hughes 96*e1fe3e4aSElliott Hughes addFeatureVariationsRaw(font, font["GSUB"].table, conditionsAndLookups, featureTags) 97*e1fe3e4aSElliott Hughes 98*e1fe3e4aSElliott Hughes 99*e1fe3e4aSElliott Hughesdef _existingVariableFeatures(table): 100*e1fe3e4aSElliott Hughes existingFeatureVarsTags = set() 101*e1fe3e4aSElliott Hughes if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None: 102*e1fe3e4aSElliott Hughes features = table.FeatureList.FeatureRecord 103*e1fe3e4aSElliott Hughes for fvr in table.FeatureVariations.FeatureVariationRecord: 104*e1fe3e4aSElliott Hughes for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord: 105*e1fe3e4aSElliott Hughes existingFeatureVarsTags.add(features[ftsr.FeatureIndex].FeatureTag) 106*e1fe3e4aSElliott Hughes return existingFeatureVarsTags 107*e1fe3e4aSElliott Hughes 108*e1fe3e4aSElliott Hughes 109*e1fe3e4aSElliott Hughesdef _checkSubstitutionGlyphsExist(glyphNames, substitutions): 110*e1fe3e4aSElliott Hughes referencedGlyphNames = set() 111*e1fe3e4aSElliott Hughes for _, substitution in substitutions: 112*e1fe3e4aSElliott Hughes referencedGlyphNames |= substitution.keys() 113*e1fe3e4aSElliott Hughes referencedGlyphNames |= set(substitution.values()) 114*e1fe3e4aSElliott Hughes missing = referencedGlyphNames - glyphNames 115*e1fe3e4aSElliott Hughes if missing: 116*e1fe3e4aSElliott Hughes raise VarLibValidationError( 117*e1fe3e4aSElliott Hughes "Missing glyphs are referenced in conditional substitution rules:" 118*e1fe3e4aSElliott Hughes f" {', '.join(missing)}" 119*e1fe3e4aSElliott Hughes ) 120*e1fe3e4aSElliott Hughes 121*e1fe3e4aSElliott Hughes 122*e1fe3e4aSElliott Hughesdef overlayFeatureVariations(conditionalSubstitutions): 123*e1fe3e4aSElliott Hughes """Compute overlaps between all conditional substitutions. 124*e1fe3e4aSElliott Hughes 125*e1fe3e4aSElliott Hughes The `conditionalSubstitutions` argument is a list of (Region, Substitutions) 126*e1fe3e4aSElliott Hughes tuples. 127*e1fe3e4aSElliott Hughes 128*e1fe3e4aSElliott Hughes A Region is a list of Boxes. A Box is a dict mapping axisTags to 129*e1fe3e4aSElliott Hughes (minValue, maxValue) tuples. Irrelevant axes may be omitted and they are 130*e1fe3e4aSElliott Hughes interpretted as extending to end of axis in each direction. A Box represents 131*e1fe3e4aSElliott Hughes an orthogonal 'rectangular' subset of an N-dimensional design space. 132*e1fe3e4aSElliott Hughes A Region represents a more complex subset of an N-dimensional design space, 133*e1fe3e4aSElliott Hughes ie. the union of all the Boxes in the Region. 134*e1fe3e4aSElliott Hughes For efficiency, Boxes within a Region should ideally not overlap, but 135*e1fe3e4aSElliott Hughes functionality is not compromised if they do. 136*e1fe3e4aSElliott Hughes 137*e1fe3e4aSElliott Hughes The minimum and maximum values are expressed in normalized coordinates. 138*e1fe3e4aSElliott Hughes 139*e1fe3e4aSElliott Hughes A Substitution is a dict mapping source glyph names to substitute glyph names. 140*e1fe3e4aSElliott Hughes 141*e1fe3e4aSElliott Hughes Returns data is in similar but different format. Overlaps of distinct 142*e1fe3e4aSElliott Hughes substitution Boxes (*not* Regions) are explicitly listed as distinct rules, 143*e1fe3e4aSElliott Hughes and rules with the same Box merged. The more specific rules appear earlier 144*e1fe3e4aSElliott Hughes in the resulting list. Moreover, instead of just a dictionary of substitutions, 145*e1fe3e4aSElliott Hughes a list of dictionaries is returned for substitutions corresponding to each 146*e1fe3e4aSElliott Hughes unique space, with each dictionary being identical to one of the input 147*e1fe3e4aSElliott Hughes substitution dictionaries. These dictionaries are not merged to allow data 148*e1fe3e4aSElliott Hughes sharing when they are converted into font tables. 149*e1fe3e4aSElliott Hughes 150*e1fe3e4aSElliott Hughes Example:: 151*e1fe3e4aSElliott Hughes 152*e1fe3e4aSElliott Hughes >>> condSubst = [ 153*e1fe3e4aSElliott Hughes ... # A list of (Region, Substitution) tuples. 154*e1fe3e4aSElliott Hughes ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}), 155*e1fe3e4aSElliott Hughes ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}), 156*e1fe3e4aSElliott Hughes ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}), 157*e1fe3e4aSElliott Hughes ... ([{"wght": (0.5, 1.0), "wdth": (-1, 1.0)}], {"dollar": "dollar.rvrn"}), 158*e1fe3e4aSElliott Hughes ... ] 159*e1fe3e4aSElliott Hughes >>> from pprint import pprint 160*e1fe3e4aSElliott Hughes >>> pprint(overlayFeatureVariations(condSubst)) 161*e1fe3e4aSElliott Hughes [({'wdth': (0.5, 1.0), 'wght': (0.5, 1.0)}, 162*e1fe3e4aSElliott Hughes [{'dollar': 'dollar.rvrn'}, {'cent': 'cent.rvrn'}]), 163*e1fe3e4aSElliott Hughes ({'wdth': (0.5, 1.0)}, [{'cent': 'cent.rvrn'}]), 164*e1fe3e4aSElliott Hughes ({'wght': (0.5, 1.0)}, [{'dollar': 'dollar.rvrn'}])] 165*e1fe3e4aSElliott Hughes 166*e1fe3e4aSElliott Hughes """ 167*e1fe3e4aSElliott Hughes 168*e1fe3e4aSElliott Hughes # Merge same-substitutions rules, as this creates fewer number oflookups. 169*e1fe3e4aSElliott Hughes merged = OrderedDict() 170*e1fe3e4aSElliott Hughes for value, key in conditionalSubstitutions: 171*e1fe3e4aSElliott Hughes key = hashdict(key) 172*e1fe3e4aSElliott Hughes if key in merged: 173*e1fe3e4aSElliott Hughes merged[key].extend(value) 174*e1fe3e4aSElliott Hughes else: 175*e1fe3e4aSElliott Hughes merged[key] = value 176*e1fe3e4aSElliott Hughes conditionalSubstitutions = [(v, dict(k)) for k, v in merged.items()] 177*e1fe3e4aSElliott Hughes del merged 178*e1fe3e4aSElliott Hughes 179*e1fe3e4aSElliott Hughes # Merge same-region rules, as this is cheaper. 180*e1fe3e4aSElliott Hughes # Also convert boxes to hashdict() 181*e1fe3e4aSElliott Hughes # 182*e1fe3e4aSElliott Hughes # Reversing is such that earlier entries win in case of conflicting substitution 183*e1fe3e4aSElliott Hughes # rules for the same region. 184*e1fe3e4aSElliott Hughes merged = OrderedDict() 185*e1fe3e4aSElliott Hughes for key, value in reversed(conditionalSubstitutions): 186*e1fe3e4aSElliott Hughes key = tuple( 187*e1fe3e4aSElliott Hughes sorted( 188*e1fe3e4aSElliott Hughes (hashdict(cleanupBox(k)) for k in key), 189*e1fe3e4aSElliott Hughes key=lambda d: tuple(sorted(d.items())), 190*e1fe3e4aSElliott Hughes ) 191*e1fe3e4aSElliott Hughes ) 192*e1fe3e4aSElliott Hughes if key in merged: 193*e1fe3e4aSElliott Hughes merged[key].update(value) 194*e1fe3e4aSElliott Hughes else: 195*e1fe3e4aSElliott Hughes merged[key] = dict(value) 196*e1fe3e4aSElliott Hughes conditionalSubstitutions = list(reversed(merged.items())) 197*e1fe3e4aSElliott Hughes del merged 198*e1fe3e4aSElliott Hughes 199*e1fe3e4aSElliott Hughes # Overlay 200*e1fe3e4aSElliott Hughes # 201*e1fe3e4aSElliott Hughes # Rank is the bit-set of the index of all contributing layers. 202*e1fe3e4aSElliott Hughes initMapInit = ((hashdict(), 0),) # Initializer representing the entire space 203*e1fe3e4aSElliott Hughes boxMap = OrderedDict(initMapInit) # Map from Box to Rank 204*e1fe3e4aSElliott Hughes for i, (currRegion, _) in enumerate(conditionalSubstitutions): 205*e1fe3e4aSElliott Hughes newMap = OrderedDict(initMapInit) 206*e1fe3e4aSElliott Hughes currRank = 1 << i 207*e1fe3e4aSElliott Hughes for box, rank in boxMap.items(): 208*e1fe3e4aSElliott Hughes for currBox in currRegion: 209*e1fe3e4aSElliott Hughes intersection, remainder = overlayBox(currBox, box) 210*e1fe3e4aSElliott Hughes if intersection is not None: 211*e1fe3e4aSElliott Hughes intersection = hashdict(intersection) 212*e1fe3e4aSElliott Hughes newMap[intersection] = newMap.get(intersection, 0) | rank | currRank 213*e1fe3e4aSElliott Hughes if remainder is not None: 214*e1fe3e4aSElliott Hughes remainder = hashdict(remainder) 215*e1fe3e4aSElliott Hughes newMap[remainder] = newMap.get(remainder, 0) | rank 216*e1fe3e4aSElliott Hughes boxMap = newMap 217*e1fe3e4aSElliott Hughes 218*e1fe3e4aSElliott Hughes # Generate output 219*e1fe3e4aSElliott Hughes items = [] 220*e1fe3e4aSElliott Hughes for box, rank in sorted( 221*e1fe3e4aSElliott Hughes boxMap.items(), key=(lambda BoxAndRank: -bit_count(BoxAndRank[1])) 222*e1fe3e4aSElliott Hughes ): 223*e1fe3e4aSElliott Hughes # Skip any box that doesn't have any substitution. 224*e1fe3e4aSElliott Hughes if rank == 0: 225*e1fe3e4aSElliott Hughes continue 226*e1fe3e4aSElliott Hughes substsList = [] 227*e1fe3e4aSElliott Hughes i = 0 228*e1fe3e4aSElliott Hughes while rank: 229*e1fe3e4aSElliott Hughes if rank & 1: 230*e1fe3e4aSElliott Hughes substsList.append(conditionalSubstitutions[i][1]) 231*e1fe3e4aSElliott Hughes rank >>= 1 232*e1fe3e4aSElliott Hughes i += 1 233*e1fe3e4aSElliott Hughes items.append((dict(box), substsList)) 234*e1fe3e4aSElliott Hughes return items 235*e1fe3e4aSElliott Hughes 236*e1fe3e4aSElliott Hughes 237*e1fe3e4aSElliott Hughes# 238*e1fe3e4aSElliott Hughes# Terminology: 239*e1fe3e4aSElliott Hughes# 240*e1fe3e4aSElliott Hughes# A 'Box' is a dict representing an orthogonal "rectangular" bit of N-dimensional space. 241*e1fe3e4aSElliott Hughes# The keys in the dict are axis tags, the values are (minValue, maxValue) tuples. 242*e1fe3e4aSElliott Hughes# Missing dimensions (keys) are substituted by the default min and max values 243*e1fe3e4aSElliott Hughes# from the corresponding axes. 244*e1fe3e4aSElliott Hughes# 245*e1fe3e4aSElliott Hughes 246*e1fe3e4aSElliott Hughes 247*e1fe3e4aSElliott Hughesdef overlayBox(top, bot): 248*e1fe3e4aSElliott Hughes """Overlays ``top`` box on top of ``bot`` box. 249*e1fe3e4aSElliott Hughes 250*e1fe3e4aSElliott Hughes Returns two items: 251*e1fe3e4aSElliott Hughes 252*e1fe3e4aSElliott Hughes * Box for intersection of ``top`` and ``bot``, or None if they don't intersect. 253*e1fe3e4aSElliott Hughes * Box for remainder of ``bot``. Remainder box might not be exact (since the 254*e1fe3e4aSElliott Hughes remainder might not be a simple box), but is inclusive of the exact 255*e1fe3e4aSElliott Hughes remainder. 256*e1fe3e4aSElliott Hughes """ 257*e1fe3e4aSElliott Hughes 258*e1fe3e4aSElliott Hughes # Intersection 259*e1fe3e4aSElliott Hughes intersection = {} 260*e1fe3e4aSElliott Hughes intersection.update(top) 261*e1fe3e4aSElliott Hughes intersection.update(bot) 262*e1fe3e4aSElliott Hughes for axisTag in set(top) & set(bot): 263*e1fe3e4aSElliott Hughes min1, max1 = top[axisTag] 264*e1fe3e4aSElliott Hughes min2, max2 = bot[axisTag] 265*e1fe3e4aSElliott Hughes minimum = max(min1, min2) 266*e1fe3e4aSElliott Hughes maximum = min(max1, max2) 267*e1fe3e4aSElliott Hughes if not minimum < maximum: 268*e1fe3e4aSElliott Hughes return None, bot # Do not intersect 269*e1fe3e4aSElliott Hughes intersection[axisTag] = minimum, maximum 270*e1fe3e4aSElliott Hughes 271*e1fe3e4aSElliott Hughes # Remainder 272*e1fe3e4aSElliott Hughes # 273*e1fe3e4aSElliott Hughes # Remainder is empty if bot's each axis range lies within that of intersection. 274*e1fe3e4aSElliott Hughes # 275*e1fe3e4aSElliott Hughes # Remainder is shrank if bot's each, except for exactly one, axis range lies 276*e1fe3e4aSElliott Hughes # within that of intersection, and that one axis, it extrudes out of the 277*e1fe3e4aSElliott Hughes # intersection only on one side. 278*e1fe3e4aSElliott Hughes # 279*e1fe3e4aSElliott Hughes # Bot is returned in full as remainder otherwise, as true remainder is not 280*e1fe3e4aSElliott Hughes # representable as a single box. 281*e1fe3e4aSElliott Hughes 282*e1fe3e4aSElliott Hughes remainder = dict(bot) 283*e1fe3e4aSElliott Hughes extruding = False 284*e1fe3e4aSElliott Hughes fullyInside = True 285*e1fe3e4aSElliott Hughes for axisTag in top: 286*e1fe3e4aSElliott Hughes if axisTag in bot: 287*e1fe3e4aSElliott Hughes continue 288*e1fe3e4aSElliott Hughes extruding = True 289*e1fe3e4aSElliott Hughes fullyInside = False 290*e1fe3e4aSElliott Hughes break 291*e1fe3e4aSElliott Hughes for axisTag in bot: 292*e1fe3e4aSElliott Hughes if axisTag not in top: 293*e1fe3e4aSElliott Hughes continue # Axis range lies fully within 294*e1fe3e4aSElliott Hughes min1, max1 = intersection[axisTag] 295*e1fe3e4aSElliott Hughes min2, max2 = bot[axisTag] 296*e1fe3e4aSElliott Hughes if min1 <= min2 and max2 <= max1: 297*e1fe3e4aSElliott Hughes continue # Axis range lies fully within 298*e1fe3e4aSElliott Hughes 299*e1fe3e4aSElliott Hughes # Bot's range doesn't fully lie within that of top's for this axis. 300*e1fe3e4aSElliott Hughes # We know they intersect, so it cannot lie fully without either; so they 301*e1fe3e4aSElliott Hughes # overlap. 302*e1fe3e4aSElliott Hughes 303*e1fe3e4aSElliott Hughes # If we have had an overlapping axis before, remainder is not 304*e1fe3e4aSElliott Hughes # representable as a box, so return full bottom and go home. 305*e1fe3e4aSElliott Hughes if extruding: 306*e1fe3e4aSElliott Hughes return intersection, bot 307*e1fe3e4aSElliott Hughes extruding = True 308*e1fe3e4aSElliott Hughes fullyInside = False 309*e1fe3e4aSElliott Hughes 310*e1fe3e4aSElliott Hughes # Otherwise, cut remainder on this axis and continue. 311*e1fe3e4aSElliott Hughes if min1 <= min2: 312*e1fe3e4aSElliott Hughes # Right side survives. 313*e1fe3e4aSElliott Hughes minimum = max(max1, min2) 314*e1fe3e4aSElliott Hughes maximum = max2 315*e1fe3e4aSElliott Hughes elif max2 <= max1: 316*e1fe3e4aSElliott Hughes # Left side survives. 317*e1fe3e4aSElliott Hughes minimum = min2 318*e1fe3e4aSElliott Hughes maximum = min(min1, max2) 319*e1fe3e4aSElliott Hughes else: 320*e1fe3e4aSElliott Hughes # Remainder leaks out from both sides. Can't cut either. 321*e1fe3e4aSElliott Hughes return intersection, bot 322*e1fe3e4aSElliott Hughes 323*e1fe3e4aSElliott Hughes remainder[axisTag] = minimum, maximum 324*e1fe3e4aSElliott Hughes 325*e1fe3e4aSElliott Hughes if fullyInside: 326*e1fe3e4aSElliott Hughes # bot is fully within intersection. Remainder is empty. 327*e1fe3e4aSElliott Hughes return intersection, None 328*e1fe3e4aSElliott Hughes 329*e1fe3e4aSElliott Hughes return intersection, remainder 330*e1fe3e4aSElliott Hughes 331*e1fe3e4aSElliott Hughes 332*e1fe3e4aSElliott Hughesdef cleanupBox(box): 333*e1fe3e4aSElliott Hughes """Return a sparse copy of `box`, without redundant (default) values. 334*e1fe3e4aSElliott Hughes 335*e1fe3e4aSElliott Hughes >>> cleanupBox({}) 336*e1fe3e4aSElliott Hughes {} 337*e1fe3e4aSElliott Hughes >>> cleanupBox({'wdth': (0.0, 1.0)}) 338*e1fe3e4aSElliott Hughes {'wdth': (0.0, 1.0)} 339*e1fe3e4aSElliott Hughes >>> cleanupBox({'wdth': (-1.0, 1.0)}) 340*e1fe3e4aSElliott Hughes {} 341*e1fe3e4aSElliott Hughes 342*e1fe3e4aSElliott Hughes """ 343*e1fe3e4aSElliott Hughes return {tag: limit for tag, limit in box.items() if limit != (-1.0, 1.0)} 344*e1fe3e4aSElliott Hughes 345*e1fe3e4aSElliott Hughes 346*e1fe3e4aSElliott Hughes# 347*e1fe3e4aSElliott Hughes# Low level implementation 348*e1fe3e4aSElliott Hughes# 349*e1fe3e4aSElliott Hughes 350*e1fe3e4aSElliott Hughes 351*e1fe3e4aSElliott Hughesdef addFeatureVariationsRaw(font, table, conditionalSubstitutions, featureTag="rvrn"): 352*e1fe3e4aSElliott Hughes """Low level implementation of addFeatureVariations that directly 353*e1fe3e4aSElliott Hughes models the possibilities of the FeatureVariations table.""" 354*e1fe3e4aSElliott Hughes 355*e1fe3e4aSElliott Hughes featureTags = [featureTag] if isinstance(featureTag, str) else sorted(featureTag) 356*e1fe3e4aSElliott Hughes processLast = "rvrn" not in featureTags or len(featureTags) > 1 357*e1fe3e4aSElliott Hughes 358*e1fe3e4aSElliott Hughes # 359*e1fe3e4aSElliott Hughes # if a <featureTag> feature is not present: 360*e1fe3e4aSElliott Hughes # make empty <featureTag> feature 361*e1fe3e4aSElliott Hughes # sort features, get <featureTag> feature index 362*e1fe3e4aSElliott Hughes # add <featureTag> feature to all scripts 363*e1fe3e4aSElliott Hughes # if a <featureTag> feature is present: 364*e1fe3e4aSElliott Hughes # reuse <featureTag> feature index 365*e1fe3e4aSElliott Hughes # make lookups 366*e1fe3e4aSElliott Hughes # add feature variations 367*e1fe3e4aSElliott Hughes # 368*e1fe3e4aSElliott Hughes if table.Version < 0x00010001: 369*e1fe3e4aSElliott Hughes table.Version = 0x00010001 # allow table.FeatureVariations 370*e1fe3e4aSElliott Hughes 371*e1fe3e4aSElliott Hughes varFeatureIndices = set() 372*e1fe3e4aSElliott Hughes 373*e1fe3e4aSElliott Hughes existingTags = { 374*e1fe3e4aSElliott Hughes feature.FeatureTag 375*e1fe3e4aSElliott Hughes for feature in table.FeatureList.FeatureRecord 376*e1fe3e4aSElliott Hughes if feature.FeatureTag in featureTags 377*e1fe3e4aSElliott Hughes } 378*e1fe3e4aSElliott Hughes 379*e1fe3e4aSElliott Hughes newTags = set(featureTags) - existingTags 380*e1fe3e4aSElliott Hughes if newTags: 381*e1fe3e4aSElliott Hughes varFeatures = [] 382*e1fe3e4aSElliott Hughes for featureTag in sorted(newTags): 383*e1fe3e4aSElliott Hughes varFeature = buildFeatureRecord(featureTag, []) 384*e1fe3e4aSElliott Hughes table.FeatureList.FeatureRecord.append(varFeature) 385*e1fe3e4aSElliott Hughes varFeatures.append(varFeature) 386*e1fe3e4aSElliott Hughes table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord) 387*e1fe3e4aSElliott Hughes 388*e1fe3e4aSElliott Hughes sortFeatureList(table) 389*e1fe3e4aSElliott Hughes 390*e1fe3e4aSElliott Hughes for varFeature in varFeatures: 391*e1fe3e4aSElliott Hughes varFeatureIndex = table.FeatureList.FeatureRecord.index(varFeature) 392*e1fe3e4aSElliott Hughes 393*e1fe3e4aSElliott Hughes for scriptRecord in table.ScriptList.ScriptRecord: 394*e1fe3e4aSElliott Hughes if scriptRecord.Script.DefaultLangSys is None: 395*e1fe3e4aSElliott Hughes raise VarLibError( 396*e1fe3e4aSElliott Hughes "Feature variations require that the script " 397*e1fe3e4aSElliott Hughes f"'{scriptRecord.ScriptTag}' defines a default language system." 398*e1fe3e4aSElliott Hughes ) 399*e1fe3e4aSElliott Hughes langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord] 400*e1fe3e4aSElliott Hughes for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems: 401*e1fe3e4aSElliott Hughes langSys.FeatureIndex.append(varFeatureIndex) 402*e1fe3e4aSElliott Hughes langSys.FeatureCount = len(langSys.FeatureIndex) 403*e1fe3e4aSElliott Hughes varFeatureIndices.add(varFeatureIndex) 404*e1fe3e4aSElliott Hughes 405*e1fe3e4aSElliott Hughes if existingTags: 406*e1fe3e4aSElliott Hughes # indices may have changed if we inserted new features and sorted feature list 407*e1fe3e4aSElliott Hughes # so we must do this after the above 408*e1fe3e4aSElliott Hughes varFeatureIndices.update( 409*e1fe3e4aSElliott Hughes index 410*e1fe3e4aSElliott Hughes for index, feature in enumerate(table.FeatureList.FeatureRecord) 411*e1fe3e4aSElliott Hughes if feature.FeatureTag in existingTags 412*e1fe3e4aSElliott Hughes ) 413*e1fe3e4aSElliott Hughes 414*e1fe3e4aSElliott Hughes axisIndices = { 415*e1fe3e4aSElliott Hughes axis.axisTag: axisIndex for axisIndex, axis in enumerate(font["fvar"].axes) 416*e1fe3e4aSElliott Hughes } 417*e1fe3e4aSElliott Hughes 418*e1fe3e4aSElliott Hughes hasFeatureVariations = ( 419*e1fe3e4aSElliott Hughes hasattr(table, "FeatureVariations") and table.FeatureVariations is not None 420*e1fe3e4aSElliott Hughes ) 421*e1fe3e4aSElliott Hughes 422*e1fe3e4aSElliott Hughes featureVariationRecords = [] 423*e1fe3e4aSElliott Hughes for conditionSet, lookupIndices in conditionalSubstitutions: 424*e1fe3e4aSElliott Hughes conditionTable = [] 425*e1fe3e4aSElliott Hughes for axisTag, (minValue, maxValue) in sorted(conditionSet.items()): 426*e1fe3e4aSElliott Hughes if minValue > maxValue: 427*e1fe3e4aSElliott Hughes raise VarLibValidationError( 428*e1fe3e4aSElliott Hughes "A condition set has a minimum value above the maximum value." 429*e1fe3e4aSElliott Hughes ) 430*e1fe3e4aSElliott Hughes ct = buildConditionTable(axisIndices[axisTag], minValue, maxValue) 431*e1fe3e4aSElliott Hughes conditionTable.append(ct) 432*e1fe3e4aSElliott Hughes records = [] 433*e1fe3e4aSElliott Hughes for varFeatureIndex in sorted(varFeatureIndices): 434*e1fe3e4aSElliott Hughes existingLookupIndices = table.FeatureList.FeatureRecord[ 435*e1fe3e4aSElliott Hughes varFeatureIndex 436*e1fe3e4aSElliott Hughes ].Feature.LookupListIndex 437*e1fe3e4aSElliott Hughes combinedLookupIndices = ( 438*e1fe3e4aSElliott Hughes existingLookupIndices + lookupIndices 439*e1fe3e4aSElliott Hughes if processLast 440*e1fe3e4aSElliott Hughes else lookupIndices + existingLookupIndices 441*e1fe3e4aSElliott Hughes ) 442*e1fe3e4aSElliott Hughes 443*e1fe3e4aSElliott Hughes records.append( 444*e1fe3e4aSElliott Hughes buildFeatureTableSubstitutionRecord( 445*e1fe3e4aSElliott Hughes varFeatureIndex, combinedLookupIndices 446*e1fe3e4aSElliott Hughes ) 447*e1fe3e4aSElliott Hughes ) 448*e1fe3e4aSElliott Hughes if hasFeatureVariations and ( 449*e1fe3e4aSElliott Hughes fvr := findFeatureVariationRecord(table.FeatureVariations, conditionTable) 450*e1fe3e4aSElliott Hughes ): 451*e1fe3e4aSElliott Hughes fvr.FeatureTableSubstitution.SubstitutionRecord.extend(records) 452*e1fe3e4aSElliott Hughes fvr.FeatureTableSubstitution.SubstitutionCount = len( 453*e1fe3e4aSElliott Hughes fvr.FeatureTableSubstitution.SubstitutionRecord 454*e1fe3e4aSElliott Hughes ) 455*e1fe3e4aSElliott Hughes else: 456*e1fe3e4aSElliott Hughes featureVariationRecords.append( 457*e1fe3e4aSElliott Hughes buildFeatureVariationRecord(conditionTable, records) 458*e1fe3e4aSElliott Hughes ) 459*e1fe3e4aSElliott Hughes 460*e1fe3e4aSElliott Hughes if hasFeatureVariations: 461*e1fe3e4aSElliott Hughes if table.FeatureVariations.Version != 0x00010000: 462*e1fe3e4aSElliott Hughes raise VarLibError( 463*e1fe3e4aSElliott Hughes "Unsupported FeatureVariations table version: " 464*e1fe3e4aSElliott Hughes f"0x{table.FeatureVariations.Version:08x} (expected 0x00010000)." 465*e1fe3e4aSElliott Hughes ) 466*e1fe3e4aSElliott Hughes table.FeatureVariations.FeatureVariationRecord.extend(featureVariationRecords) 467*e1fe3e4aSElliott Hughes table.FeatureVariations.FeatureVariationCount = len( 468*e1fe3e4aSElliott Hughes table.FeatureVariations.FeatureVariationRecord 469*e1fe3e4aSElliott Hughes ) 470*e1fe3e4aSElliott Hughes else: 471*e1fe3e4aSElliott Hughes table.FeatureVariations = buildFeatureVariations(featureVariationRecords) 472*e1fe3e4aSElliott Hughes 473*e1fe3e4aSElliott Hughes 474*e1fe3e4aSElliott Hughes# 475*e1fe3e4aSElliott Hughes# Building GSUB/FeatureVariations internals 476*e1fe3e4aSElliott Hughes# 477*e1fe3e4aSElliott Hughes 478*e1fe3e4aSElliott Hughes 479*e1fe3e4aSElliott Hughesdef buildGSUB(): 480*e1fe3e4aSElliott Hughes """Build a GSUB table from scratch.""" 481*e1fe3e4aSElliott Hughes fontTable = newTable("GSUB") 482*e1fe3e4aSElliott Hughes gsub = fontTable.table = ot.GSUB() 483*e1fe3e4aSElliott Hughes gsub.Version = 0x00010001 # allow gsub.FeatureVariations 484*e1fe3e4aSElliott Hughes 485*e1fe3e4aSElliott Hughes gsub.ScriptList = ot.ScriptList() 486*e1fe3e4aSElliott Hughes gsub.ScriptList.ScriptRecord = [] 487*e1fe3e4aSElliott Hughes gsub.FeatureList = ot.FeatureList() 488*e1fe3e4aSElliott Hughes gsub.FeatureList.FeatureRecord = [] 489*e1fe3e4aSElliott Hughes gsub.LookupList = ot.LookupList() 490*e1fe3e4aSElliott Hughes gsub.LookupList.Lookup = [] 491*e1fe3e4aSElliott Hughes 492*e1fe3e4aSElliott Hughes srec = ot.ScriptRecord() 493*e1fe3e4aSElliott Hughes srec.ScriptTag = "DFLT" 494*e1fe3e4aSElliott Hughes srec.Script = ot.Script() 495*e1fe3e4aSElliott Hughes srec.Script.DefaultLangSys = None 496*e1fe3e4aSElliott Hughes srec.Script.LangSysRecord = [] 497*e1fe3e4aSElliott Hughes srec.Script.LangSysCount = 0 498*e1fe3e4aSElliott Hughes 499*e1fe3e4aSElliott Hughes langrec = ot.LangSysRecord() 500*e1fe3e4aSElliott Hughes langrec.LangSys = ot.LangSys() 501*e1fe3e4aSElliott Hughes langrec.LangSys.ReqFeatureIndex = 0xFFFF 502*e1fe3e4aSElliott Hughes langrec.LangSys.FeatureIndex = [] 503*e1fe3e4aSElliott Hughes srec.Script.DefaultLangSys = langrec.LangSys 504*e1fe3e4aSElliott Hughes 505*e1fe3e4aSElliott Hughes gsub.ScriptList.ScriptRecord.append(srec) 506*e1fe3e4aSElliott Hughes gsub.ScriptList.ScriptCount = 1 507*e1fe3e4aSElliott Hughes gsub.FeatureVariations = None 508*e1fe3e4aSElliott Hughes 509*e1fe3e4aSElliott Hughes return fontTable 510*e1fe3e4aSElliott Hughes 511*e1fe3e4aSElliott Hughes 512*e1fe3e4aSElliott Hughesdef makeSubstitutionsHashable(conditionalSubstitutions): 513*e1fe3e4aSElliott Hughes """Turn all the substitution dictionaries in sorted tuples of tuples so 514*e1fe3e4aSElliott Hughes they are hashable, to detect duplicates so we don't write out redundant 515*e1fe3e4aSElliott Hughes data.""" 516*e1fe3e4aSElliott Hughes allSubstitutions = set() 517*e1fe3e4aSElliott Hughes condSubst = [] 518*e1fe3e4aSElliott Hughes for conditionSet, substitutionMaps in conditionalSubstitutions: 519*e1fe3e4aSElliott Hughes substitutions = [] 520*e1fe3e4aSElliott Hughes for substitutionMap in substitutionMaps: 521*e1fe3e4aSElliott Hughes subst = tuple(sorted(substitutionMap.items())) 522*e1fe3e4aSElliott Hughes substitutions.append(subst) 523*e1fe3e4aSElliott Hughes allSubstitutions.add(subst) 524*e1fe3e4aSElliott Hughes condSubst.append((conditionSet, substitutions)) 525*e1fe3e4aSElliott Hughes return condSubst, sorted(allSubstitutions) 526*e1fe3e4aSElliott Hughes 527*e1fe3e4aSElliott Hughes 528*e1fe3e4aSElliott Hughesclass ShifterVisitor(TTVisitor): 529*e1fe3e4aSElliott Hughes def __init__(self, shift): 530*e1fe3e4aSElliott Hughes self.shift = shift 531*e1fe3e4aSElliott Hughes 532*e1fe3e4aSElliott Hughes 533*e1fe3e4aSElliott Hughes@ShifterVisitor.register_attr(ot.Feature, "LookupListIndex") # GSUB/GPOS 534*e1fe3e4aSElliott Hughesdef visit(visitor, obj, attr, value): 535*e1fe3e4aSElliott Hughes shift = visitor.shift 536*e1fe3e4aSElliott Hughes value = [l + shift for l in value] 537*e1fe3e4aSElliott Hughes setattr(obj, attr, value) 538*e1fe3e4aSElliott Hughes 539*e1fe3e4aSElliott Hughes 540*e1fe3e4aSElliott Hughes@ShifterVisitor.register_attr( 541*e1fe3e4aSElliott Hughes (ot.SubstLookupRecord, ot.PosLookupRecord), "LookupListIndex" 542*e1fe3e4aSElliott Hughes) 543*e1fe3e4aSElliott Hughesdef visit(visitor, obj, attr, value): 544*e1fe3e4aSElliott Hughes setattr(obj, attr, visitor.shift + value) 545*e1fe3e4aSElliott Hughes 546*e1fe3e4aSElliott Hughes 547*e1fe3e4aSElliott Hughesdef buildSubstitutionLookups(gsub, allSubstitutions, processLast=False): 548*e1fe3e4aSElliott Hughes """Build the lookups for the glyph substitutions, return a dict mapping 549*e1fe3e4aSElliott Hughes the substitution to lookup indices.""" 550*e1fe3e4aSElliott Hughes 551*e1fe3e4aSElliott Hughes # Insert lookups at the beginning of the lookup vector 552*e1fe3e4aSElliott Hughes # https://github.com/googlefonts/fontmake/issues/950 553*e1fe3e4aSElliott Hughes 554*e1fe3e4aSElliott Hughes firstIndex = len(gsub.LookupList.Lookup) if processLast else 0 555*e1fe3e4aSElliott Hughes lookupMap = {} 556*e1fe3e4aSElliott Hughes for i, substitutionMap in enumerate(allSubstitutions): 557*e1fe3e4aSElliott Hughes lookupMap[substitutionMap] = firstIndex + i 558*e1fe3e4aSElliott Hughes 559*e1fe3e4aSElliott Hughes if not processLast: 560*e1fe3e4aSElliott Hughes # Shift all lookup indices in gsub by len(allSubstitutions) 561*e1fe3e4aSElliott Hughes shift = len(allSubstitutions) 562*e1fe3e4aSElliott Hughes visitor = ShifterVisitor(shift) 563*e1fe3e4aSElliott Hughes visitor.visit(gsub.FeatureList.FeatureRecord) 564*e1fe3e4aSElliott Hughes visitor.visit(gsub.LookupList.Lookup) 565*e1fe3e4aSElliott Hughes 566*e1fe3e4aSElliott Hughes for i, subst in enumerate(allSubstitutions): 567*e1fe3e4aSElliott Hughes substMap = dict(subst) 568*e1fe3e4aSElliott Hughes lookup = buildLookup([buildSingleSubstSubtable(substMap)]) 569*e1fe3e4aSElliott Hughes if processLast: 570*e1fe3e4aSElliott Hughes gsub.LookupList.Lookup.append(lookup) 571*e1fe3e4aSElliott Hughes else: 572*e1fe3e4aSElliott Hughes gsub.LookupList.Lookup.insert(i, lookup) 573*e1fe3e4aSElliott Hughes assert gsub.LookupList.Lookup[lookupMap[subst]] is lookup 574*e1fe3e4aSElliott Hughes gsub.LookupList.LookupCount = len(gsub.LookupList.Lookup) 575*e1fe3e4aSElliott Hughes return lookupMap 576*e1fe3e4aSElliott Hughes 577*e1fe3e4aSElliott Hughes 578*e1fe3e4aSElliott Hughesdef buildFeatureVariations(featureVariationRecords): 579*e1fe3e4aSElliott Hughes """Build the FeatureVariations subtable.""" 580*e1fe3e4aSElliott Hughes fv = ot.FeatureVariations() 581*e1fe3e4aSElliott Hughes fv.Version = 0x00010000 582*e1fe3e4aSElliott Hughes fv.FeatureVariationRecord = featureVariationRecords 583*e1fe3e4aSElliott Hughes fv.FeatureVariationCount = len(featureVariationRecords) 584*e1fe3e4aSElliott Hughes return fv 585*e1fe3e4aSElliott Hughes 586*e1fe3e4aSElliott Hughes 587*e1fe3e4aSElliott Hughesdef buildFeatureRecord(featureTag, lookupListIndices): 588*e1fe3e4aSElliott Hughes """Build a FeatureRecord.""" 589*e1fe3e4aSElliott Hughes fr = ot.FeatureRecord() 590*e1fe3e4aSElliott Hughes fr.FeatureTag = featureTag 591*e1fe3e4aSElliott Hughes fr.Feature = ot.Feature() 592*e1fe3e4aSElliott Hughes fr.Feature.LookupListIndex = lookupListIndices 593*e1fe3e4aSElliott Hughes fr.Feature.populateDefaults() 594*e1fe3e4aSElliott Hughes return fr 595*e1fe3e4aSElliott Hughes 596*e1fe3e4aSElliott Hughes 597*e1fe3e4aSElliott Hughesdef buildFeatureVariationRecord(conditionTable, substitutionRecords): 598*e1fe3e4aSElliott Hughes """Build a FeatureVariationRecord.""" 599*e1fe3e4aSElliott Hughes fvr = ot.FeatureVariationRecord() 600*e1fe3e4aSElliott Hughes fvr.ConditionSet = ot.ConditionSet() 601*e1fe3e4aSElliott Hughes fvr.ConditionSet.ConditionTable = conditionTable 602*e1fe3e4aSElliott Hughes fvr.ConditionSet.ConditionCount = len(conditionTable) 603*e1fe3e4aSElliott Hughes fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution() 604*e1fe3e4aSElliott Hughes fvr.FeatureTableSubstitution.Version = 0x00010000 605*e1fe3e4aSElliott Hughes fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords 606*e1fe3e4aSElliott Hughes fvr.FeatureTableSubstitution.SubstitutionCount = len(substitutionRecords) 607*e1fe3e4aSElliott Hughes return fvr 608*e1fe3e4aSElliott Hughes 609*e1fe3e4aSElliott Hughes 610*e1fe3e4aSElliott Hughesdef buildFeatureTableSubstitutionRecord(featureIndex, lookupListIndices): 611*e1fe3e4aSElliott Hughes """Build a FeatureTableSubstitutionRecord.""" 612*e1fe3e4aSElliott Hughes ftsr = ot.FeatureTableSubstitutionRecord() 613*e1fe3e4aSElliott Hughes ftsr.FeatureIndex = featureIndex 614*e1fe3e4aSElliott Hughes ftsr.Feature = ot.Feature() 615*e1fe3e4aSElliott Hughes ftsr.Feature.LookupListIndex = lookupListIndices 616*e1fe3e4aSElliott Hughes ftsr.Feature.LookupCount = len(lookupListIndices) 617*e1fe3e4aSElliott Hughes return ftsr 618*e1fe3e4aSElliott Hughes 619*e1fe3e4aSElliott Hughes 620*e1fe3e4aSElliott Hughesdef buildConditionTable(axisIndex, filterRangeMinValue, filterRangeMaxValue): 621*e1fe3e4aSElliott Hughes """Build a ConditionTable.""" 622*e1fe3e4aSElliott Hughes ct = ot.ConditionTable() 623*e1fe3e4aSElliott Hughes ct.Format = 1 624*e1fe3e4aSElliott Hughes ct.AxisIndex = axisIndex 625*e1fe3e4aSElliott Hughes ct.FilterRangeMinValue = filterRangeMinValue 626*e1fe3e4aSElliott Hughes ct.FilterRangeMaxValue = filterRangeMaxValue 627*e1fe3e4aSElliott Hughes return ct 628*e1fe3e4aSElliott Hughes 629*e1fe3e4aSElliott Hughes 630*e1fe3e4aSElliott Hughesdef findFeatureVariationRecord(featureVariations, conditionTable): 631*e1fe3e4aSElliott Hughes """Find a FeatureVariationRecord that has the same conditionTable.""" 632*e1fe3e4aSElliott Hughes if featureVariations.Version != 0x00010000: 633*e1fe3e4aSElliott Hughes raise VarLibError( 634*e1fe3e4aSElliott Hughes "Unsupported FeatureVariations table version: " 635*e1fe3e4aSElliott Hughes f"0x{featureVariations.Version:08x} (expected 0x00010000)." 636*e1fe3e4aSElliott Hughes ) 637*e1fe3e4aSElliott Hughes 638*e1fe3e4aSElliott Hughes for fvr in featureVariations.FeatureVariationRecord: 639*e1fe3e4aSElliott Hughes if conditionTable == fvr.ConditionSet.ConditionTable: 640*e1fe3e4aSElliott Hughes return fvr 641*e1fe3e4aSElliott Hughes 642*e1fe3e4aSElliott Hughes return None 643*e1fe3e4aSElliott Hughes 644*e1fe3e4aSElliott Hughes 645*e1fe3e4aSElliott Hughesdef sortFeatureList(table): 646*e1fe3e4aSElliott Hughes """Sort the feature list by feature tag, and remap the feature indices 647*e1fe3e4aSElliott Hughes elsewhere. This is needed after the feature list has been modified. 648*e1fe3e4aSElliott Hughes """ 649*e1fe3e4aSElliott Hughes # decorate, sort, undecorate, because we need to make an index remapping table 650*e1fe3e4aSElliott Hughes tagIndexFea = [ 651*e1fe3e4aSElliott Hughes (fea.FeatureTag, index, fea) 652*e1fe3e4aSElliott Hughes for index, fea in enumerate(table.FeatureList.FeatureRecord) 653*e1fe3e4aSElliott Hughes ] 654*e1fe3e4aSElliott Hughes tagIndexFea.sort() 655*e1fe3e4aSElliott Hughes table.FeatureList.FeatureRecord = [fea for tag, index, fea in tagIndexFea] 656*e1fe3e4aSElliott Hughes featureRemap = dict( 657*e1fe3e4aSElliott Hughes zip([index for tag, index, fea in tagIndexFea], range(len(tagIndexFea))) 658*e1fe3e4aSElliott Hughes ) 659*e1fe3e4aSElliott Hughes 660*e1fe3e4aSElliott Hughes # Remap the feature indices 661*e1fe3e4aSElliott Hughes remapFeatures(table, featureRemap) 662*e1fe3e4aSElliott Hughes 663*e1fe3e4aSElliott Hughes 664*e1fe3e4aSElliott Hughesdef remapFeatures(table, featureRemap): 665*e1fe3e4aSElliott Hughes """Go through the scripts list, and remap feature indices.""" 666*e1fe3e4aSElliott Hughes for scriptIndex, script in enumerate(table.ScriptList.ScriptRecord): 667*e1fe3e4aSElliott Hughes defaultLangSys = script.Script.DefaultLangSys 668*e1fe3e4aSElliott Hughes if defaultLangSys is not None: 669*e1fe3e4aSElliott Hughes _remapLangSys(defaultLangSys, featureRemap) 670*e1fe3e4aSElliott Hughes for langSysRecordIndex, langSysRec in enumerate(script.Script.LangSysRecord): 671*e1fe3e4aSElliott Hughes langSys = langSysRec.LangSys 672*e1fe3e4aSElliott Hughes _remapLangSys(langSys, featureRemap) 673*e1fe3e4aSElliott Hughes 674*e1fe3e4aSElliott Hughes if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None: 675*e1fe3e4aSElliott Hughes for fvr in table.FeatureVariations.FeatureVariationRecord: 676*e1fe3e4aSElliott Hughes for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord: 677*e1fe3e4aSElliott Hughes ftsr.FeatureIndex = featureRemap[ftsr.FeatureIndex] 678*e1fe3e4aSElliott Hughes 679*e1fe3e4aSElliott Hughes 680*e1fe3e4aSElliott Hughesdef _remapLangSys(langSys, featureRemap): 681*e1fe3e4aSElliott Hughes if langSys.ReqFeatureIndex != 0xFFFF: 682*e1fe3e4aSElliott Hughes langSys.ReqFeatureIndex = featureRemap[langSys.ReqFeatureIndex] 683*e1fe3e4aSElliott Hughes langSys.FeatureIndex = [featureRemap[index] for index in langSys.FeatureIndex] 684*e1fe3e4aSElliott Hughes 685*e1fe3e4aSElliott Hughes 686*e1fe3e4aSElliott Hughesif __name__ == "__main__": 687*e1fe3e4aSElliott Hughes import doctest, sys 688*e1fe3e4aSElliott Hughes 689*e1fe3e4aSElliott Hughes sys.exit(doctest.testmod().failed) 690