Generating an avar table

In the variable font tutorial there is a beta note stating that instances should ideally line up with their associated weight class values, but this does not happen with the current implementation without importing your instances as masters.

I am wondering if support for the ‘avar’ table will be implemented in the future to solve this problem, effectively warping/mapping the design space so that the regular weight can actually sit at 400 on a weight axis, bold at 700, etc.?

I have had success with manually creating an ‘avar’ table and inserting it into my font once converted to .ttx to get weights to line up with their weight class values, but I am curious if this table will be supported when exporting from Glyphs App in the future.

It is on our list, but may still take a little.

Can you describe your steps in TTX? Asking because I could not get it to work myself.

Awesome, good to know!

Sure thing, I started with this as a reference for the xml structure of the table:


My workflow for this currently:

  • Set the weightClass values as custom parameters on the extreme masters so that the font exports with the correct scale min and max

  • Export the variable font and convert to ttx

  • Paste the sample ‘avar’ table before the ‘fvar’ table

  • I ran a script in the macro panel to calculate the values based on my current project (but I’m trying to figure out one to generate the table for any project with a single axis):

      font = Glyphs.font
    
      instanceIndex = 0.0
    
      for instance in font.instances:
      	if instance.active == True:
      		# scaled fvar weight based on a Min of 5 and a max of 254, difference = 249
      		scaledWeight = ((instance.weightValue - 5.0) / 249) * 900
      		
      		# toValue is the normalized value on the original scale (based on scaled weight)
      		toValue = (scaledWeight / 900)
      		
      		# css weight starting at 0 for Hairline and Thin at 100
      		cssWeight = (instanceIndex * 100)
      		
      		# fromValue is the normalized value on the post-mapping scale (100, 200, 300, etc.)
      		fromValue = (cssWeight / 900)
      		
      		# print values
      		print "Original weight = %f" % instance.weightValue
      		print "Scaled Weight = %f" % scaledWeight
      		print "CSS weight = %f" % cssWeight
      		print "avar to value = %f" % toValue
      		print "avar from value = %f" % fromValue
      		print "\n"
      		instanceIndex = instanceIndex + 1
    
  • I then copied and pasted the to and from values to my ‘avar’ table in the ttx

    • My final table for a weight axis from 0 to 900:

      <avar>
        <segment axis="wght">
          <mapping from="-1.0" to="-1.0"/>
          <mapping from="0.0" to="0.0"/>
          <mapping from="0.1111" to="0.0281"/>
          <mapping from="0.2222" to="0.0763"/>
          <mapping from="0.3333" to="0.1566"/>
          <mapping from="0.4444" to="0.2690"/>
          <mapping from="0.5556" to="0.3574"/>
          <mapping from="0.6667" to="0.4699"/>
          <mapping from="0.7778" to="0.5904"/>
          <mapping from="0.8889" to="0.7831"/>
          <mapping from="1.0" to="1.0"/>
        </segment>
      </avar>
      
  • Lastly I changed the instance values in the ‘fvar’ table to their intended weightClass. These numbers correspond to the post-avar-mapped scale and their original values at this point are useless


This is the way my from and to values are calculated:

  • If the instance is lighter than or equal to your default
    • to value is ((instanceWeight - axisMin) / (default - axisMin)) -1
    • from value is ((cssWeight - axisMin) / (defaultCss - axisMin)) -1
  • If the instance is heavier than or equal to the default:
    • to value is ((instanceWeight - default) / (axisMax - default))
    • from value is ((cssWeight - defaultCss) / (axisMax - defaultCss))

It’s worth noting that the normalized scale goes from -1 to 0 to 1 but these two segments are not necessarily equal. 0 is the default on the scale, so if your lightest master is the default then your map should be:

<mapping from="-1.0" to="-1.0"/>
<mapping from="0.0" to="0.0"/>
# intermediate instance mappings go here
<mapping from="1.0" to="1.0"/>
1 Like

Thanks, this is very helpful!

You’re welcome! I ended up rounding mine to the fourth decimal place because the is what I have seen elsewhere; the spec says these values are stored as “F2DOT14” but I am not entirely sure what that means

The OpenType Font File data types, including F2DOT14, are defined here: https://docs.microsoft.com/en-us/typography/opentype/spec/otff#data-types

Thanks for the link @composerjk!

The examples there show decimals with six digits after the decimal point. I am guessing this is the necessary number of digits to cover the full range of fractions from 0/16384 to 16383/16384 whereas four or five digits may result in multiple decimal values equating to the same fraction?

