mirror of
https://github.com/rsms/inter.git
synced 2024-11-23 11:43:47 +03:00
607 lines
18 KiB
Python
Executable File
607 lines
18 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 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 7–15: 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 Apple’s specification for details
|
||
# regarding behavior in Apple platforms.)
|
||
if v & 0b0000000000100000: s.append(NOSET % 5)
|
||
# Bits 6–10 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()
|