1
1
mirror of https://github.com/rsms/inter.git synced 2024-11-24 05:30:41 +03:00
inter/misc/tools/fontinfo.py

513 lines
14 KiB
Python
Executable File

#!/usr/bin/env python
# encoding: utf8
#
# Generates JSON-encoded information about fonts
#
import os, sys
from os.path import dirname, basename, abspath, relpath, join as pjoin
sys.path.append(abspath(pjoin(dirname(__file__), 'tools')))
import common # for the side effeects
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]
if version.lower() == 'version':
version = v[1]
version = '.'.join([str(int(v)) for v in version.split('.')])
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).items():
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.items():
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()