summaryrefslogtreecommitdiff
path: root/misc/fontinfo.py
diff options
context:
space:
mode:
Diffstat (limited to 'misc/fontinfo.py')
-rwxr-xr-xmisc/fontinfo.py391
1 files changed, 391 insertions, 0 deletions
diff --git a/misc/fontinfo.py b/misc/fontinfo.py
new file mode 100755
index 000000000..47e2d66b1
--- /dev/null
+++ b/misc/fontinfo.py
@@ -0,0 +1,391 @@
+#!/usr/bin/env python
+# encoding: utf8
+#
+# Generates JSON-encoded information about fonts
+#
+import os
+import sys
+import argparse
+import json
+
+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
+# from robofab.world import world, RFont, RGlyph, OpenFont, NewFont
+# from robofab.objects.objectsRF import RFont, RGlyph, OpenFont, NewFont, RContour
+
+_NAME_IDS = {}
+
+
+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 = []
+ glyphset = tt.getGlyphSet(preferCFF=glyphsType is GLYPHS_TYPE_CFF)
+
+ 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
+
+ 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 outputType is not OUTPUT_TYPE_GLYPHLIST:
+ if len(nameDict):
+ info['names'] = nameDict
+
+ if 'head' in tt:
+ info['head'] = sstructTableToDict(tt['head'], headFormat)
+
+ 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']
+ if t.version == 1:
+ info['os/2'] = sstructTableToDict(t, OS2_format_1)
+ elif t.version in (2, 3, 4):
+ info['os/2'] = sstructTableToDict(t, OS2_format_2)
+ elif t.version == 5:
+ info['os/2'] = sstructTableToDict(t, OS2_format_5)
+ info['os/2']['usLowerOpticalPointSize'] /= 20
+ info['os/2']['usUpperOpticalPointSize'] /= 20
+ if 'panose' in info['os/2']:
+ del info['os/2']['panose']
+
+ # 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[font['id']] = 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=(',', ': '))
+ 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()