1*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import newTable 2*e1fe3e4aSElliott Hughesfrom fontTools.ttLib.tables._f_v_a_r import Axis as fvarAxis 3*e1fe3e4aSElliott Hughesfrom fontTools.pens.areaPen import AreaPen 4*e1fe3e4aSElliott Hughesfrom fontTools.pens.basePen import NullPen 5*e1fe3e4aSElliott Hughesfrom fontTools.pens.statisticsPen import StatisticsPen 6*e1fe3e4aSElliott Hughesfrom fontTools.varLib.models import piecewiseLinearMap, normalizeValue 7*e1fe3e4aSElliott Hughesfrom fontTools.misc.cliTools import makeOutputFileName 8*e1fe3e4aSElliott Hughesimport math 9*e1fe3e4aSElliott Hughesimport logging 10*e1fe3e4aSElliott Hughesfrom pprint import pformat 11*e1fe3e4aSElliott Hughes 12*e1fe3e4aSElliott Hughes__all__ = [ 13*e1fe3e4aSElliott Hughes "planWeightAxis", 14*e1fe3e4aSElliott Hughes "planWidthAxis", 15*e1fe3e4aSElliott Hughes "planSlantAxis", 16*e1fe3e4aSElliott Hughes "planOpticalSizeAxis", 17*e1fe3e4aSElliott Hughes "planAxis", 18*e1fe3e4aSElliott Hughes "sanitizeWeight", 19*e1fe3e4aSElliott Hughes "sanitizeWidth", 20*e1fe3e4aSElliott Hughes "sanitizeSlant", 21*e1fe3e4aSElliott Hughes "measureWeight", 22*e1fe3e4aSElliott Hughes "measureWidth", 23*e1fe3e4aSElliott Hughes "measureSlant", 24*e1fe3e4aSElliott Hughes "normalizeLinear", 25*e1fe3e4aSElliott Hughes "normalizeLog", 26*e1fe3e4aSElliott Hughes "normalizeDegrees", 27*e1fe3e4aSElliott Hughes "interpolateLinear", 28*e1fe3e4aSElliott Hughes "interpolateLog", 29*e1fe3e4aSElliott Hughes "processAxis", 30*e1fe3e4aSElliott Hughes "makeDesignspaceSnippet", 31*e1fe3e4aSElliott Hughes "addEmptyAvar", 32*e1fe3e4aSElliott Hughes "main", 33*e1fe3e4aSElliott Hughes] 34*e1fe3e4aSElliott Hughes 35*e1fe3e4aSElliott Hugheslog = logging.getLogger("fontTools.varLib.avarPlanner") 36*e1fe3e4aSElliott Hughes 37*e1fe3e4aSElliott HughesWEIGHTS = [ 38*e1fe3e4aSElliott Hughes 50, 39*e1fe3e4aSElliott Hughes 100, 40*e1fe3e4aSElliott Hughes 150, 41*e1fe3e4aSElliott Hughes 200, 42*e1fe3e4aSElliott Hughes 250, 43*e1fe3e4aSElliott Hughes 300, 44*e1fe3e4aSElliott Hughes 350, 45*e1fe3e4aSElliott Hughes 400, 46*e1fe3e4aSElliott Hughes 450, 47*e1fe3e4aSElliott Hughes 500, 48*e1fe3e4aSElliott Hughes 550, 49*e1fe3e4aSElliott Hughes 600, 50*e1fe3e4aSElliott Hughes 650, 51*e1fe3e4aSElliott Hughes 700, 52*e1fe3e4aSElliott Hughes 750, 53*e1fe3e4aSElliott Hughes 800, 54*e1fe3e4aSElliott Hughes 850, 55*e1fe3e4aSElliott Hughes 900, 56*e1fe3e4aSElliott Hughes 950, 57*e1fe3e4aSElliott Hughes] 58*e1fe3e4aSElliott Hughes 59*e1fe3e4aSElliott HughesWIDTHS = [ 60*e1fe3e4aSElliott Hughes 25.0, 61*e1fe3e4aSElliott Hughes 37.5, 62*e1fe3e4aSElliott Hughes 50.0, 63*e1fe3e4aSElliott Hughes 62.5, 64*e1fe3e4aSElliott Hughes 75.0, 65*e1fe3e4aSElliott Hughes 87.5, 66*e1fe3e4aSElliott Hughes 100.0, 67*e1fe3e4aSElliott Hughes 112.5, 68*e1fe3e4aSElliott Hughes 125.0, 69*e1fe3e4aSElliott Hughes 137.5, 70*e1fe3e4aSElliott Hughes 150.0, 71*e1fe3e4aSElliott Hughes 162.5, 72*e1fe3e4aSElliott Hughes 175.0, 73*e1fe3e4aSElliott Hughes 187.5, 74*e1fe3e4aSElliott Hughes 200.0, 75*e1fe3e4aSElliott Hughes] 76*e1fe3e4aSElliott Hughes 77*e1fe3e4aSElliott HughesSLANTS = list(math.degrees(math.atan(d / 20.0)) for d in range(-20, 21)) 78*e1fe3e4aSElliott Hughes 79*e1fe3e4aSElliott HughesSIZES = [ 80*e1fe3e4aSElliott Hughes 5, 81*e1fe3e4aSElliott Hughes 6, 82*e1fe3e4aSElliott Hughes 7, 83*e1fe3e4aSElliott Hughes 8, 84*e1fe3e4aSElliott Hughes 9, 85*e1fe3e4aSElliott Hughes 10, 86*e1fe3e4aSElliott Hughes 11, 87*e1fe3e4aSElliott Hughes 12, 88*e1fe3e4aSElliott Hughes 14, 89*e1fe3e4aSElliott Hughes 18, 90*e1fe3e4aSElliott Hughes 24, 91*e1fe3e4aSElliott Hughes 30, 92*e1fe3e4aSElliott Hughes 36, 93*e1fe3e4aSElliott Hughes 48, 94*e1fe3e4aSElliott Hughes 60, 95*e1fe3e4aSElliott Hughes 72, 96*e1fe3e4aSElliott Hughes 96, 97*e1fe3e4aSElliott Hughes 120, 98*e1fe3e4aSElliott Hughes 144, 99*e1fe3e4aSElliott Hughes 192, 100*e1fe3e4aSElliott Hughes 240, 101*e1fe3e4aSElliott Hughes 288, 102*e1fe3e4aSElliott Hughes] 103*e1fe3e4aSElliott Hughes 104*e1fe3e4aSElliott Hughes 105*e1fe3e4aSElliott HughesSAMPLES = 8 106*e1fe3e4aSElliott Hughes 107*e1fe3e4aSElliott Hughes 108*e1fe3e4aSElliott Hughesdef normalizeLinear(value, rangeMin, rangeMax): 109*e1fe3e4aSElliott Hughes """Linearly normalize value in [rangeMin, rangeMax] to [0, 1], with extrapolation.""" 110*e1fe3e4aSElliott Hughes return (value - rangeMin) / (rangeMax - rangeMin) 111*e1fe3e4aSElliott Hughes 112*e1fe3e4aSElliott Hughes 113*e1fe3e4aSElliott Hughesdef interpolateLinear(t, a, b): 114*e1fe3e4aSElliott Hughes """Linear interpolation between a and b, with t typically in [0, 1].""" 115*e1fe3e4aSElliott Hughes return a + t * (b - a) 116*e1fe3e4aSElliott Hughes 117*e1fe3e4aSElliott Hughes 118*e1fe3e4aSElliott Hughesdef normalizeLog(value, rangeMin, rangeMax): 119*e1fe3e4aSElliott Hughes """Logarithmically normalize value in [rangeMin, rangeMax] to [0, 1], with extrapolation.""" 120*e1fe3e4aSElliott Hughes logMin = math.log(rangeMin) 121*e1fe3e4aSElliott Hughes logMax = math.log(rangeMax) 122*e1fe3e4aSElliott Hughes return (math.log(value) - logMin) / (logMax - logMin) 123*e1fe3e4aSElliott Hughes 124*e1fe3e4aSElliott Hughes 125*e1fe3e4aSElliott Hughesdef interpolateLog(t, a, b): 126*e1fe3e4aSElliott Hughes """Logarithmic interpolation between a and b, with t typically in [0, 1].""" 127*e1fe3e4aSElliott Hughes logA = math.log(a) 128*e1fe3e4aSElliott Hughes logB = math.log(b) 129*e1fe3e4aSElliott Hughes return math.exp(logA + t * (logB - logA)) 130*e1fe3e4aSElliott Hughes 131*e1fe3e4aSElliott Hughes 132*e1fe3e4aSElliott Hughesdef normalizeDegrees(value, rangeMin, rangeMax): 133*e1fe3e4aSElliott Hughes """Angularly normalize value in [rangeMin, rangeMax] to [0, 1], with extrapolation.""" 134*e1fe3e4aSElliott Hughes tanMin = math.tan(math.radians(rangeMin)) 135*e1fe3e4aSElliott Hughes tanMax = math.tan(math.radians(rangeMax)) 136*e1fe3e4aSElliott Hughes return (math.tan(math.radians(value)) - tanMin) / (tanMax - tanMin) 137*e1fe3e4aSElliott Hughes 138*e1fe3e4aSElliott Hughes 139*e1fe3e4aSElliott Hughesdef measureWeight(glyphset, glyphs=None): 140*e1fe3e4aSElliott Hughes """Measure the perceptual average weight of the given glyphs.""" 141*e1fe3e4aSElliott Hughes if isinstance(glyphs, dict): 142*e1fe3e4aSElliott Hughes frequencies = glyphs 143*e1fe3e4aSElliott Hughes else: 144*e1fe3e4aSElliott Hughes frequencies = {g: 1 for g in glyphs} 145*e1fe3e4aSElliott Hughes 146*e1fe3e4aSElliott Hughes wght_sum = wdth_sum = 0 147*e1fe3e4aSElliott Hughes for glyph_name in glyphs: 148*e1fe3e4aSElliott Hughes if frequencies is not None: 149*e1fe3e4aSElliott Hughes frequency = frequencies.get(glyph_name, 0) 150*e1fe3e4aSElliott Hughes if frequency == 0: 151*e1fe3e4aSElliott Hughes continue 152*e1fe3e4aSElliott Hughes else: 153*e1fe3e4aSElliott Hughes frequency = 1 154*e1fe3e4aSElliott Hughes 155*e1fe3e4aSElliott Hughes glyph = glyphset[glyph_name] 156*e1fe3e4aSElliott Hughes 157*e1fe3e4aSElliott Hughes pen = AreaPen(glyphset=glyphset) 158*e1fe3e4aSElliott Hughes glyph.draw(pen) 159*e1fe3e4aSElliott Hughes 160*e1fe3e4aSElliott Hughes mult = glyph.width * frequency 161*e1fe3e4aSElliott Hughes wght_sum += mult * abs(pen.value) 162*e1fe3e4aSElliott Hughes wdth_sum += mult 163*e1fe3e4aSElliott Hughes 164*e1fe3e4aSElliott Hughes return wght_sum / wdth_sum 165*e1fe3e4aSElliott Hughes 166*e1fe3e4aSElliott Hughes 167*e1fe3e4aSElliott Hughesdef measureWidth(glyphset, glyphs=None): 168*e1fe3e4aSElliott Hughes """Measure the average width of the given glyphs.""" 169*e1fe3e4aSElliott Hughes if isinstance(glyphs, dict): 170*e1fe3e4aSElliott Hughes frequencies = glyphs 171*e1fe3e4aSElliott Hughes else: 172*e1fe3e4aSElliott Hughes frequencies = {g: 1 for g in glyphs} 173*e1fe3e4aSElliott Hughes 174*e1fe3e4aSElliott Hughes wdth_sum = 0 175*e1fe3e4aSElliott Hughes freq_sum = 0 176*e1fe3e4aSElliott Hughes for glyph_name in glyphs: 177*e1fe3e4aSElliott Hughes if frequencies is not None: 178*e1fe3e4aSElliott Hughes frequency = frequencies.get(glyph_name, 0) 179*e1fe3e4aSElliott Hughes if frequency == 0: 180*e1fe3e4aSElliott Hughes continue 181*e1fe3e4aSElliott Hughes else: 182*e1fe3e4aSElliott Hughes frequency = 1 183*e1fe3e4aSElliott Hughes 184*e1fe3e4aSElliott Hughes glyph = glyphset[glyph_name] 185*e1fe3e4aSElliott Hughes 186*e1fe3e4aSElliott Hughes pen = NullPen() 187*e1fe3e4aSElliott Hughes glyph.draw(pen) 188*e1fe3e4aSElliott Hughes 189*e1fe3e4aSElliott Hughes wdth_sum += glyph.width * frequency 190*e1fe3e4aSElliott Hughes freq_sum += frequency 191*e1fe3e4aSElliott Hughes 192*e1fe3e4aSElliott Hughes return wdth_sum / freq_sum 193*e1fe3e4aSElliott Hughes 194*e1fe3e4aSElliott Hughes 195*e1fe3e4aSElliott Hughesdef measureSlant(glyphset, glyphs=None): 196*e1fe3e4aSElliott Hughes """Measure the perceptual average slant angle of the given glyphs.""" 197*e1fe3e4aSElliott Hughes if isinstance(glyphs, dict): 198*e1fe3e4aSElliott Hughes frequencies = glyphs 199*e1fe3e4aSElliott Hughes else: 200*e1fe3e4aSElliott Hughes frequencies = {g: 1 for g in glyphs} 201*e1fe3e4aSElliott Hughes 202*e1fe3e4aSElliott Hughes slnt_sum = 0 203*e1fe3e4aSElliott Hughes freq_sum = 0 204*e1fe3e4aSElliott Hughes for glyph_name in glyphs: 205*e1fe3e4aSElliott Hughes if frequencies is not None: 206*e1fe3e4aSElliott Hughes frequency = frequencies.get(glyph_name, 0) 207*e1fe3e4aSElliott Hughes if frequency == 0: 208*e1fe3e4aSElliott Hughes continue 209*e1fe3e4aSElliott Hughes else: 210*e1fe3e4aSElliott Hughes frequency = 1 211*e1fe3e4aSElliott Hughes 212*e1fe3e4aSElliott Hughes glyph = glyphset[glyph_name] 213*e1fe3e4aSElliott Hughes 214*e1fe3e4aSElliott Hughes pen = StatisticsPen(glyphset=glyphset) 215*e1fe3e4aSElliott Hughes glyph.draw(pen) 216*e1fe3e4aSElliott Hughes 217*e1fe3e4aSElliott Hughes mult = glyph.width * frequency 218*e1fe3e4aSElliott Hughes slnt_sum += mult * pen.slant 219*e1fe3e4aSElliott Hughes freq_sum += mult 220*e1fe3e4aSElliott Hughes 221*e1fe3e4aSElliott Hughes return -math.degrees(math.atan(slnt_sum / freq_sum)) 222*e1fe3e4aSElliott Hughes 223*e1fe3e4aSElliott Hughes 224*e1fe3e4aSElliott Hughesdef sanitizeWidth(userTriple, designTriple, pins, measurements): 225*e1fe3e4aSElliott Hughes """Sanitize the width axis limits.""" 226*e1fe3e4aSElliott Hughes 227*e1fe3e4aSElliott Hughes minVal, defaultVal, maxVal = ( 228*e1fe3e4aSElliott Hughes measurements[designTriple[0]], 229*e1fe3e4aSElliott Hughes measurements[designTriple[1]], 230*e1fe3e4aSElliott Hughes measurements[designTriple[2]], 231*e1fe3e4aSElliott Hughes ) 232*e1fe3e4aSElliott Hughes 233*e1fe3e4aSElliott Hughes calculatedMinVal = userTriple[1] * (minVal / defaultVal) 234*e1fe3e4aSElliott Hughes calculatedMaxVal = userTriple[1] * (maxVal / defaultVal) 235*e1fe3e4aSElliott Hughes 236*e1fe3e4aSElliott Hughes log.info("Original width axis limits: %g:%g:%g", *userTriple) 237*e1fe3e4aSElliott Hughes log.info( 238*e1fe3e4aSElliott Hughes "Calculated width axis limits: %g:%g:%g", 239*e1fe3e4aSElliott Hughes calculatedMinVal, 240*e1fe3e4aSElliott Hughes userTriple[1], 241*e1fe3e4aSElliott Hughes calculatedMaxVal, 242*e1fe3e4aSElliott Hughes ) 243*e1fe3e4aSElliott Hughes 244*e1fe3e4aSElliott Hughes if ( 245*e1fe3e4aSElliott Hughes abs(calculatedMinVal - userTriple[0]) / userTriple[1] > 0.05 246*e1fe3e4aSElliott Hughes or abs(calculatedMaxVal - userTriple[2]) / userTriple[1] > 0.05 247*e1fe3e4aSElliott Hughes ): 248*e1fe3e4aSElliott Hughes log.warning("Calculated width axis min/max do not match user input.") 249*e1fe3e4aSElliott Hughes log.warning( 250*e1fe3e4aSElliott Hughes " Current width axis limits: %g:%g:%g", 251*e1fe3e4aSElliott Hughes *userTriple, 252*e1fe3e4aSElliott Hughes ) 253*e1fe3e4aSElliott Hughes log.warning( 254*e1fe3e4aSElliott Hughes " Suggested width axis limits: %g:%g:%g", 255*e1fe3e4aSElliott Hughes calculatedMinVal, 256*e1fe3e4aSElliott Hughes userTriple[1], 257*e1fe3e4aSElliott Hughes calculatedMaxVal, 258*e1fe3e4aSElliott Hughes ) 259*e1fe3e4aSElliott Hughes 260*e1fe3e4aSElliott Hughes return False 261*e1fe3e4aSElliott Hughes 262*e1fe3e4aSElliott Hughes return True 263*e1fe3e4aSElliott Hughes 264*e1fe3e4aSElliott Hughes 265*e1fe3e4aSElliott Hughesdef sanitizeWeight(userTriple, designTriple, pins, measurements): 266*e1fe3e4aSElliott Hughes """Sanitize the weight axis limits.""" 267*e1fe3e4aSElliott Hughes 268*e1fe3e4aSElliott Hughes if len(set(userTriple)) < 3: 269*e1fe3e4aSElliott Hughes return True 270*e1fe3e4aSElliott Hughes 271*e1fe3e4aSElliott Hughes minVal, defaultVal, maxVal = ( 272*e1fe3e4aSElliott Hughes measurements[designTriple[0]], 273*e1fe3e4aSElliott Hughes measurements[designTriple[1]], 274*e1fe3e4aSElliott Hughes measurements[designTriple[2]], 275*e1fe3e4aSElliott Hughes ) 276*e1fe3e4aSElliott Hughes 277*e1fe3e4aSElliott Hughes logMin = math.log(minVal) 278*e1fe3e4aSElliott Hughes logDefault = math.log(defaultVal) 279*e1fe3e4aSElliott Hughes logMax = math.log(maxVal) 280*e1fe3e4aSElliott Hughes 281*e1fe3e4aSElliott Hughes t = (userTriple[1] - userTriple[0]) / (userTriple[2] - userTriple[0]) 282*e1fe3e4aSElliott Hughes y = math.exp(logMin + t * (logMax - logMin)) 283*e1fe3e4aSElliott Hughes t = (y - minVal) / (maxVal - minVal) 284*e1fe3e4aSElliott Hughes calculatedDefaultVal = userTriple[0] + t * (userTriple[2] - userTriple[0]) 285*e1fe3e4aSElliott Hughes 286*e1fe3e4aSElliott Hughes log.info("Original weight axis limits: %g:%g:%g", *userTriple) 287*e1fe3e4aSElliott Hughes log.info( 288*e1fe3e4aSElliott Hughes "Calculated weight axis limits: %g:%g:%g", 289*e1fe3e4aSElliott Hughes userTriple[0], 290*e1fe3e4aSElliott Hughes calculatedDefaultVal, 291*e1fe3e4aSElliott Hughes userTriple[2], 292*e1fe3e4aSElliott Hughes ) 293*e1fe3e4aSElliott Hughes 294*e1fe3e4aSElliott Hughes if abs(calculatedDefaultVal - userTriple[1]) / userTriple[1] > 0.05: 295*e1fe3e4aSElliott Hughes log.warning("Calculated weight axis default does not match user input.") 296*e1fe3e4aSElliott Hughes 297*e1fe3e4aSElliott Hughes log.warning( 298*e1fe3e4aSElliott Hughes " Current weight axis limits: %g:%g:%g", 299*e1fe3e4aSElliott Hughes *userTriple, 300*e1fe3e4aSElliott Hughes ) 301*e1fe3e4aSElliott Hughes 302*e1fe3e4aSElliott Hughes log.warning( 303*e1fe3e4aSElliott Hughes " Suggested weight axis limits, changing default: %g:%g:%g", 304*e1fe3e4aSElliott Hughes userTriple[0], 305*e1fe3e4aSElliott Hughes calculatedDefaultVal, 306*e1fe3e4aSElliott Hughes userTriple[2], 307*e1fe3e4aSElliott Hughes ) 308*e1fe3e4aSElliott Hughes 309*e1fe3e4aSElliott Hughes t = (userTriple[2] - userTriple[0]) / (userTriple[1] - userTriple[0]) 310*e1fe3e4aSElliott Hughes y = math.exp(logMin + t * (logDefault - logMin)) 311*e1fe3e4aSElliott Hughes t = (y - minVal) / (defaultVal - minVal) 312*e1fe3e4aSElliott Hughes calculatedMaxVal = userTriple[0] + t * (userTriple[1] - userTriple[0]) 313*e1fe3e4aSElliott Hughes log.warning( 314*e1fe3e4aSElliott Hughes " Suggested weight axis limits, changing maximum: %g:%g:%g", 315*e1fe3e4aSElliott Hughes userTriple[0], 316*e1fe3e4aSElliott Hughes userTriple[1], 317*e1fe3e4aSElliott Hughes calculatedMaxVal, 318*e1fe3e4aSElliott Hughes ) 319*e1fe3e4aSElliott Hughes 320*e1fe3e4aSElliott Hughes t = (userTriple[0] - userTriple[2]) / (userTriple[1] - userTriple[2]) 321*e1fe3e4aSElliott Hughes y = math.exp(logMax + t * (logDefault - logMax)) 322*e1fe3e4aSElliott Hughes t = (y - maxVal) / (defaultVal - maxVal) 323*e1fe3e4aSElliott Hughes calculatedMinVal = userTriple[2] + t * (userTriple[1] - userTriple[2]) 324*e1fe3e4aSElliott Hughes log.warning( 325*e1fe3e4aSElliott Hughes " Suggested weight axis limits, changing minimum: %g:%g:%g", 326*e1fe3e4aSElliott Hughes calculatedMinVal, 327*e1fe3e4aSElliott Hughes userTriple[1], 328*e1fe3e4aSElliott Hughes userTriple[2], 329*e1fe3e4aSElliott Hughes ) 330*e1fe3e4aSElliott Hughes 331*e1fe3e4aSElliott Hughes return False 332*e1fe3e4aSElliott Hughes 333*e1fe3e4aSElliott Hughes return True 334*e1fe3e4aSElliott Hughes 335*e1fe3e4aSElliott Hughes 336*e1fe3e4aSElliott Hughesdef sanitizeSlant(userTriple, designTriple, pins, measurements): 337*e1fe3e4aSElliott Hughes """Sanitize the slant axis limits.""" 338*e1fe3e4aSElliott Hughes 339*e1fe3e4aSElliott Hughes log.info("Original slant axis limits: %g:%g:%g", *userTriple) 340*e1fe3e4aSElliott Hughes log.info( 341*e1fe3e4aSElliott Hughes "Calculated slant axis limits: %g:%g:%g", 342*e1fe3e4aSElliott Hughes measurements[designTriple[0]], 343*e1fe3e4aSElliott Hughes measurements[designTriple[1]], 344*e1fe3e4aSElliott Hughes measurements[designTriple[2]], 345*e1fe3e4aSElliott Hughes ) 346*e1fe3e4aSElliott Hughes 347*e1fe3e4aSElliott Hughes if ( 348*e1fe3e4aSElliott Hughes abs(measurements[designTriple[0]] - userTriple[0]) > 1 349*e1fe3e4aSElliott Hughes or abs(measurements[designTriple[1]] - userTriple[1]) > 1 350*e1fe3e4aSElliott Hughes or abs(measurements[designTriple[2]] - userTriple[2]) > 1 351*e1fe3e4aSElliott Hughes ): 352*e1fe3e4aSElliott Hughes log.warning("Calculated slant axis min/default/max do not match user input.") 353*e1fe3e4aSElliott Hughes log.warning( 354*e1fe3e4aSElliott Hughes " Current slant axis limits: %g:%g:%g", 355*e1fe3e4aSElliott Hughes *userTriple, 356*e1fe3e4aSElliott Hughes ) 357*e1fe3e4aSElliott Hughes log.warning( 358*e1fe3e4aSElliott Hughes " Suggested slant axis limits: %g:%g:%g", 359*e1fe3e4aSElliott Hughes measurements[designTriple[0]], 360*e1fe3e4aSElliott Hughes measurements[designTriple[1]], 361*e1fe3e4aSElliott Hughes measurements[designTriple[2]], 362*e1fe3e4aSElliott Hughes ) 363*e1fe3e4aSElliott Hughes 364*e1fe3e4aSElliott Hughes return False 365*e1fe3e4aSElliott Hughes 366*e1fe3e4aSElliott Hughes return True 367*e1fe3e4aSElliott Hughes 368*e1fe3e4aSElliott Hughes 369*e1fe3e4aSElliott Hughesdef planAxis( 370*e1fe3e4aSElliott Hughes measureFunc, 371*e1fe3e4aSElliott Hughes normalizeFunc, 372*e1fe3e4aSElliott Hughes interpolateFunc, 373*e1fe3e4aSElliott Hughes glyphSetFunc, 374*e1fe3e4aSElliott Hughes axisTag, 375*e1fe3e4aSElliott Hughes axisLimits, 376*e1fe3e4aSElliott Hughes values, 377*e1fe3e4aSElliott Hughes samples=None, 378*e1fe3e4aSElliott Hughes glyphs=None, 379*e1fe3e4aSElliott Hughes designLimits=None, 380*e1fe3e4aSElliott Hughes pins=None, 381*e1fe3e4aSElliott Hughes sanitizeFunc=None, 382*e1fe3e4aSElliott Hughes): 383*e1fe3e4aSElliott Hughes """Plan an axis. 384*e1fe3e4aSElliott Hughes 385*e1fe3e4aSElliott Hughes measureFunc: callable that takes a glyphset and an optional 386*e1fe3e4aSElliott Hughes list of glyphnames, and returns the glyphset-wide measurement 387*e1fe3e4aSElliott Hughes to be used for the axis. 388*e1fe3e4aSElliott Hughes 389*e1fe3e4aSElliott Hughes normalizeFunc: callable that takes a measurement and a minimum 390*e1fe3e4aSElliott Hughes and maximum, and normalizes the measurement into the range 0..1, 391*e1fe3e4aSElliott Hughes possibly extrapolating too. 392*e1fe3e4aSElliott Hughes 393*e1fe3e4aSElliott Hughes interpolateFunc: callable that takes a normalized t value, and a 394*e1fe3e4aSElliott Hughes minimum and maximum, and returns the interpolated value, 395*e1fe3e4aSElliott Hughes possibly extrapolating too. 396*e1fe3e4aSElliott Hughes 397*e1fe3e4aSElliott Hughes glyphSetFunc: callable that takes a variations "location" dictionary, 398*e1fe3e4aSElliott Hughes and returns a glyphset. 399*e1fe3e4aSElliott Hughes 400*e1fe3e4aSElliott Hughes axisTag: the axis tag string. 401*e1fe3e4aSElliott Hughes 402*e1fe3e4aSElliott Hughes axisLimits: a triple of minimum, default, and maximum values for 403*e1fe3e4aSElliott Hughes the axis. Or an `fvar` Axis object. 404*e1fe3e4aSElliott Hughes 405*e1fe3e4aSElliott Hughes values: a list of output values to map for this axis. 406*e1fe3e4aSElliott Hughes 407*e1fe3e4aSElliott Hughes samples: the number of samples to use when sampling. Default 8. 408*e1fe3e4aSElliott Hughes 409*e1fe3e4aSElliott Hughes glyphs: a list of glyph names to use when sampling. Defaults to None, 410*e1fe3e4aSElliott Hughes which will process all glyphs. 411*e1fe3e4aSElliott Hughes 412*e1fe3e4aSElliott Hughes designLimits: an optional triple of minimum, default, and maximum values 413*e1fe3e4aSElliott Hughes represenging the "design" limits for the axis. If not provided, the 414*e1fe3e4aSElliott Hughes axisLimits will be used. 415*e1fe3e4aSElliott Hughes 416*e1fe3e4aSElliott Hughes pins: an optional dictionary of before/after mapping entries to pin in 417*e1fe3e4aSElliott Hughes the output. 418*e1fe3e4aSElliott Hughes 419*e1fe3e4aSElliott Hughes sanitizeFunc: an optional callable to call to sanitize the axis limits. 420*e1fe3e4aSElliott Hughes """ 421*e1fe3e4aSElliott Hughes 422*e1fe3e4aSElliott Hughes if isinstance(axisLimits, fvarAxis): 423*e1fe3e4aSElliott Hughes axisLimits = (axisLimits.minValue, axisLimits.defaultValue, axisLimits.maxValue) 424*e1fe3e4aSElliott Hughes minValue, defaultValue, maxValue = axisLimits 425*e1fe3e4aSElliott Hughes 426*e1fe3e4aSElliott Hughes if samples is None: 427*e1fe3e4aSElliott Hughes samples = SAMPLES 428*e1fe3e4aSElliott Hughes if glyphs is None: 429*e1fe3e4aSElliott Hughes glyphs = glyphSetFunc({}).keys() 430*e1fe3e4aSElliott Hughes if pins is None: 431*e1fe3e4aSElliott Hughes pins = {} 432*e1fe3e4aSElliott Hughes else: 433*e1fe3e4aSElliott Hughes pins = pins.copy() 434*e1fe3e4aSElliott Hughes 435*e1fe3e4aSElliott Hughes log.info( 436*e1fe3e4aSElliott Hughes "Axis limits min %g / default %g / max %g", minValue, defaultValue, maxValue 437*e1fe3e4aSElliott Hughes ) 438*e1fe3e4aSElliott Hughes triple = (minValue, defaultValue, maxValue) 439*e1fe3e4aSElliott Hughes 440*e1fe3e4aSElliott Hughes if designLimits is not None: 441*e1fe3e4aSElliott Hughes log.info("Axis design-limits min %g / default %g / max %g", *designLimits) 442*e1fe3e4aSElliott Hughes else: 443*e1fe3e4aSElliott Hughes designLimits = triple 444*e1fe3e4aSElliott Hughes 445*e1fe3e4aSElliott Hughes if pins: 446*e1fe3e4aSElliott Hughes log.info("Pins %s", sorted(pins.items())) 447*e1fe3e4aSElliott Hughes pins.update( 448*e1fe3e4aSElliott Hughes { 449*e1fe3e4aSElliott Hughes minValue: designLimits[0], 450*e1fe3e4aSElliott Hughes defaultValue: designLimits[1], 451*e1fe3e4aSElliott Hughes maxValue: designLimits[2], 452*e1fe3e4aSElliott Hughes } 453*e1fe3e4aSElliott Hughes ) 454*e1fe3e4aSElliott Hughes 455*e1fe3e4aSElliott Hughes out = {} 456*e1fe3e4aSElliott Hughes outNormalized = {} 457*e1fe3e4aSElliott Hughes 458*e1fe3e4aSElliott Hughes axisMeasurements = {} 459*e1fe3e4aSElliott Hughes for value in sorted({minValue, defaultValue, maxValue} | set(pins.keys())): 460*e1fe3e4aSElliott Hughes glyphset = glyphSetFunc(location={axisTag: value}) 461*e1fe3e4aSElliott Hughes designValue = pins[value] 462*e1fe3e4aSElliott Hughes axisMeasurements[designValue] = measureFunc(glyphset, glyphs) 463*e1fe3e4aSElliott Hughes 464*e1fe3e4aSElliott Hughes if sanitizeFunc is not None: 465*e1fe3e4aSElliott Hughes log.info("Sanitizing axis limit values for the `%s` axis.", axisTag) 466*e1fe3e4aSElliott Hughes sanitizeFunc(triple, designLimits, pins, axisMeasurements) 467*e1fe3e4aSElliott Hughes 468*e1fe3e4aSElliott Hughes log.debug("Calculated average value:\n%s", pformat(axisMeasurements)) 469*e1fe3e4aSElliott Hughes 470*e1fe3e4aSElliott Hughes for (rangeMin, targetMin), (rangeMax, targetMax) in zip( 471*e1fe3e4aSElliott Hughes list(sorted(pins.items()))[:-1], 472*e1fe3e4aSElliott Hughes list(sorted(pins.items()))[1:], 473*e1fe3e4aSElliott Hughes ): 474*e1fe3e4aSElliott Hughes targetValues = {w for w in values if rangeMin < w < rangeMax} 475*e1fe3e4aSElliott Hughes if not targetValues: 476*e1fe3e4aSElliott Hughes continue 477*e1fe3e4aSElliott Hughes 478*e1fe3e4aSElliott Hughes normalizedMin = normalizeValue(rangeMin, triple) 479*e1fe3e4aSElliott Hughes normalizedMax = normalizeValue(rangeMax, triple) 480*e1fe3e4aSElliott Hughes normalizedTargetMin = normalizeValue(targetMin, designLimits) 481*e1fe3e4aSElliott Hughes normalizedTargetMax = normalizeValue(targetMax, designLimits) 482*e1fe3e4aSElliott Hughes 483*e1fe3e4aSElliott Hughes log.info("Planning target values %s.", sorted(targetValues)) 484*e1fe3e4aSElliott Hughes log.info("Sampling %u points in range %g,%g.", samples, rangeMin, rangeMax) 485*e1fe3e4aSElliott Hughes valueMeasurements = axisMeasurements.copy() 486*e1fe3e4aSElliott Hughes for sample in range(1, samples + 1): 487*e1fe3e4aSElliott Hughes value = rangeMin + (rangeMax - rangeMin) * sample / (samples + 1) 488*e1fe3e4aSElliott Hughes log.debug("Sampling value %g.", value) 489*e1fe3e4aSElliott Hughes glyphset = glyphSetFunc(location={axisTag: value}) 490*e1fe3e4aSElliott Hughes designValue = piecewiseLinearMap(value, pins) 491*e1fe3e4aSElliott Hughes valueMeasurements[designValue] = measureFunc(glyphset, glyphs) 492*e1fe3e4aSElliott Hughes log.debug("Sampled average value:\n%s", pformat(valueMeasurements)) 493*e1fe3e4aSElliott Hughes 494*e1fe3e4aSElliott Hughes measurementValue = {} 495*e1fe3e4aSElliott Hughes for value in sorted(valueMeasurements): 496*e1fe3e4aSElliott Hughes measurementValue[valueMeasurements[value]] = value 497*e1fe3e4aSElliott Hughes 498*e1fe3e4aSElliott Hughes out[rangeMin] = targetMin 499*e1fe3e4aSElliott Hughes outNormalized[normalizedMin] = normalizedTargetMin 500*e1fe3e4aSElliott Hughes for value in sorted(targetValues): 501*e1fe3e4aSElliott Hughes t = normalizeFunc(value, rangeMin, rangeMax) 502*e1fe3e4aSElliott Hughes targetMeasurement = interpolateFunc( 503*e1fe3e4aSElliott Hughes t, valueMeasurements[targetMin], valueMeasurements[targetMax] 504*e1fe3e4aSElliott Hughes ) 505*e1fe3e4aSElliott Hughes targetValue = piecewiseLinearMap(targetMeasurement, measurementValue) 506*e1fe3e4aSElliott Hughes log.debug("Planned mapping value %g to %g." % (value, targetValue)) 507*e1fe3e4aSElliott Hughes out[value] = targetValue 508*e1fe3e4aSElliott Hughes valueNormalized = normalizedMin + (value - rangeMin) / ( 509*e1fe3e4aSElliott Hughes rangeMax - rangeMin 510*e1fe3e4aSElliott Hughes ) * (normalizedMax - normalizedMin) 511*e1fe3e4aSElliott Hughes outNormalized[valueNormalized] = normalizedTargetMin + ( 512*e1fe3e4aSElliott Hughes targetValue - targetMin 513*e1fe3e4aSElliott Hughes ) / (targetMax - targetMin) * (normalizedTargetMax - normalizedTargetMin) 514*e1fe3e4aSElliott Hughes out[rangeMax] = targetMax 515*e1fe3e4aSElliott Hughes outNormalized[normalizedMax] = normalizedTargetMax 516*e1fe3e4aSElliott Hughes 517*e1fe3e4aSElliott Hughes log.info("Planned mapping for the `%s` axis:\n%s", axisTag, pformat(out)) 518*e1fe3e4aSElliott Hughes log.info( 519*e1fe3e4aSElliott Hughes "Planned normalized mapping for the `%s` axis:\n%s", 520*e1fe3e4aSElliott Hughes axisTag, 521*e1fe3e4aSElliott Hughes pformat(outNormalized), 522*e1fe3e4aSElliott Hughes ) 523*e1fe3e4aSElliott Hughes 524*e1fe3e4aSElliott Hughes if all(abs(k - v) < 0.01 for k, v in outNormalized.items()): 525*e1fe3e4aSElliott Hughes log.info("Detected identity mapping for the `%s` axis. Dropping.", axisTag) 526*e1fe3e4aSElliott Hughes out = {} 527*e1fe3e4aSElliott Hughes outNormalized = {} 528*e1fe3e4aSElliott Hughes 529*e1fe3e4aSElliott Hughes return out, outNormalized 530*e1fe3e4aSElliott Hughes 531*e1fe3e4aSElliott Hughes 532*e1fe3e4aSElliott Hughesdef planWeightAxis( 533*e1fe3e4aSElliott Hughes glyphSetFunc, 534*e1fe3e4aSElliott Hughes axisLimits, 535*e1fe3e4aSElliott Hughes weights=None, 536*e1fe3e4aSElliott Hughes samples=None, 537*e1fe3e4aSElliott Hughes glyphs=None, 538*e1fe3e4aSElliott Hughes designLimits=None, 539*e1fe3e4aSElliott Hughes pins=None, 540*e1fe3e4aSElliott Hughes sanitize=False, 541*e1fe3e4aSElliott Hughes): 542*e1fe3e4aSElliott Hughes """Plan a weight (`wght`) axis. 543*e1fe3e4aSElliott Hughes 544*e1fe3e4aSElliott Hughes weights: A list of weight values to plan for. If None, the default 545*e1fe3e4aSElliott Hughes values are used. 546*e1fe3e4aSElliott Hughes 547*e1fe3e4aSElliott Hughes This function simply calls planAxis with values=weights, and the appropriate 548*e1fe3e4aSElliott Hughes arguments. See documenation for planAxis for more information. 549*e1fe3e4aSElliott Hughes """ 550*e1fe3e4aSElliott Hughes 551*e1fe3e4aSElliott Hughes if weights is None: 552*e1fe3e4aSElliott Hughes weights = WEIGHTS 553*e1fe3e4aSElliott Hughes 554*e1fe3e4aSElliott Hughes return planAxis( 555*e1fe3e4aSElliott Hughes measureWeight, 556*e1fe3e4aSElliott Hughes normalizeLinear, 557*e1fe3e4aSElliott Hughes interpolateLog, 558*e1fe3e4aSElliott Hughes glyphSetFunc, 559*e1fe3e4aSElliott Hughes "wght", 560*e1fe3e4aSElliott Hughes axisLimits, 561*e1fe3e4aSElliott Hughes values=weights, 562*e1fe3e4aSElliott Hughes samples=samples, 563*e1fe3e4aSElliott Hughes glyphs=glyphs, 564*e1fe3e4aSElliott Hughes designLimits=designLimits, 565*e1fe3e4aSElliott Hughes pins=pins, 566*e1fe3e4aSElliott Hughes sanitizeFunc=sanitizeWeight if sanitize else None, 567*e1fe3e4aSElliott Hughes ) 568*e1fe3e4aSElliott Hughes 569*e1fe3e4aSElliott Hughes 570*e1fe3e4aSElliott Hughesdef planWidthAxis( 571*e1fe3e4aSElliott Hughes glyphSetFunc, 572*e1fe3e4aSElliott Hughes axisLimits, 573*e1fe3e4aSElliott Hughes widths=None, 574*e1fe3e4aSElliott Hughes samples=None, 575*e1fe3e4aSElliott Hughes glyphs=None, 576*e1fe3e4aSElliott Hughes designLimits=None, 577*e1fe3e4aSElliott Hughes pins=None, 578*e1fe3e4aSElliott Hughes sanitize=False, 579*e1fe3e4aSElliott Hughes): 580*e1fe3e4aSElliott Hughes """Plan a width (`wdth`) axis. 581*e1fe3e4aSElliott Hughes 582*e1fe3e4aSElliott Hughes widths: A list of width values (percentages) to plan for. If None, the default 583*e1fe3e4aSElliott Hughes values are used. 584*e1fe3e4aSElliott Hughes 585*e1fe3e4aSElliott Hughes This function simply calls planAxis with values=widths, and the appropriate 586*e1fe3e4aSElliott Hughes arguments. See documenation for planAxis for more information. 587*e1fe3e4aSElliott Hughes """ 588*e1fe3e4aSElliott Hughes 589*e1fe3e4aSElliott Hughes if widths is None: 590*e1fe3e4aSElliott Hughes widths = WIDTHS 591*e1fe3e4aSElliott Hughes 592*e1fe3e4aSElliott Hughes return planAxis( 593*e1fe3e4aSElliott Hughes measureWidth, 594*e1fe3e4aSElliott Hughes normalizeLinear, 595*e1fe3e4aSElliott Hughes interpolateLinear, 596*e1fe3e4aSElliott Hughes glyphSetFunc, 597*e1fe3e4aSElliott Hughes "wdth", 598*e1fe3e4aSElliott Hughes axisLimits, 599*e1fe3e4aSElliott Hughes values=widths, 600*e1fe3e4aSElliott Hughes samples=samples, 601*e1fe3e4aSElliott Hughes glyphs=glyphs, 602*e1fe3e4aSElliott Hughes designLimits=designLimits, 603*e1fe3e4aSElliott Hughes pins=pins, 604*e1fe3e4aSElliott Hughes sanitizeFunc=sanitizeWidth if sanitize else None, 605*e1fe3e4aSElliott Hughes ) 606*e1fe3e4aSElliott Hughes 607*e1fe3e4aSElliott Hughes 608*e1fe3e4aSElliott Hughesdef planSlantAxis( 609*e1fe3e4aSElliott Hughes glyphSetFunc, 610*e1fe3e4aSElliott Hughes axisLimits, 611*e1fe3e4aSElliott Hughes slants=None, 612*e1fe3e4aSElliott Hughes samples=None, 613*e1fe3e4aSElliott Hughes glyphs=None, 614*e1fe3e4aSElliott Hughes designLimits=None, 615*e1fe3e4aSElliott Hughes pins=None, 616*e1fe3e4aSElliott Hughes sanitize=False, 617*e1fe3e4aSElliott Hughes): 618*e1fe3e4aSElliott Hughes """Plan a slant (`slnt`) axis. 619*e1fe3e4aSElliott Hughes 620*e1fe3e4aSElliott Hughes slants: A list slant angles to plan for. If None, the default 621*e1fe3e4aSElliott Hughes values are used. 622*e1fe3e4aSElliott Hughes 623*e1fe3e4aSElliott Hughes This function simply calls planAxis with values=slants, and the appropriate 624*e1fe3e4aSElliott Hughes arguments. See documenation for planAxis for more information. 625*e1fe3e4aSElliott Hughes """ 626*e1fe3e4aSElliott Hughes 627*e1fe3e4aSElliott Hughes if slants is None: 628*e1fe3e4aSElliott Hughes slants = SLANTS 629*e1fe3e4aSElliott Hughes 630*e1fe3e4aSElliott Hughes return planAxis( 631*e1fe3e4aSElliott Hughes measureSlant, 632*e1fe3e4aSElliott Hughes normalizeDegrees, 633*e1fe3e4aSElliott Hughes interpolateLinear, 634*e1fe3e4aSElliott Hughes glyphSetFunc, 635*e1fe3e4aSElliott Hughes "slnt", 636*e1fe3e4aSElliott Hughes axisLimits, 637*e1fe3e4aSElliott Hughes values=slants, 638*e1fe3e4aSElliott Hughes samples=samples, 639*e1fe3e4aSElliott Hughes glyphs=glyphs, 640*e1fe3e4aSElliott Hughes designLimits=designLimits, 641*e1fe3e4aSElliott Hughes pins=pins, 642*e1fe3e4aSElliott Hughes sanitizeFunc=sanitizeSlant if sanitize else None, 643*e1fe3e4aSElliott Hughes ) 644*e1fe3e4aSElliott Hughes 645*e1fe3e4aSElliott Hughes 646*e1fe3e4aSElliott Hughesdef planOpticalSizeAxis( 647*e1fe3e4aSElliott Hughes glyphSetFunc, 648*e1fe3e4aSElliott Hughes axisLimits, 649*e1fe3e4aSElliott Hughes sizes=None, 650*e1fe3e4aSElliott Hughes samples=None, 651*e1fe3e4aSElliott Hughes glyphs=None, 652*e1fe3e4aSElliott Hughes designLimits=None, 653*e1fe3e4aSElliott Hughes pins=None, 654*e1fe3e4aSElliott Hughes sanitize=False, 655*e1fe3e4aSElliott Hughes): 656*e1fe3e4aSElliott Hughes """Plan a optical-size (`opsz`) axis. 657*e1fe3e4aSElliott Hughes 658*e1fe3e4aSElliott Hughes sizes: A list of optical size values to plan for. If None, the default 659*e1fe3e4aSElliott Hughes values are used. 660*e1fe3e4aSElliott Hughes 661*e1fe3e4aSElliott Hughes This function simply calls planAxis with values=sizes, and the appropriate 662*e1fe3e4aSElliott Hughes arguments. See documenation for planAxis for more information. 663*e1fe3e4aSElliott Hughes """ 664*e1fe3e4aSElliott Hughes 665*e1fe3e4aSElliott Hughes if sizes is None: 666*e1fe3e4aSElliott Hughes sizes = SIZES 667*e1fe3e4aSElliott Hughes 668*e1fe3e4aSElliott Hughes return planAxis( 669*e1fe3e4aSElliott Hughes measureWeight, 670*e1fe3e4aSElliott Hughes normalizeLog, 671*e1fe3e4aSElliott Hughes interpolateLog, 672*e1fe3e4aSElliott Hughes glyphSetFunc, 673*e1fe3e4aSElliott Hughes "opsz", 674*e1fe3e4aSElliott Hughes axisLimits, 675*e1fe3e4aSElliott Hughes values=sizes, 676*e1fe3e4aSElliott Hughes samples=samples, 677*e1fe3e4aSElliott Hughes glyphs=glyphs, 678*e1fe3e4aSElliott Hughes designLimits=designLimits, 679*e1fe3e4aSElliott Hughes pins=pins, 680*e1fe3e4aSElliott Hughes ) 681*e1fe3e4aSElliott Hughes 682*e1fe3e4aSElliott Hughes 683*e1fe3e4aSElliott Hughesdef makeDesignspaceSnippet(axisTag, axisName, axisLimit, mapping): 684*e1fe3e4aSElliott Hughes """Make a designspace snippet for a single axis.""" 685*e1fe3e4aSElliott Hughes 686*e1fe3e4aSElliott Hughes designspaceSnippet = ( 687*e1fe3e4aSElliott Hughes ' <axis tag="%s" name="%s" minimum="%g" default="%g" maximum="%g"' 688*e1fe3e4aSElliott Hughes % ((axisTag, axisName) + axisLimit) 689*e1fe3e4aSElliott Hughes ) 690*e1fe3e4aSElliott Hughes if mapping: 691*e1fe3e4aSElliott Hughes designspaceSnippet += ">\n" 692*e1fe3e4aSElliott Hughes else: 693*e1fe3e4aSElliott Hughes designspaceSnippet += "/>" 694*e1fe3e4aSElliott Hughes 695*e1fe3e4aSElliott Hughes for key, value in mapping.items(): 696*e1fe3e4aSElliott Hughes designspaceSnippet += ' <map input="%g" output="%g"/>\n' % (key, value) 697*e1fe3e4aSElliott Hughes 698*e1fe3e4aSElliott Hughes if mapping: 699*e1fe3e4aSElliott Hughes designspaceSnippet += " </axis>" 700*e1fe3e4aSElliott Hughes 701*e1fe3e4aSElliott Hughes return designspaceSnippet 702*e1fe3e4aSElliott Hughes 703*e1fe3e4aSElliott Hughes 704*e1fe3e4aSElliott Hughesdef addEmptyAvar(font): 705*e1fe3e4aSElliott Hughes """Add an empty `avar` table to the font.""" 706*e1fe3e4aSElliott Hughes font["avar"] = avar = newTable("avar") 707*e1fe3e4aSElliott Hughes for axis in fvar.axes: 708*e1fe3e4aSElliott Hughes avar.segments[axis.axisTag] = {} 709*e1fe3e4aSElliott Hughes 710*e1fe3e4aSElliott Hughes 711*e1fe3e4aSElliott Hughesdef processAxis( 712*e1fe3e4aSElliott Hughes font, 713*e1fe3e4aSElliott Hughes planFunc, 714*e1fe3e4aSElliott Hughes axisTag, 715*e1fe3e4aSElliott Hughes axisName, 716*e1fe3e4aSElliott Hughes values, 717*e1fe3e4aSElliott Hughes samples=None, 718*e1fe3e4aSElliott Hughes glyphs=None, 719*e1fe3e4aSElliott Hughes designLimits=None, 720*e1fe3e4aSElliott Hughes pins=None, 721*e1fe3e4aSElliott Hughes sanitize=False, 722*e1fe3e4aSElliott Hughes plot=False, 723*e1fe3e4aSElliott Hughes): 724*e1fe3e4aSElliott Hughes """Process a single axis.""" 725*e1fe3e4aSElliott Hughes 726*e1fe3e4aSElliott Hughes axisLimits = None 727*e1fe3e4aSElliott Hughes for axis in font["fvar"].axes: 728*e1fe3e4aSElliott Hughes if axis.axisTag == axisTag: 729*e1fe3e4aSElliott Hughes axisLimits = axis 730*e1fe3e4aSElliott Hughes break 731*e1fe3e4aSElliott Hughes if axisLimits is None: 732*e1fe3e4aSElliott Hughes return "" 733*e1fe3e4aSElliott Hughes axisLimits = (axisLimits.minValue, axisLimits.defaultValue, axisLimits.maxValue) 734*e1fe3e4aSElliott Hughes 735*e1fe3e4aSElliott Hughes log.info("Planning %s axis.", axisName) 736*e1fe3e4aSElliott Hughes 737*e1fe3e4aSElliott Hughes if "avar" in font: 738*e1fe3e4aSElliott Hughes existingMapping = font["avar"].segments[axisTag] 739*e1fe3e4aSElliott Hughes font["avar"].segments[axisTag] = {} 740*e1fe3e4aSElliott Hughes else: 741*e1fe3e4aSElliott Hughes existingMapping = None 742*e1fe3e4aSElliott Hughes 743*e1fe3e4aSElliott Hughes if values is not None and isinstance(values, str): 744*e1fe3e4aSElliott Hughes values = [float(w) for w in values.split()] 745*e1fe3e4aSElliott Hughes 746*e1fe3e4aSElliott Hughes if designLimits is not None and isinstance(designLimits, str): 747*e1fe3e4aSElliott Hughes designLimits = [float(d) for d in options.designLimits.split(":")] 748*e1fe3e4aSElliott Hughes assert ( 749*e1fe3e4aSElliott Hughes len(designLimits) == 3 750*e1fe3e4aSElliott Hughes and designLimits[0] <= designLimits[1] <= designLimits[2] 751*e1fe3e4aSElliott Hughes ) 752*e1fe3e4aSElliott Hughes else: 753*e1fe3e4aSElliott Hughes designLimits = None 754*e1fe3e4aSElliott Hughes 755*e1fe3e4aSElliott Hughes if pins is not None and isinstance(pins, str): 756*e1fe3e4aSElliott Hughes newPins = {} 757*e1fe3e4aSElliott Hughes for pin in pins.split(): 758*e1fe3e4aSElliott Hughes before, after = pin.split(":") 759*e1fe3e4aSElliott Hughes newPins[float(before)] = float(after) 760*e1fe3e4aSElliott Hughes pins = newPins 761*e1fe3e4aSElliott Hughes del newPins 762*e1fe3e4aSElliott Hughes 763*e1fe3e4aSElliott Hughes mapping, mappingNormalized = planFunc( 764*e1fe3e4aSElliott Hughes font.getGlyphSet, 765*e1fe3e4aSElliott Hughes axisLimits, 766*e1fe3e4aSElliott Hughes values, 767*e1fe3e4aSElliott Hughes samples=samples, 768*e1fe3e4aSElliott Hughes glyphs=glyphs, 769*e1fe3e4aSElliott Hughes designLimits=designLimits, 770*e1fe3e4aSElliott Hughes pins=pins, 771*e1fe3e4aSElliott Hughes sanitize=sanitize, 772*e1fe3e4aSElliott Hughes ) 773*e1fe3e4aSElliott Hughes 774*e1fe3e4aSElliott Hughes if plot: 775*e1fe3e4aSElliott Hughes from matplotlib import pyplot 776*e1fe3e4aSElliott Hughes 777*e1fe3e4aSElliott Hughes pyplot.plot( 778*e1fe3e4aSElliott Hughes sorted(mappingNormalized), 779*e1fe3e4aSElliott Hughes [mappingNormalized[k] for k in sorted(mappingNormalized)], 780*e1fe3e4aSElliott Hughes ) 781*e1fe3e4aSElliott Hughes pyplot.show() 782*e1fe3e4aSElliott Hughes 783*e1fe3e4aSElliott Hughes if existingMapping is not None: 784*e1fe3e4aSElliott Hughes log.info("Existing %s mapping:\n%s", axisName, pformat(existingMapping)) 785*e1fe3e4aSElliott Hughes 786*e1fe3e4aSElliott Hughes if mapping: 787*e1fe3e4aSElliott Hughes if "avar" not in font: 788*e1fe3e4aSElliott Hughes addEmptyAvar(font) 789*e1fe3e4aSElliott Hughes font["avar"].segments[axisTag] = mappingNormalized 790*e1fe3e4aSElliott Hughes else: 791*e1fe3e4aSElliott Hughes if "avar" in font: 792*e1fe3e4aSElliott Hughes font["avar"].segments[axisTag] = {} 793*e1fe3e4aSElliott Hughes 794*e1fe3e4aSElliott Hughes designspaceSnippet = makeDesignspaceSnippet( 795*e1fe3e4aSElliott Hughes axisTag, 796*e1fe3e4aSElliott Hughes axisName, 797*e1fe3e4aSElliott Hughes axisLimits, 798*e1fe3e4aSElliott Hughes mapping, 799*e1fe3e4aSElliott Hughes ) 800*e1fe3e4aSElliott Hughes return designspaceSnippet 801*e1fe3e4aSElliott Hughes 802*e1fe3e4aSElliott Hughes 803*e1fe3e4aSElliott Hughesdef main(args=None): 804*e1fe3e4aSElliott Hughes """Plan the standard axis mappings for a variable font""" 805*e1fe3e4aSElliott Hughes 806*e1fe3e4aSElliott Hughes if args is None: 807*e1fe3e4aSElliott Hughes import sys 808*e1fe3e4aSElliott Hughes 809*e1fe3e4aSElliott Hughes args = sys.argv[1:] 810*e1fe3e4aSElliott Hughes 811*e1fe3e4aSElliott Hughes from fontTools import configLogger 812*e1fe3e4aSElliott Hughes from fontTools.ttLib import TTFont 813*e1fe3e4aSElliott Hughes import argparse 814*e1fe3e4aSElliott Hughes 815*e1fe3e4aSElliott Hughes parser = argparse.ArgumentParser( 816*e1fe3e4aSElliott Hughes "fonttools varLib.avarPlanner", 817*e1fe3e4aSElliott Hughes description="Plan `avar` table for variable font", 818*e1fe3e4aSElliott Hughes ) 819*e1fe3e4aSElliott Hughes parser.add_argument("font", metavar="varfont.ttf", help="Variable-font file.") 820*e1fe3e4aSElliott Hughes parser.add_argument( 821*e1fe3e4aSElliott Hughes "-o", 822*e1fe3e4aSElliott Hughes "--output-file", 823*e1fe3e4aSElliott Hughes type=str, 824*e1fe3e4aSElliott Hughes help="Output font file name.", 825*e1fe3e4aSElliott Hughes ) 826*e1fe3e4aSElliott Hughes parser.add_argument( 827*e1fe3e4aSElliott Hughes "--weights", type=str, help="Space-separate list of weights to generate." 828*e1fe3e4aSElliott Hughes ) 829*e1fe3e4aSElliott Hughes parser.add_argument( 830*e1fe3e4aSElliott Hughes "--widths", type=str, help="Space-separate list of widths to generate." 831*e1fe3e4aSElliott Hughes ) 832*e1fe3e4aSElliott Hughes parser.add_argument( 833*e1fe3e4aSElliott Hughes "--slants", type=str, help="Space-separate list of slants to generate." 834*e1fe3e4aSElliott Hughes ) 835*e1fe3e4aSElliott Hughes parser.add_argument( 836*e1fe3e4aSElliott Hughes "--sizes", type=str, help="Space-separate list of optical-sizes to generate." 837*e1fe3e4aSElliott Hughes ) 838*e1fe3e4aSElliott Hughes parser.add_argument("--samples", type=int, help="Number of samples.") 839*e1fe3e4aSElliott Hughes parser.add_argument( 840*e1fe3e4aSElliott Hughes "-s", "--sanitize", action="store_true", help="Sanitize axis limits" 841*e1fe3e4aSElliott Hughes ) 842*e1fe3e4aSElliott Hughes parser.add_argument( 843*e1fe3e4aSElliott Hughes "-g", 844*e1fe3e4aSElliott Hughes "--glyphs", 845*e1fe3e4aSElliott Hughes type=str, 846*e1fe3e4aSElliott Hughes help="Space-separate list of glyphs to use for sampling.", 847*e1fe3e4aSElliott Hughes ) 848*e1fe3e4aSElliott Hughes parser.add_argument( 849*e1fe3e4aSElliott Hughes "--weight-design-limits", 850*e1fe3e4aSElliott Hughes type=str, 851*e1fe3e4aSElliott Hughes help="min:default:max in design units for the `wght` axis.", 852*e1fe3e4aSElliott Hughes ) 853*e1fe3e4aSElliott Hughes parser.add_argument( 854*e1fe3e4aSElliott Hughes "--width-design-limits", 855*e1fe3e4aSElliott Hughes type=str, 856*e1fe3e4aSElliott Hughes help="min:default:max in design units for the `wdth` axis.", 857*e1fe3e4aSElliott Hughes ) 858*e1fe3e4aSElliott Hughes parser.add_argument( 859*e1fe3e4aSElliott Hughes "--slant-design-limits", 860*e1fe3e4aSElliott Hughes type=str, 861*e1fe3e4aSElliott Hughes help="min:default:max in design units for the `slnt` axis.", 862*e1fe3e4aSElliott Hughes ) 863*e1fe3e4aSElliott Hughes parser.add_argument( 864*e1fe3e4aSElliott Hughes "--optical-size-design-limits", 865*e1fe3e4aSElliott Hughes type=str, 866*e1fe3e4aSElliott Hughes help="min:default:max in design units for the `opsz` axis.", 867*e1fe3e4aSElliott Hughes ) 868*e1fe3e4aSElliott Hughes parser.add_argument( 869*e1fe3e4aSElliott Hughes "--weight-pins", 870*e1fe3e4aSElliott Hughes type=str, 871*e1fe3e4aSElliott Hughes help="Space-separate list of before:after pins for the `wght` axis.", 872*e1fe3e4aSElliott Hughes ) 873*e1fe3e4aSElliott Hughes parser.add_argument( 874*e1fe3e4aSElliott Hughes "--width-pins", 875*e1fe3e4aSElliott Hughes type=str, 876*e1fe3e4aSElliott Hughes help="Space-separate list of before:after pins for the `wdth` axis.", 877*e1fe3e4aSElliott Hughes ) 878*e1fe3e4aSElliott Hughes parser.add_argument( 879*e1fe3e4aSElliott Hughes "--slant-pins", 880*e1fe3e4aSElliott Hughes type=str, 881*e1fe3e4aSElliott Hughes help="Space-separate list of before:after pins for the `slnt` axis.", 882*e1fe3e4aSElliott Hughes ) 883*e1fe3e4aSElliott Hughes parser.add_argument( 884*e1fe3e4aSElliott Hughes "--optical-size-pins", 885*e1fe3e4aSElliott Hughes type=str, 886*e1fe3e4aSElliott Hughes help="Space-separate list of before:after pins for the `opsz` axis.", 887*e1fe3e4aSElliott Hughes ) 888*e1fe3e4aSElliott Hughes parser.add_argument( 889*e1fe3e4aSElliott Hughes "-p", "--plot", action="store_true", help="Plot the resulting mapping." 890*e1fe3e4aSElliott Hughes ) 891*e1fe3e4aSElliott Hughes 892*e1fe3e4aSElliott Hughes logging_group = parser.add_mutually_exclusive_group(required=False) 893*e1fe3e4aSElliott Hughes logging_group.add_argument( 894*e1fe3e4aSElliott Hughes "-v", "--verbose", action="store_true", help="Run more verbosely." 895*e1fe3e4aSElliott Hughes ) 896*e1fe3e4aSElliott Hughes logging_group.add_argument( 897*e1fe3e4aSElliott Hughes "-q", "--quiet", action="store_true", help="Turn verbosity off." 898*e1fe3e4aSElliott Hughes ) 899*e1fe3e4aSElliott Hughes 900*e1fe3e4aSElliott Hughes options = parser.parse_args(args) 901*e1fe3e4aSElliott Hughes 902*e1fe3e4aSElliott Hughes configLogger( 903*e1fe3e4aSElliott Hughes level=("DEBUG" if options.verbose else "WARNING" if options.quiet else "INFO") 904*e1fe3e4aSElliott Hughes ) 905*e1fe3e4aSElliott Hughes 906*e1fe3e4aSElliott Hughes font = TTFont(options.font) 907*e1fe3e4aSElliott Hughes if not "fvar" in font: 908*e1fe3e4aSElliott Hughes log.error("Not a variable font.") 909*e1fe3e4aSElliott Hughes return 1 910*e1fe3e4aSElliott Hughes 911*e1fe3e4aSElliott Hughes if options.glyphs is not None: 912*e1fe3e4aSElliott Hughes glyphs = options.glyphs.split() 913*e1fe3e4aSElliott Hughes if ":" in options.glyphs: 914*e1fe3e4aSElliott Hughes glyphs = {} 915*e1fe3e4aSElliott Hughes for g in options.glyphs.split(): 916*e1fe3e4aSElliott Hughes if ":" in g: 917*e1fe3e4aSElliott Hughes glyph, frequency = g.split(":") 918*e1fe3e4aSElliott Hughes glyphs[glyph] = float(frequency) 919*e1fe3e4aSElliott Hughes else: 920*e1fe3e4aSElliott Hughes glyphs[g] = 1.0 921*e1fe3e4aSElliott Hughes else: 922*e1fe3e4aSElliott Hughes glyphs = None 923*e1fe3e4aSElliott Hughes 924*e1fe3e4aSElliott Hughes designspaceSnippets = [] 925*e1fe3e4aSElliott Hughes 926*e1fe3e4aSElliott Hughes designspaceSnippets.append( 927*e1fe3e4aSElliott Hughes processAxis( 928*e1fe3e4aSElliott Hughes font, 929*e1fe3e4aSElliott Hughes planWeightAxis, 930*e1fe3e4aSElliott Hughes "wght", 931*e1fe3e4aSElliott Hughes "Weight", 932*e1fe3e4aSElliott Hughes values=options.weights, 933*e1fe3e4aSElliott Hughes samples=options.samples, 934*e1fe3e4aSElliott Hughes glyphs=glyphs, 935*e1fe3e4aSElliott Hughes designLimits=options.weight_design_limits, 936*e1fe3e4aSElliott Hughes pins=options.weight_pins, 937*e1fe3e4aSElliott Hughes sanitize=options.sanitize, 938*e1fe3e4aSElliott Hughes plot=options.plot, 939*e1fe3e4aSElliott Hughes ) 940*e1fe3e4aSElliott Hughes ) 941*e1fe3e4aSElliott Hughes designspaceSnippets.append( 942*e1fe3e4aSElliott Hughes processAxis( 943*e1fe3e4aSElliott Hughes font, 944*e1fe3e4aSElliott Hughes planWidthAxis, 945*e1fe3e4aSElliott Hughes "wdth", 946*e1fe3e4aSElliott Hughes "Width", 947*e1fe3e4aSElliott Hughes values=options.widths, 948*e1fe3e4aSElliott Hughes samples=options.samples, 949*e1fe3e4aSElliott Hughes glyphs=glyphs, 950*e1fe3e4aSElliott Hughes designLimits=options.width_design_limits, 951*e1fe3e4aSElliott Hughes pins=options.width_pins, 952*e1fe3e4aSElliott Hughes sanitize=options.sanitize, 953*e1fe3e4aSElliott Hughes plot=options.plot, 954*e1fe3e4aSElliott Hughes ) 955*e1fe3e4aSElliott Hughes ) 956*e1fe3e4aSElliott Hughes designspaceSnippets.append( 957*e1fe3e4aSElliott Hughes processAxis( 958*e1fe3e4aSElliott Hughes font, 959*e1fe3e4aSElliott Hughes planSlantAxis, 960*e1fe3e4aSElliott Hughes "slnt", 961*e1fe3e4aSElliott Hughes "Slant", 962*e1fe3e4aSElliott Hughes values=options.slants, 963*e1fe3e4aSElliott Hughes samples=options.samples, 964*e1fe3e4aSElliott Hughes glyphs=glyphs, 965*e1fe3e4aSElliott Hughes designLimits=options.slant_design_limits, 966*e1fe3e4aSElliott Hughes pins=options.slant_pins, 967*e1fe3e4aSElliott Hughes sanitize=options.sanitize, 968*e1fe3e4aSElliott Hughes plot=options.plot, 969*e1fe3e4aSElliott Hughes ) 970*e1fe3e4aSElliott Hughes ) 971*e1fe3e4aSElliott Hughes designspaceSnippets.append( 972*e1fe3e4aSElliott Hughes processAxis( 973*e1fe3e4aSElliott Hughes font, 974*e1fe3e4aSElliott Hughes planOpticalSizeAxis, 975*e1fe3e4aSElliott Hughes "opsz", 976*e1fe3e4aSElliott Hughes "OpticalSize", 977*e1fe3e4aSElliott Hughes values=options.sizes, 978*e1fe3e4aSElliott Hughes samples=options.samples, 979*e1fe3e4aSElliott Hughes glyphs=glyphs, 980*e1fe3e4aSElliott Hughes designLimits=options.optical_size_design_limits, 981*e1fe3e4aSElliott Hughes pins=options.optical_size_pins, 982*e1fe3e4aSElliott Hughes sanitize=options.sanitize, 983*e1fe3e4aSElliott Hughes plot=options.plot, 984*e1fe3e4aSElliott Hughes ) 985*e1fe3e4aSElliott Hughes ) 986*e1fe3e4aSElliott Hughes 987*e1fe3e4aSElliott Hughes log.info("Designspace snippet:") 988*e1fe3e4aSElliott Hughes for snippet in designspaceSnippets: 989*e1fe3e4aSElliott Hughes if snippet: 990*e1fe3e4aSElliott Hughes print(snippet) 991*e1fe3e4aSElliott Hughes 992*e1fe3e4aSElliott Hughes if options.output_file is None: 993*e1fe3e4aSElliott Hughes outfile = makeOutputFileName(options.font, overWrite=True, suffix=".avar") 994*e1fe3e4aSElliott Hughes else: 995*e1fe3e4aSElliott Hughes outfile = options.output_file 996*e1fe3e4aSElliott Hughes if outfile: 997*e1fe3e4aSElliott Hughes log.info("Saving %s", outfile) 998*e1fe3e4aSElliott Hughes font.save(outfile) 999*e1fe3e4aSElliott Hughes 1000*e1fe3e4aSElliott Hughes 1001*e1fe3e4aSElliott Hughesif __name__ == "__main__": 1002*e1fe3e4aSElliott Hughes import sys 1003*e1fe3e4aSElliott Hughes 1004*e1fe3e4aSElliott Hughes sys.exit(main()) 1005