Simple script: Find and complete real ascender and descender values in all open fonts

Hi there,

This script will find the highest and lowest Y values in all open fonts (typically a family) and use them to fill the OpenType specific font metrics values. Works in Fontlab + Robofab (and probably Robofont too).



#FLM: ReType Complete Real Asc/Desc V2.0
# Find and fill real asc and desc values in all open fonts 
from robofab.world import CurrentFont,AllFonts


max_Y_values = []
min_Y_values = []


for font in AllFonts():
	for x in font:
			max_Y_values.append(x.box[3])
			min_Y_values.append(x.box[1])

WinAscend = max(max_Y_values)
WinDescend = min(min_Y_values)
Ascender =	font.info.ascender 
Descender =	font.info.descender
Gap =  (Ascender + abs(Descender)) - (WinAscend + abs(WinDescend)) 

for font in AllFonts():
	font.info.openTypeOS2WinAscent = WinAscend
	font.info.openTypeOS2WinDescent = abs(WinDescend)
	font.info.openTypeOS2TypoAscender = Ascender
	font.info.openTypeOS2TypoDescender = Descender
	font.info.openTypeHheaAscender = WinAscend
	font.info.openTypeHheaDescender = -abs(WinDescend)
	font.info.openTypeHheaLineGap = 0
	font.info.openTypeOS2TypoLineGap = abs(Gap)

print
print "In all open fonts:"
print "The maximun Y value is: "+ str(max(max_Y_values))
print "The minimum Y value is: "+ str(min(min_Y_values))
print "TypoLineGap is: " + str(Gap) 