Asking because when inputing decimal values into ttx I would want them to convert properly and accurately

you only can make sure that you get the value that you enter is by round tripping them to F2DOT14 and back.

Apologies, my avar to and from equations I had detailed were incorrect. They were correct for a specific project with the axis minimum at 0 but upon revisiting this for another project I realized the calculations were not applicable at large.

I’ve edited my previous post so that they are now correct:

This is the way my from and to values are calculated:

  • If the instance is lighter than or equal to your default
    • to value is ((instanceWeight - axisMin) / (default - axisMin)) -1
    • from value is ((cssWeight - axisMin) / (defaultCss - axisMin)) -1
  • If the instance is heavier than or equal to the default:
    • to value is ((instanceWeight - default) / (axisMax - default))
    • from value is ((cssWeight - defaultCss) / (axisMax - defaultCss))

Additionally I created a macro that is generally applicable to other weight-axis-only projects based on the following assumptions:

  • Font has a custom parameter for “Variation Font Origin”

  • Masters are ordered thinnest to boldest

  • The extreme masters have the custom parameter “Axes Location”

  • Instances are ordered thinnest to boldest

  • Instances are named conventionally

      font = Glyphs.font
    
      cssMin = float(font.masters[0].customParameters['Axis Location'][0]['Location'])
      cssMax = float(font.masters[-1].customParameters['Axis Location'][0]['Location'])
      cssDefault = font.masters[font.customParameters['Variation Font Origin']].customParameters['Axis Location'][0]['Location']
      weightMin = font.instances[0].weightValue
      weightMax = font.instances[-1].weightValue
      weightDefault = font.masters[font.customParameters['Variation Font Origin']].weightValue
    
      # scales weight based on min and max declared in master custom parameters
      def convertWeight(inputWeight):
      	outputWeight = (((inputWeight - weightMin) / (weightMax-weightMin)) * (cssMax - cssMin)) + cssMin
      	return outputWeight
      	
    
      for instance in font.instances:
      	if instance.active == True:
      		scaledWeight = convertWeight(instance.weightValue)
    
      		
      		# css weight based on canonical naming
      		if instance.name  == "Thin":
      			cssWeight = 100
      		elif instance.name  == "ExtraLight" or instance.name == "UltraLight":
      			cssWeight = 200
      		elif instance.name  == "Light":
      			cssWeight = 300
      		elif instance.name  == "Regular":
      			cssWeight = 400
      		elif instance.name  == "Medium":
      			cssWeight = 500
      		elif instance.name  == "SemiBold":
      			cssWeight = 600
      		elif instance.name  == "Bold":
      			cssWeight = 700
      		elif instance.name  == "ExtraBold" or instance.name == "UltraBold":
      			cssWeight = 800
      		elif instance.name  == "Black" or instance.name == "Heavy":
      			cssWeight = 900
      		elif instance.name  == "Hairline":
      			cssWeight = 0
      		else:
      			cssWeight = 1000
      		
      		# toValue is the normalized value on the original scale (based on scaled weight)
      		# fromValue is the normalized value on the post-mapping scale (100, 200, 300, etc.)
      		if instance.weightValue <= weightDefault and weightMin != weightDefault:
      			toValue = ((scaledWeight - cssMin) / (convertWeight(weightDefault) - cssMin)) - 1
      			fromValue = ((cssWeight - cssMin) / (cssDefault - cssMin)) - 1
      		elif instance.weightValue >= weightDefault:
      			toValue = (scaledWeight - convertWeight(weightDefault)) / (cssMax - convertWeight(weightDefault))
      			fromValue = (cssWeight - cssDefault) / (cssMax - cssDefault)
      		
      		# print values
      		print instance.name
      		print "Original weight = %f" % instance.weightValue
      		print "Scaled Weight = %f" % scaledWeight
      		print "CSS weight = %f" % cssWeight
      		print "avar to value = %f" % toValue
      		print "avar from value = %f" % fromValue
      		print "\n"
1 Like

Hi, I have written two scripts (with my very limited knowledge) which tackle this. MakeWebVariable.py creates the AVAR table and converts all values (including the values in intermediate layers) into USWeightClass values. WriteAVAR.py is the basic version of that and only writes an AVAR table with the normal (stem) weight values already set in the file.

Let me know if this is what you’re looking for. Please also let me know if something is horribly wrong with them :slight_smile:

1 Like