diff options
author | Rasmus Andersson <rasmus@notion.se> | 2018-09-03 22:55:49 +0300 |
---|---|---|
committer | Rasmus Andersson <rasmus@notion.se> | 2018-09-03 22:55:49 +0300 |
commit | c833e252c925e8dd68108660710ca835d95daa6f (patch) | |
tree | 6b2e28264ed45efd7f054e453b622098d0d875b8 /misc/tools/fontinfo.py | |
parent | 8c1a4c181ef12000179dfec541f1af87e9b03122 (diff) | |
download | inter-c833e252c925e8dd68108660710ca835d95daa6f.tar.xz |
Major overhaul, moving from UFO2 to Glyphs and UFO3, plus a brand new and much simpler fontbuild
Diffstat (limited to 'misc/tools/fontinfo.py')
-rwxr-xr-x | misc/tools/fontinfo.py | 506 |
1 files changed, 506 insertions, 0 deletions
diff --git a/misc/tools/fontinfo.py b/misc/tools/fontinfo.py new file mode 100755 index 000000000..0f406a14d --- /dev/null +++ b/misc/tools/fontinfo.py @@ -0,0 +1,506 @@ +#!/usr/bin/env python +# encoding: utf8 +# +# Generates JSON-encoded information about fonts +# +import os +import sys +import argparse +import json +import re +from base64 import b64encode + +from fontTools import ttLib +from fontTools.misc import sstruct +from fontTools.ttLib.tables._h_e_a_d import headFormat +from fontTools.ttLib.tables._h_h_e_a import hheaFormat +from fontTools.ttLib.tables._m_a_x_p import maxpFormat_0_5, maxpFormat_1_0_add +from fontTools.ttLib.tables._p_o_s_t import postFormat +from fontTools.ttLib.tables.O_S_2f_2 import OS2_format_1, OS2_format_2, OS2_format_5, panoseFormat +from fontTools.ttLib.tables._m_e_t_a import table__m_e_t_a +# from robofab.world import world, RFont, RGlyph, OpenFont, NewFont +# from robofab.objects.objectsRF import RFont, RGlyph, OpenFont, NewFont, RContour + +_NAME_IDS = {} + + +panoseWeights = [ + 'Any', # 0 + 'No Fit', # 1 + 'Very Light', # 2 + 'Light', # 3 + 'Thin', # 4 + 'Book', # 5 + 'Medium', # 6 + 'Demi', # 7 + 'Bold', # 8 + 'Heavy', # 9 + 'Black', # 10 + 'Extra Black', # 11 +] + +panoseProportion = [ + 'Any', # 0 + 'No fit', # 1 + 'Old Style/Regular', # 2 + 'Modern', # 3 + 'Even Width', # 4 + 'Extended', # 5 + 'Condensed', # 6 + 'Very Extended', # 7 + 'Very Condensed', # 8 + 'Monospaced', # 9 +] + +os2WidthClass = [ + None, + 'Ultra-condensed', # 1 + 'Extra-condensed', # 2 + 'Condensed', # 3 + 'Semi-condensed', # 4 + 'Medium (normal)', # 5 + 'Semi-expanded', # 6 + 'Expanded', # 7 + 'Extra-expanded', # 8 + 'Ultra-expanded', # 9 +] + +os2WeightClass = { + 100: 'Thin', + 200: 'Extra-light (Ultra-light)', + 300: 'Light', + 400: 'Normal (Regular)', + 500: 'Medium', + 600: 'Semi-bold (Demi-bold)', + 700: 'Bold', + 800: 'Extra-bold (Ultra-bold)', + 900: 'Black (Heavy)', +} + + +def num(s): + return int(s) if s.find('.') == -1 else float(s) + + +def tableNamesToDict(table, names): + t = {} + for name in names: + if name.find('reserved') == 0: + continue + t[name] = getattr(table, name) + return t + + +def sstructTableToDict(table, format): + _, names, _ = sstruct.getformat(format) + return tableNamesToDict(table, names) + + +OUTPUT_TYPE_COMPLETE = 'complete' +OUTPUT_TYPE_GLYPHLIST = 'glyphlist' + + +GLYPHS_TYPE_UNKNOWN = '?' +GLYPHS_TYPE_TT = 'tt' +GLYPHS_TYPE_CFF = 'cff' + +def getGlyphsType(tt): + if 'CFF ' in tt: + return GLYPHS_TYPE_CFF + elif 'glyf' in tt: + return GLYPHS_TYPE_TT + return GLYPHS_TYPE_UNKNOWN + + +class GlyphInfo: + def __init__(self, g, name, unicodes, type, glyphTable): + self._type = type # GLYPHS_TYPE_* + self._glyphTable = glyphTable + + self.name = name + self.width = g.width + self.lsb = g.lsb + self.unicodes = unicodes + + if g.height is not None: + self.tsb = g.tsb + self.height = g.height + else: + self.tsb = 0 + self.height = 0 + + self.numContours = 0 + self.contoursBBox = (0,0,0,0) # xMin, yMin, xMax, yMax + self.hasHints = False + + if self._type is GLYPHS_TYPE_CFF: + self._addCFFInfo() + elif self._type is GLYPHS_TYPE_TT: + self._addTTInfo() + + def _addTTInfo(self): + g = self._glyphTable[self.name] + self.numContours = g.numberOfContours + if g.numberOfContours: + self.contoursBBox = (g.xMin,g.xMin,g.xMax,g.yMax) + self.hasHints = hasattr(g, "program") + + def _addCFFInfo(self): + # TODO: parse CFF dict tree + pass + + @classmethod + def structKeys(cls, type): + v = [ + 'name', + 'unicodes', + 'width', + 'lsb', + 'height', + 'tsb', + 'hasHints', + ] + if type is GLYPHS_TYPE_TT: + v += ( + 'numContours', + 'contoursBBox', + ) + return v + + def structValues(self): + v = [ + self.name, + self.unicodes, + self.width, + self.lsb, + self.height, + self.tsb, + self.hasHints, + ] + if self._type is GLYPHS_TYPE_TT: + v += ( + self.numContours, + self.contoursBBox, + ) + return v + + +# exported convenience function +def GenGlyphList(font, withGlyphs=None): + if isinstance(font, str): + font = ttLib.TTFont(font) + return genGlyphsInfo(font, OUTPUT_TYPE_GLYPHLIST) + + +def genGlyphsInfo(tt, outputType, glyphsType=GLYPHS_TYPE_UNKNOWN, glyphsTable=None, withGlyphs=None): + unicodeMap = {} + + glyphnameFilter = None + if isinstance(withGlyphs, str): + glyphnameFilter = withGlyphs.split(',') + + if 'cmap' in tt: + # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html + bestCodeSubTable = None + bestCodeSubTableFormat = 0 + for st in tt['cmap'].tables: + if st.platformID == 0: # 0=unicode, 1=mac, 2=(reserved), 3=microsoft + if st.format > bestCodeSubTableFormat: + bestCodeSubTable = st + bestCodeSubTableFormat = st.format + for cp, glyphname in bestCodeSubTable.cmap.items(): + if glyphname in unicodeMap: + unicodeMap[glyphname].append(cp) + else: + unicodeMap[glyphname] = [cp] + + glyphValues = [] + + glyphnames = tt.getGlyphOrder() if glyphnameFilter is None else glyphnameFilter + + if outputType is OUTPUT_TYPE_GLYPHLIST: + glyphValues = [] + for glyphname in glyphnames: + v = [glyphname] + if glyphname in unicodeMap: + v += unicodeMap[glyphname] + glyphValues.append(v) + return glyphValues + + glyphset = tt.getGlyphSet(preferCFF=glyphsType is GLYPHS_TYPE_CFF) + + for glyphname in glyphnames: + unicodes = unicodeMap[glyphname] if glyphname in unicodeMap else [] + try: + g = glyphset[glyphname] + except KeyError: + raise Exception('no such glyph "'+glyphname+'"') + gi = GlyphInfo(g, glyphname, unicodes, glyphsType, glyphsTable) + glyphValues.append(gi.structValues()) + + return { + 'keys': GlyphInfo.structKeys(glyphsType), + 'values': glyphValues, + } + + +def copyDictEntry(srcD, srcName, dstD, dstName): + try: + dstD[dstName] = srcD[srcName] + except: + pass + + +def addCFFFontInfo(tt, info, cffTable): + d = cffTable.rawDict + + nameDict = None + if 'name' not in info: + nameDict = {} + info['name'] = nameDict + else: + nameDict = info['name'] + + copyDictEntry(d, 'Weight', nameDict, 'weight') + copyDictEntry(d, 'version', nameDict, 'version') + + +def genFontInfo(fontpath, outputType, withGlyphs=True): + tt = ttLib.TTFont(fontpath) # lazy=True + info = { + 'id': fontpath, + } + + # for tableName in tt.keys(): + # print 'table', tableName + + nameDict = {} + if 'name' in tt: + nameDict = {} + for rec in tt['name'].names: + k = _NAME_IDS[rec.nameID] if rec.nameID in _NAME_IDS else ('#%d' % rec.nameID) + nameDict[k] = rec.toUnicode() + if 'fontId' in nameDict: + info['id'] = nameDict['fontId'] + + if 'postscriptName' in nameDict: + info['name'] = nameDict['postscriptName'] + elif 'familyName' in nameDict: + info['name'] = nameDict['familyName'].replace(' ', '') + if 'subfamilyName' in nameDict: + info['name'] += '-' + nameDict['subfamilyName'].replace(' ', '') + + if 'version' in nameDict: + version = nameDict['version'] + v = re.split(r'[\s;]+', version) + if v and len(v) > 0: + version = v[0] + info['version'] = version + + if outputType is not OUTPUT_TYPE_GLYPHLIST: + if len(nameDict): + info['names'] = nameDict + + if 'head' in tt: + head = sstructTableToDict(tt['head'], headFormat) + if 'macStyle' in head: + s = [] + v = head['macStyle'] + if isinstance(v, int): + if v & 0b00000001: s.append('Bold') + if v & 0b00000010: s.append('Italic') + if v & 0b00000100: s.append('Underline') + if v & 0b00001000: s.append('Outline') + if v & 0b00010000: s.append('Shadow') + if v & 0b00100000: s.append('Condensed') + if v & 0b01000000: s.append('Extended') + head['macStyle_raw'] = head['macStyle'] + head['macStyle'] = s + info['head'] = head + + if 'hhea' in tt: + info['hhea'] = sstructTableToDict(tt['hhea'], hheaFormat) + + if 'post' in tt: + info['post'] = sstructTableToDict(tt['post'], postFormat) + + if 'OS/2' in tt: + t = tt['OS/2'] + os2 = None + if t.version == 1: + os2 = sstructTableToDict(t, OS2_format_1) + elif t.version in (2, 3, 4): + os2 = sstructTableToDict(t, OS2_format_2) + elif t.version == 5: + os2 = sstructTableToDict(t, OS2_format_5) + os2['usLowerOpticalPointSize'] /= 20 + os2['usUpperOpticalPointSize'] /= 20 + + if 'panose' in os2: + panose = {} + for k,v in sstructTableToDict(os2['panose'], panoseFormat).iteritems(): + if k[0:1] == 'b' and k[1].isupper(): + k = k[1].lower() + k[2:] + # bFooBar => fooBar + if k == 'weight' and isinstance(v, int) and v < len(panoseWeights): + panose['weightName'] = panoseWeights[v] + elif k == 'proportion' and isinstance(v, int) and v < len(panoseProportion): + panose['proportionName'] = panoseProportion[v] + panose[k] = v + os2['panose'] = panose + + if 'usWidthClass' in os2: + v = os2['usWidthClass'] + if isinstance(v, int) and v > 0 and v < len(os2WidthClass): + os2['usWidthClassName'] = os2WidthClass[v] + + if 'usWeightClass' in os2: + v = os2['usWeightClass'] + name = os2WeightClass.get(os2['usWeightClass']) + if name: + os2['usWeightClassName'] = name + + info['os/2'] = os2 + + if 'meta' in tt: + meta = {} + for k,v in tt['meta'].data.iteritems(): + try: + v.decode('utf8') + meta[k] = v + except: + meta[k] = 'data:;base64,' + b64encode(v) + info['meta'] = meta + + # if 'maxp' in tt: + # table = tt['maxp'] + # _, names, _ = sstruct.getformat(maxpFormat_0_5) + # if table.tableVersion != 0x00005000: + # _, names_1_0, _ = sstruct.getformat(maxpFormat_1_0_add) + # names += names_1_0 + # info['maxp'] = tableNamesToDict(table, names) + + glyphsType = getGlyphsType(tt) + glyphsTable = None + if glyphsType is GLYPHS_TYPE_CFF: + cff = tt["CFF "].cff + cffDictIndex = cff.topDictIndex + if len(cffDictIndex) > 1: + sys.stderr.write( + 'warning: multi-font CFF table is unsupported. Only reporting first table.\n' + ) + cffTable = cffDictIndex[0] + if outputType is not OUTPUT_TYPE_GLYPHLIST: + addCFFFontInfo(tt, info, cffTable) + elif glyphsType is GLYPHS_TYPE_TT: + glyphsTable = tt["glyf"] + # print 'glyphs type:', glyphsType, 'flavor:', tt.flavor, 'sfntVersion:', tt.sfntVersion + + if (withGlyphs is not False or outputType is OUTPUT_TYPE_GLYPHLIST) and withGlyphs is not '': + info['glyphs'] = genGlyphsInfo(tt, outputType, glyphsType, glyphsTable, withGlyphs) + + # sys.exit(1) + + return info + + +# ———————————————————————————————————————————————————————————————————————— +# main + +def main(): + argparser = argparse.ArgumentParser(description='Generate JSON describing fonts') + + argparser.add_argument('-out', dest='outfile', metavar='<file>', type=str, + help='Write JSON to <file>. Writes to stdout if not specified') + + argparser.add_argument('-pretty', dest='prettyJson', action='store_const', + const=True, default=False, + help='Generate pretty JSON with linebreaks and indentation') + + argparser.add_argument('-with-all-glyphs', dest='withGlyphs', action='store_const', + const=True, default=False, + help='Include glyph information on all glyphs.') + + argparser.add_argument('-with-glyphs', dest='withGlyphs', metavar='glyphname[,glyphname ...]', + type=str, + help='Include glyph information on specific glyphs') + + argparser.add_argument('-as-glyphlist', dest='asGlyphList', + action='store_const', const=True, default=False, + help='Only generate a list of glyphs and their unicode mappings.') + + argparser.add_argument('fontpaths', metavar='<path>', type=str, nargs='+', + help='TrueType or OpenType font files') + + args = argparser.parse_args() + + fonts = [] + outputType = OUTPUT_TYPE_COMPLETE + if args.asGlyphList: + outputType = OUTPUT_TYPE_GLYPHLIST + + n = 0 + for fontpath in args.fontpaths: + if n > 0: + # workaround for a bug in fontTools.misc.sstruct where it keeps a global + # internal cache that mixes up values for different fonts. + reload(sstruct) + font = genFontInfo(fontpath, outputType=outputType, withGlyphs=args.withGlyphs) + fonts.append(font) + n += 1 + + ostream = sys.stdout + if args.outfile is not None: + ostream = open(args.outfile, 'w') + + + if args.prettyJson: + json.dump(fonts, ostream, sort_keys=True, indent=2, separators=(',', ': ')) + sys.stdout.write('\n') + else: + json.dump(fonts, ostream, separators=(',', ':')) + + + if ostream is not sys.stdout: + ostream.close() + + + +# "name" table name identifiers +_NAME_IDS = { + # TrueType & OpenType + 0: 'copyright', + 1: 'familyName', + 2: 'subfamilyName', + 3: 'fontId', + 4: 'fullName', + 5: 'version', # e.g. 'Version <number>.<number>' + 6: 'postscriptName', + 7: 'trademark', + 8: 'manufacturerName', + 9: 'designer', + 10: 'description', + 11: 'vendorURL', + 12: 'designerURL', + 13: 'licenseDescription', + 14: 'licenseURL', + 15: 'RESERVED', + 16: 'typoFamilyName', + 17: 'typoSubfamilyName', + 18: 'macCompatibleFullName', # Mac only (FOND) + 19: 'sampleText', + + # OpenType + 20: 'postScriptCIDName', + 21: 'wwsFamilyName', + 22: 'wwsSubfamilyName', + 23: 'lightBackgoundPalette', + 24: 'darkBackgoundPalette', + 25: 'variationsPostScriptNamePrefix', + + # 26-255: Reserved for future expansion + # 256-32767: Font-specific names (layout features and settings, variations, track names, etc.) +} + +if __name__ == '__main__': + main() |