mirror of
https://github.com/rsms/inter.git
synced 2024-12-28 18:11:30 +03:00
512 lines
14 KiB
Python
Executable File
512 lines
14 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# encoding: utf8
|
|
#
|
|
# Generates JSON-encoded information about fonts
|
|
#
|
|
from __future__ import print_function
|
|
|
|
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]
|
|
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()
|