1
1
mirror of https://github.com/rsms/inter.git synced 2024-11-26 12:26:34 +03:00
inter/misc/tools/fontinfo.py

607 lines
18 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 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:
NOSET = '%d: SHOULD NOT BE SET'
head = sstructTableToDict(tt['head'], headFormat)
if 'macStyle' in head:
s = []
v = head['macStyle']
if isinstance(v, int): # uint16
if v & 0b0000000000000001: s.append('0: Bold')
if v & 0b0000000000000010: s.append('1: Italic')
if v & 0b0000000000000100: s.append('2: Underline')
if v & 0b0000000000001000: s.append('3: Outline')
if v & 0b0000000000010000: s.append('4: Shadow')
if v & 0b0000000000100000: s.append('5: Condensed')
if v & 0b0000000001000000: s.append('6: Extended')
# Bits 715: Reserved (set to 0)
if v & 0b0000000010000000: s.append(NOSET % 7)
if v & 0b0000000100000000: s.append(NOSET % 8)
if v & 0b0000001000000000: s.append(NOSET % 9)
if v & 0b0000010000000000: s.append(NOSET % 10)
if v & 0b0000100000000000: s.append(NOSET % 11)
if v & 0b0001000000000000: s.append(NOSET % 12)
if v & 0b0010000000000000: s.append(NOSET % 13)
if v & 0b0100000000000000: s.append(NOSET % 14)
if v & 0b1000000000000000: s.append(NOSET % 15)
head['macStyle_raw'] = head['macStyle']
head['macStyle'] = s
v = head['flags'] # uint16
if isinstance(v, int):
# https://docs.microsoft.com/en-us/typography/opentype/spec/head
s = []
if v & 0b0000000000000001: s.append('0: Baseline at y=0')
if v & 0b0000000000000010: s.append('1: Left sidebearing point at x=0')
if v & 0b0000000000000100: s.append('2: Instructions may depend on point size')
if v & 0b0000000000001000: s.append('3: Force ppem to integer values')
if v & 0b0000000000010000: s.append('4: Instructions may alter advance width')
# Bit 5: This bit is not used in OpenType, and should not be set in order to ensure
# compatible behavior on all platforms. If set, it may result in different behavior
# for vertical layout in some platforms. (See Apples specification for details
# regarding behavior in Apple platforms.)
if v & 0b0000000000100000: s.append(NOSET % 5)
# Bits 610 are not used in Opentype and should always be cleared
if v & 0b0000000001000000: s.append(NOSET % 6)
if v & 0b0000000010000000: s.append(NOSET % 7)
if v & 0b0000000100000000: s.append(NOSET % 8)
if v & 0b0000001000000000: s.append(NOSET % 9)
if v & 0b0000010000000000: s.append(NOSET % 10)
if v & 0b0000100000000000: s.append('11: Losslessly optimized')
if v & 0b0001000000000000: s.append('12: Converted')
if v & 0b0010000000000000: s.append('13: Optimized for ClearType')
if v & 0b0100000000000000: s.append('14: Last Resort font')
# Bit 15 is reserved
if v & 0b1000000000000000: s.append(NOSET % 15)
head['flags_raw'] = head['flags']
head['flags'] = 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
fsType = os2.get('fsType')
if fsType is not None:
obj = {"raw":fsType}
# Usage permissions: 0x000F
perm = fsType & 0x000F
perms = ""
if perm == 0:
perms = "Freely installable & embeddable"
elif perm == 2:
perms = "Restricted License embedding"
elif perm == 4:
perms = "Preview & Print embedding"
elif perm == 8:
perms = "Editable embedding"
else:
perms = "<INVALID VALUE %r>" % perm
obj['perm'] = '0x%04X: %s' % (perm, perms)
obj['no_subset'] = "no" if fsType & 0x0100 == 0 else "yes"
obj['bitmap_embed_only'] = "no" if fsType & 0x0200 == 0 else "yes"
os2['fsType'] = obj
fsSelection = os2.get('fsSelection')
if fsSelection is not None:
# https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fsselection
# Bit macStyle bit Symbolic name
# 0 1 ITALIC
# 1 UNDERSCORE
# 2 NEGATIVE
# 3 OUTLINED
# 4 STRIKEOUT
# 5 0 BOLD
# 6 REGULAR
# 7 USE_TYPO_METRICS
# 8 WWS
# 9 OBLIQUE
# 10-15 <reserved>
s = []
if fsSelection & 0b0000000000000001: s.append('0: ITALIC')
if fsSelection & 0b0000000000000010: s.append('1: UNDERSCORE')
if fsSelection & 0b0000000000000100: s.append('2: NEGATIVE')
if fsSelection & 0b0000000000001000: s.append('3: OUTLINED')
if fsSelection & 0b0000000000010000: s.append('4: STRIKEOUT')
if fsSelection & 0b0000000000100000: s.append('5: BOLD')
if fsSelection & 0b0000000010000000: s.append('6: REGULAR')
if fsSelection & 0b0000000100000000: s.append('7: USE_TYPO_METRICS')
if fsSelection & 0b0000001000000000: s.append('8: WWS')
if fsSelection & 0b0000010000000000: s.append('9: OBLIQUE')
os2['fsSelection_raw'] = fsSelection
os2['fsSelection'] = s
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
# rest of tables
for tname in tt.keys():
if tname not in info:
info[tname] = "[present but not decoded]"
# 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 != False or outputType is OUTPUT_TYPE_GLYPHLIST) and withGlyphs != '':
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()