Comments

  • Hi Ramiro, maybe it would be good to leave out certain glyphs (like aringacute/Aringacute for example)?
    there is another script by @Jens Kutilek (based on Karsten Lücke’s Calculations) for calculating the vertical metrics as well:
    https://github.com/jenskutilek/RoboFont/blob/master/scripts/SetVerticalMetrics.py
  • Hi Lukas,
    I sort of forgot the 'Aringacute' issue  because I don't include it anymore in my fonts.
    But you are right, this situation should be addressed. Below is the same script but with a line that exclude 
    'Aringacute' from the list.  IMO, 'aringacute' is not usually a problem. I will check later Jens' script (surely way much better than mine).

    #FLM: ReType Complete Real Asc/Desc V2.1
    # Find and fill real asc and desc values in all open fonts 
    from robofab.world import CurrentFont,AllFonts
    
    
    max_Y_values = []
    min_Y_values = []
    
    for font in AllFonts():
    	for x in font:
    		if x.name != 'Aringacute':
    			max_Y_values.append(x.box[3])
    			min_Y_values.append(x.box[1])
    
    WinAscend = max(max_Y_values)
    WinDescend = min(min_Y_values)
    Ascender =	font.info.ascender 
    Descender =	font.info.descender
    Gap =  (Ascender + abs(Descender)) - (WinAscend + abs(WinDescend)) 
    
    for font in AllFonts():
    	font.info.openTypeOS2WinAscent = WinAscend
    	font.info.openTypeOS2WinDescent = abs(WinDescend)
    	font.info.openTypeOS2TypoAscender = Ascender
    	font.info.openTypeOS2TypoDescender = Descender
    	font.info.openTypeHheaAscender = WinAscend
    	font.info.openTypeHheaDescender = -abs(WinDescend)
    	font.info.openTypeHheaLineGap = 0
    	font.info.openTypeOS2TypoLineGap = abs(Gap)
    
    print
    print "In all open fonts:"
    print "The maximun Y value is: "+ str(max(max_Y_values))
    print "The minimum Y value is: "+ str(min(min_Y_values))
    print "TypoLineGap is: " + str(Gap) 

  • Vassil Kateliev
    Vassil Kateliev Posts: 56
    edited January 2017
    Here is a slightly better and a little more complex, de-robofabed script.
    It will also get dimensions of composite characters and utilizes a banList of omitted glyphs.

    #FLM: Set Vertical Metrics 1.2 
    #(based on ReType.com Complete Real Asc/Desc V2.1)
    # ------------------------
    # (C) Vassil Kateliev, 2016 (http://www.kateliev.com)
    
    # No warranties. By using this you agree
    # that you use it at your own risk!
    
    from FL import *
    
    # - Global configuration
    banList = 'Aring Abreve'.split(' ') # List of omitted glyphs
    workWithComponents = True # Process composite characters - non destructive
    
    # - Functions
    def getFontYBounds(font, banList, workWithComponents = False):
    	'''
    	Returns tuple(min_Y, max_Y) values for given [font] omitting glyph names (str) listed in [banList].
    	'''
    
    	def getYBounds(glyph):
    		return (glyph.GetBoundingRect().y, glyph.GetBoundingRect().height + glyph.GetBoundingRect().y)
    
    	fontMinY, fontMaxY = [], []
    
    	for gID in range(len(font)):
    		currGlyph = font[gID]
    		if currGlyph.name not in banList and not len(currGlyph.components):
    			yBounds = getYBounds(currGlyph)
    
    		elif currGlyph.name not in banList and len(currGlyph.components) and workWithComponents:
    			tempGlyph = Glyph(currGlyph)
    			currGlyph.Decompose() 			# Workaround! tempGlyph is an orphan so it could not be decomposed
    			yBounds = getYBounds(currGlyph)
    			currGlyph.Assign(tempGlyph) 	# Repair that mess!
    		
    		fontMinY.append(int(yBounds[0])); fontMaxY.append(int(yBounds[1]))
    
    	return (min(fontMinY), max(fontMaxY))
    
    # - Run	-------------------
    # - Get values
    fontBounds = zip(*[getFontYBounds(fl[fID], banList, workWithComponents) for fID in range(len(fl))])
    fontInfoBounds = zip(*[(fl[fID].descender[0], fl[fID].ascender[0]) for fID in range(len(fl))])
    
    WinAscend, WinDescend = max(fontBounds[1]), min(fontBounds[0])
    Ascender, Descender = max(fontInfoBounds[1]), min(fontInfoBounds[0])
    Gap = abs((Ascender + abs(Descender)) - (WinAscend + abs(WinDescend)))
    
    # - Process all open fonts
    for fID in range(len(fl)):
    	currentFont = fl[fID]
    
    	# - Report
    	print '\nProcessing:\t%s\n-----\nFont feature\tOLD\tNEW'  %currentFont.full_name
    	print 'WIN Ascent:\t%s \t%s' %(currentFont.ttinfo.os2_us_win_ascent, WinAscend)
    	print 'WIN Descent:\t%s \t%s' %(currentFont.ttinfo.os2_us_win_descent, abs(WinDescend))
    	print 'Typo Ascent:\t%s \t%s' %(currentFont.ttinfo.os2_s_typo_ascender, Ascender)
    	print 'Typo Descent:\t%s \t%s' %(currentFont.ttinfo.os2_s_typo_descender, Descender)
    	print 'Typo Line Gap:\t%s \t%s' %(currentFont.ttinfo.os2_s_typo_line_gap,Gap)
    	print 'HHEA Ascent\t%s \t%s' %(currentFont.ttinfo.hhea_ascender, WinAscend)
    	print 'HHEA Descent:\t%s \t%s' %(currentFont.ttinfo.hhea_descender, -abs(WinDescend))
    	print 'HHEA Line Gap:\t%s \t%s\n' %(currentFont.ttinfo.hhea_line_gap, 0)
    	
    	# - Process
    	currentFont.ttinfo.os2_us_win_ascent = WinAscend
    	currentFont.ttinfo.os2_us_win_descent = abs(WinDescend)
    
    	currentFont.ttinfo.os2_s_typo_ascender = Ascender
    	currentFont.ttinfo.os2_s_typo_descender = Descender
    	
    	currentFont.ttinfo.hhea_ascender = WinAscend
    	currentFont.ttinfo.hhea_descender = -abs(WinDescend)
    	
    	currentFont.ttinfo.hhea_line_gap = 0
    	currentFont.ttinfo.os2_s_typo_line_gap = Gap
    
    print 'End.'

  • Ramiro Espinoza
    Ramiro Espinoza Posts: 839
    edited January 2017
    The way I use this sort of scripts doesn't exclude the kind of planning Pablo and the rest are mentioning. It would be silly not to have an idea of the vertical boundaries of a family I have designed. But with a script you can quickly determined the highest and lowest points in the heaviest weights and see if everything is within the planned limits.
    And I frequently work on fonts from other designers and therefore need to asses the family to enter the correct values. I am not producing Vietnamese fonts and I don't need to care much about stacked diacritics.
  • Thanks for this gang! Quick way to port this to Glyphs?
  • […] when planning these values from the beginning, you should consider that you need to either allow for such things as Vietnamese stacked accents from the beginning […]
    I could be wrong, but wasn’t bit 7 (USE_TYPO_METRICS) in the in OS/2 table v.4 not specified to get rid of this problem, i.e., that one can use the largest WinAscent/Descent values to prevent clipping without influencing the line spacing?
  • Thanks for this gang! Quick way to port this to Glyphs?
    Quickest way is to buy Simon beer.
  • Thanks for this gang! Quick way to port this to Glyphs?
    Robofab scripts should run in Glyphs without modifications.
  • This script does not use RoboFab any more. I adapted it to run in Glyphs. I hope I didn't mess it up ;)
    #FLM: Set Vertical Metrics 1.2 
    #(based on ReType.com Complete Real Asc/Desc V2.1)
    # ------------------------
    # (C) Vassil Kateliev, 2016 (http://www.kateliev.com)
    # Adapted for Glyphs by Georg Seifert
    # No warranties. By using this you agree
    # that you use it at your own risk!
    
    import GlyphsApp
    
    # - Global configuration
    banList = 'Aring Abreve'.split(' ') # List of omitted glyphs
    workWithComponents = True # Process composite characters - non destructive
    
    # - Functions
    def getFontYBounds(font, banList, workWithComponents = False):
    	'''
    	Returns tuple(min_Y, max_Y) values for given [font] omitting glyph names (str) listed in [banList].
    	'''
    
    	def getYBounds(layer):
    		bounds = layer.bounds
    		return (NSMinY(bounds), NSMaxY(bounds))
    
    	fontMinY, fontMaxY = [], []
    	masterIDs = [m.id for m in font.masters]
    	
    	for currGlyph in font.glyphs:
    		if currGlyph.name not in banList:
    			for masterID in masterIDs:
    				currLayer = currGlyph.layers[masterID]
    				if not len(currLayer.components) or workWithComponents:
    					yBounds = getYBounds(currLayer)
    		
    		fontMinY.append(int(yBounds[0])); fontMaxY.append(int(yBounds[1]))
    
    	return (min(fontMinY), max(fontMaxY))
    
    # - Run	-------------------
    # - Get values
    fontBounds = []
    fontInfoBounds = []
    for currFont in Glyphs.fonts:
    	fontBounds.append(getFontYBounds(currFont, banList, workWithComponents))
    	for currMaster in currFont.masters:
    		fontInfoBounds.append((currMaster.descender, currMaster.ascender))
    fontBounds = zip(*fontBounds)
    fontInfoBounds = zip(*fontInfoBounds)
    
    WinAscend, WinDescend = max(fontBounds[1]), min(fontBounds[0])
    Ascender, Descender = max(fontInfoBounds[1]), min(fontInfoBounds[0])
    Gap = abs((Ascender + abs(Descender)) - (WinAscend + abs(WinDescend)))
    
    # - Process all open fonts
    for currFont in Glyphs.fonts:
    	
    	# - Report
    	print '\nProcessing:\t%s\n-----\nFont feature\tOLD\tNEW' % currFont.familyName
    	for currMaster in currFont.masters:
    		print 'Master:\t%s' % currMaster.name
    		if currMaster.customParameters["winAscent"]:
    			print '  WIN Ascent:\t%s \t%s' % (currMaster.customParameters["winAscent"], WinAscend)
    		if currMaster.customParameters["winDescent"]:
    			print '  WIN Descent:\t%s \t%s' % (currMaster.customParameters["winDescent"], WinDescend)
    
    		if currMaster.customParameters["typoAscender"]:
    			print '  Typo Ascent:\t%s \t%s' % (currMaster.customParameters["typoAscender"], Ascender)
    		else:
    			print '  Typo Ascent:\t%s \t%s' % (currMaster.ascender, Ascender)
    		if currMaster.customParameters["typoDescender"]:
    			print '  Typo Descent:\t%s \t%s' % (currMaster.customParameters["typoDescender"], Descender)
    		else:
    			print '  Typo Descent:\t%s \t%s' % (currMaster.descender, Descender)
    		if currMaster.customParameters["typoLineGap"]:
    			print '  Typo Line Gap:\t%s \t%s' % (currMaster.customParameters["typoLineGap"], Gap)
    		else:
    			print '  Typo Line Gap:\t%s \t%s' % (currFont.upm * 1.2 - (currMaster.ascender + currMaster.descender), Gap)
    		
    		if currMaster.customParameters["hheaAscender"]:
    			print '  HHEA Ascent\t%s \t%s' % (currMaster.customParameters["hheaAscender"], WinAscend)
    		if currMaster.customParameters["hheaDescender"]:
    			print '  HHEA Descent:\t%s \t%s' % (currMaster.customParameters["hheaDescender"], -abs(WinDescend))
    		if currMaster.customParameters["hheaLineGap"]:
    			print '  HHEA Line Gap:\t%s \t%s\n' % (currMaster.customParameters["hheaLineGap"], 0)
    	
    			# - Process
    		currMaster.customParameters["winAscent"] = WinAscend
    		currMaster.customParameters["winDescent"] = -abs(WinDescend)
    	
    		if abs(currMaster.ascender - Ascender) > 0.5:
    			currMaster.customParameters["typoAscender"] = Ascender
    		if abs(currMaster.descender - Descender) > 0.5:
    			currMaster.customParameters["typoDescender"] = Descender
    		if abs((currFont.upm * 1.2 - (currMaster.ascender + currMaster.descender)) - Gap) > 0.5:
    			currMaster.customParameters["typoLineGap"] = Gap
    		
    		currMaster.customParameters["hheaAscender"] = WinAscend
    		currMaster.customParameters["hheaDescender"] = -abs(WinDescend)
    		#currentFont.ttinfo.hhea_line_gap = 0
    		
    
    print 'End.'

  • Great work @Georg Seifert ! Love the tolerance check and adding MM functionality is a nice touch! (... makes me think about reworking my FL script to add MM)
  • Ramiro Espinoza
    Ramiro Espinoza Posts: 839
    edited January 2017
    I've made some changes in my original script so it can store more information about the highest and lowest Y values. Now the info is stored in dictionaries and I can retrieve the value's glyph and font name.
    Example:

    The maximun Y value is: 993 from Otilde in Guyot Text Black
    The minimum Y value is: -274 from Q in Guyot Text Black

    You surely can improve it but it works for me.

    #FLM: ReType Complete Real Asc/Desc V 2.0
    # Find and fill real asc and desc values in all open fonts
    import operator
    from robofab.world import AllFonts
    
    
    max_Y_values = {}
    min_Y_values = {}
    
    
    for font in AllFonts():
        for x in font:
            if x.name != 'Aringacute':
    			max_Y_values[x.name + " in " +  font.info.postscriptFullName] = x.box[3]
    			min_Y_values[x.name+ " in " +  font.info.postscriptFullName] = x.box[1]
    					
    
    WinAscend = max(max_Y_values.iteritems(), key=operator.itemgetter(1))[1]
    WinDescend = min(min_Y_values.iteritems(), key=operator.itemgetter(1))[1]
    Ascender = font.info.ascender
    Descender = font.info.descender
    Gap = (Ascender + abs(Descender)) - (WinAscend + abs(WinDescend))
    
    for font in AllFonts():
        font.info.openTypeOS2WinAscent = WinAscend
        font.info.openTypeOS2WinDescent = abs(WinDescend)
        font.info.openTypeOS2TypoAscender = Ascender
        font.info.openTypeOS2TypoDescender = Descender
        font.info.openTypeHheaAscender = WinAscend
        font.info.openTypeHheaDescender = -abs(WinDescend)
        font.info.openTypeHheaLineGap = 0
        font.info.openTypeOS2TypoLineGap = abs(Gap)
    
    print
    print "In all open fonts:"
    print
    print "The maximun Y value is: " + str(WinAscend) + " from " + max(max_Y_values.iteritems(), key=operator.itemgetter(1))[0]
    print "The minimum Y value is: " + str(WinDescend) + " from " + min(min_Y_values.iteritems(), key=operator.itemgetter(1))[0]
    print "TypoLineGap is: " + str(abs(Gap))
    
    


  • which of the strategies from https://www.glyphsapp.com/tutorials/vertical-metrics does this use?  Can it be made to work with the webfont strategy?
  • @Kristof Bastiaensen The strategy followed by my script is the 'Microsoft Strategy' described in your link but mine was taken from Karsten Lüecke's paper on font metrics: http://www.kltf.de/downloads/FontMetrics-kltf.pdf


    Regarding a webfont font metrics strategy, it is quite simple to implement in the script but I am not sure which should be the most advisable policy. I used to follow the recommendation I found on a Google's page at: https://code.google.com/archive/p/googlefontdirectory/wikis/HowToGenerateWebNativeFonts.wiki  but it's not available anymore and the recommendations were different to the one in the Glyphs' page.

    For my web fonts I usually follow the following strategy:

    First, calculate the winAscent and winDescent values, then:

    TypoAscender = winAscent
    TypoDescender = winDescent
    hheaAscender = winAscent
    hheaDescender = winDescent
    hheaLineGap = 0
    typoLineGap = 0

    So, question to everybody, what is currently the best font metric policy for web fonts?

  • Have a look at this thread: http://typedrawers.com/discussion/1705
  • Ramiro Espinoza
    Ramiro Espinoza Posts: 839
    edited January 2017
    Thanks! For what I see there is no consensus yet. Kristof, tell me what strategy you prefer and I will tweak the script to reproduce it (as soon as I have a little time).
  • Kristof Bastiaensen
    edited January 2017
    Ramiro, I have no idea, that's why I am asking :smile: I am trying to find the best strategy for a project that I am writing, so any info or algorithm is very useful!  I find there isn't much information about these metrics in the opentype spec, and the info that I found on the internet is often contradictory.
    EDIT: thanks for the links, they were what I wanted to know!
  • @Kristof Bastiaensen I would say: read the entries at http://typedrawers.com/discussion/1705 and pick the option that suit you best.