508 lines
14 KiB
Python
508 lines
14 KiB
Python
#!/usr/bin/python2
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Parse open-type font file and convert to `clm` format file
|
|
|
|
import json
|
|
import sys
|
|
from functools import reduce, partial
|
|
import fontforge
|
|
import struct
|
|
|
|
|
|
def chain(*fs):
|
|
def _chain(f, g):
|
|
return lambda x: g(f(x))
|
|
return reduce(_chain, fs, lambda x: x)
|
|
|
|
|
|
def fmap(f, it):
|
|
return [j for i in it for j in f(i)]
|
|
|
|
|
|
def do(f, x):
|
|
f(x)
|
|
return x
|
|
|
|
|
|
def do_loop(f, xs):
|
|
for x in xs:
|
|
f(x)
|
|
return xs
|
|
|
|
|
|
def find_first(xs, predicate):
|
|
for x in xs:
|
|
if predicate(x):
|
|
return x
|
|
return None
|
|
|
|
|
|
def read_math_consts(font):
|
|
m = font.math
|
|
return [
|
|
m.ScriptPercentScaleDown,
|
|
m.ScriptScriptPercentScaleDown,
|
|
m.DelimitedSubFormulaMinHeight,
|
|
m.DisplayOperatorMinHeight,
|
|
m.MathLeading,
|
|
m.AxisHeight,
|
|
m.AccentBaseHeight,
|
|
m.FlattenedAccentBaseHeight,
|
|
m.SubscriptShiftDown,
|
|
m.SubscriptTopMax,
|
|
m.SubscriptBaselineDropMin,
|
|
m.SuperscriptShiftUp,
|
|
m.SuperscriptShiftUpCramped,
|
|
m.SuperscriptBottomMin,
|
|
m.SuperscriptBaselineDropMax,
|
|
m.SubSuperscriptGapMin,
|
|
m.SuperscriptBottomMaxWithSubscript,
|
|
m.SpaceAfterScript,
|
|
m.UpperLimitGapMin,
|
|
m.UpperLimitBaselineRiseMin,
|
|
m.LowerLimitGapMin,
|
|
m.LowerLimitBaselineDropMin,
|
|
m.StackTopShiftUp,
|
|
m.StackTopDisplayStyleShiftUp,
|
|
m.StackBottomShiftDown,
|
|
m.StackBottomDisplayStyleShiftDown,
|
|
m.StackGapMin,
|
|
m.StackDisplayStyleGapMin,
|
|
m.StretchStackTopShiftUp,
|
|
m.StretchStackBottomShiftDown,
|
|
m.StretchStackGapAboveMin,
|
|
m.StretchStackGapBelowMin,
|
|
m.FractionNumeratorShiftUp,
|
|
m.FractionNumeratorDisplayStyleShiftUp,
|
|
m.FractionDenominatorShiftDown,
|
|
m.FractionDenominatorDisplayStyleShiftDown,
|
|
m.FractionNumeratorGapMin,
|
|
m.FractionNumeratorDisplayStyleGapMin,
|
|
m.FractionRuleThickness,
|
|
m.FractionDenominatorGapMin,
|
|
m.FractionDenominatorDisplayStyleGapMin,
|
|
m.SkewedFractionHorizontalGap,
|
|
m.SkewedFractionVerticalGap,
|
|
m.OverbarVerticalGap,
|
|
m.OverbarRuleThickness,
|
|
m.OverbarExtraAscender,
|
|
m.UnderbarVerticalGap,
|
|
m.UnderbarRuleThickness,
|
|
m.UnderbarExtraDescender,
|
|
m.RadicalVerticalGap,
|
|
m.RadicalDisplayStyleVerticalGap,
|
|
m.RadicalRuleThickness,
|
|
m.RadicalExtraAscender,
|
|
m.RadicalKernBeforeDegree,
|
|
m.RadicalKernAfterDegree,
|
|
m.RadicalDegreeBottomRaisePercent,
|
|
m.MinConnectorOverlap
|
|
]
|
|
|
|
|
|
def read_metrics(glyph):
|
|
'''
|
|
Return a tuple to represents metrics, in (width, height, depth) order
|
|
'''
|
|
bounding_box = glyph.boundingBox()
|
|
return (
|
|
glyph.width,
|
|
bounding_box[2],
|
|
-bounding_box[1]
|
|
)
|
|
|
|
|
|
def read_variants(get_variants):
|
|
'''
|
|
Return a array of glyph names to represents variants
|
|
'''
|
|
variants = get_variants()
|
|
if not variants:
|
|
return []
|
|
return variants.split(' ')
|
|
|
|
|
|
def read_glyph_assembly(get_assembly):
|
|
'''
|
|
Return a tuple
|
|
(
|
|
italics_correction,
|
|
(
|
|
(glyph_name, flag, start_connector_length, end_connector_length, full_advance),
|
|
...
|
|
)
|
|
)
|
|
'''
|
|
return get_assembly()
|
|
|
|
|
|
def read_math_kern_record(glyph):
|
|
'''
|
|
Return a tuple contains 4 elements in clockwise direction, None means not represent
|
|
(
|
|
((correction_height, kerning), ...),
|
|
None,
|
|
...
|
|
)
|
|
'''
|
|
return (
|
|
glyph.mathKern.topLeft,
|
|
glyph.mathKern.topRight,
|
|
glyph.mathKern.bottomLeft,
|
|
glyph.mathKern.bottomRight,
|
|
)
|
|
|
|
|
|
def read_math(glyph):
|
|
return (
|
|
glyph.italicCorrection,
|
|
glyph.topaccent,
|
|
read_variants(lambda: glyph.horizontalVariants),
|
|
read_variants(lambda: glyph.verticalVariants),
|
|
read_glyph_assembly(lambda: (
|
|
glyph.horizontalComponentItalicCorrection,
|
|
glyph.horizontalComponents,
|
|
)),
|
|
read_glyph_assembly(lambda: (
|
|
glyph.verticalComponentItalicCorrection,
|
|
glyph.verticalComponents,
|
|
)),
|
|
read_math_kern_record(glyph),
|
|
)
|
|
|
|
|
|
def read_kern(glyph, kern_subtable_names):
|
|
'''
|
|
Return array of tuples represents kerning
|
|
[
|
|
(glyph_name, kerning),
|
|
...
|
|
]
|
|
'''
|
|
return chain(
|
|
partial(fmap, lambda subtable_name: glyph.getPosSub(subtable_name)),
|
|
# only care about horizontal kerning
|
|
partial(filter, lambda kern_info: kern_info[5] != 0),
|
|
partial(map, lambda kern_info: (kern_info[2], kern_info[5],))
|
|
)(kern_subtable_names)
|
|
|
|
|
|
def read_glyph(glyph, is_math_font, kern_subtable_names):
|
|
'''
|
|
Return a tuple
|
|
(
|
|
metrics,
|
|
kerning,
|
|
math
|
|
)
|
|
'''
|
|
return (
|
|
read_metrics(glyph),
|
|
read_kern(glyph, kern_subtable_names),
|
|
None if not is_math_font else read_math(glyph),
|
|
)
|
|
|
|
|
|
def _read_lookup_subtables(
|
|
font, is_target_lookup=lambda x: True, is_target_subtable=lambda x: True):
|
|
'''
|
|
Return a function to get the subtable names
|
|
'''
|
|
def _is_target_lookup(lookup_name):
|
|
lookup_info = font.getLookupInfo(lookup_name)
|
|
return lookup_info and is_target_lookup(lookup_info)
|
|
|
|
return chain(
|
|
partial(filter, _is_target_lookup),
|
|
partial(fmap, lambda lookup_name: font.getLookupSubtables(lookup_name)),
|
|
partial(filter, is_target_subtable)
|
|
)
|
|
|
|
|
|
def read_kern_subtables(font):
|
|
'''
|
|
Return array of kerning subtable name
|
|
'''
|
|
return _read_lookup_subtables(
|
|
font,
|
|
lambda lookup_info: lookup_info[0] == 'gpos_pair',
|
|
lambda subtable_name: not font.isKerningClass(subtable_name)
|
|
)(font.gpos_lookups)
|
|
|
|
|
|
def read_ligature_subtables(font):
|
|
'''
|
|
Return array of ligture subtable name
|
|
'''
|
|
return _read_lookup_subtables(
|
|
font,
|
|
lambda lookup_info: lookup_info[0] == 'gsub_ligature',
|
|
)(font.gsub_lookups)
|
|
|
|
|
|
def read_ligatures(glyph, liga_subtable_names):
|
|
'''
|
|
Return array of ligatures info represents by this glyph
|
|
[
|
|
((glyph_name, glyph_name, ...), glyph_id),
|
|
...
|
|
]
|
|
'''
|
|
get_ligas = chain(
|
|
partial(fmap, lambda subtable_name: glyph.getPosSub(subtable_name)),
|
|
partial(map, lambda liga_info: (liga_info[2:], glyph.originalgid,))
|
|
)
|
|
return get_ligas(liga_subtable_names)
|
|
|
|
|
|
def read_kerning_class(font):
|
|
'''
|
|
Return array of kerning class table:
|
|
(
|
|
# glyphs on left
|
|
(
|
|
None, # first is None
|
|
(glyph_name, glyph_name, ...),
|
|
...
|
|
),
|
|
# glyphs on right
|
|
(
|
|
None, # first is None
|
|
(glyph_name, glyph_name, ...),
|
|
...
|
|
),
|
|
# kerning value
|
|
(
|
|
kerning_value,
|
|
...
|
|
)
|
|
)
|
|
'''
|
|
get_tables = chain(
|
|
_read_lookup_subtables(
|
|
font,
|
|
lambda lookup_info: lookup_info[0] == 'gpos_pair',
|
|
lambda subtable_name: font.isKerningClass(subtable_name)
|
|
),
|
|
partial(map, lambda subtable_name: font.getKerningClass(subtable_name))
|
|
)
|
|
return get_tables(font.gpos_lookups)
|
|
|
|
|
|
def write_clm_unicode_glyph_map(f, unicode_glyph_map):
|
|
length = len(unicode_glyph_map)
|
|
f.write(struct.pack('!H', length))
|
|
sort_map = sorted(unicode_glyph_map, key=lambda x: x[0])
|
|
for (codepoint, glyph_id,) in sort_map:
|
|
f.write(struct.pack('!IH', codepoint, glyph_id))
|
|
|
|
|
|
def write_clm_kerning_class(f, kerning_classes, glyph_name_id_map):
|
|
length = len(kerning_classes)
|
|
f.write(struct.pack('!H', length))
|
|
|
|
# map the names to (glyph, index_in_classes)
|
|
write_classes = chain(
|
|
partial(
|
|
map,
|
|
lambda (i, xs,): map(lambda x: (x, i,), xs)
|
|
),
|
|
partial(
|
|
do,
|
|
lambda xs: f.write(struct.pack('!H', len(xs)))
|
|
),
|
|
partial(fmap, lambda x: x),
|
|
partial(
|
|
map,
|
|
lambda (name, index,): (glyph_name_id_map[name], index,)
|
|
),
|
|
partial(sorted, key=lambda x: x[0]),
|
|
partial(
|
|
do,
|
|
lambda xs: f.write(struct.pack('!H', len(xs)))
|
|
),
|
|
partial(
|
|
map,
|
|
lambda (glyph, index,): struct.pack('!HH', glyph, index)
|
|
),
|
|
partial(
|
|
do_loop,
|
|
lambda bs: f.write(bs)
|
|
)
|
|
)
|
|
|
|
for (left, right, value,) in kerning_classes:
|
|
write_classes(enumerate(left[1:]))
|
|
write_classes(enumerate(right[1:]))
|
|
column_count = len(right)
|
|
for i, v in enumerate(value):
|
|
if i < column_count or i % column_count == 0:
|
|
continue
|
|
f.write(struct.pack('!h', v))
|
|
|
|
|
|
def write_ligas(f, ligas, glyph_name_id_map):
|
|
forest = []
|
|
|
|
def add_node(glyphs, liga):
|
|
children = forest
|
|
child = None
|
|
for (glyph, char,) in glyphs:
|
|
child = find_first(children, lambda x: x['glyph'] == glyph)
|
|
if not child:
|
|
child = {'glyph': glyph, 'char': char,
|
|
'liga': -1, 'children': []}
|
|
children.append(child)
|
|
children = child['children']
|
|
child['liga'] = liga
|
|
|
|
for (chars, liga,) in ligas:
|
|
add_node(
|
|
map(lambda char: (glyph_name_id_map[char], char,), chars), liga)
|
|
|
|
def sort_children(children):
|
|
for child in children:
|
|
child['children'] = sort_children(child['children'])
|
|
return sorted(children, key=lambda x: x['glyph'])
|
|
|
|
forest = sort_children(forest)
|
|
|
|
def write_node(node):
|
|
f.write(struct.pack('!H', node['glyph']))
|
|
f.write(struct.pack('!i', node['liga']))
|
|
f.write(struct.pack('!H', len(node['children'])))
|
|
for child in node['children']:
|
|
write_node(child)
|
|
|
|
root = {'glyph': 0, 'char': 0, 'liga': -1, 'children': forest}
|
|
write_node(root)
|
|
|
|
|
|
def write_math_consts(f, consts):
|
|
for v in consts:
|
|
f.write(struct.pack('!h', v))
|
|
|
|
|
|
def write_glyphs(f, glyphs, glyph_name_id_map, is_math_font):
|
|
f.write(struct.pack('!H', len(glyphs)))
|
|
|
|
def write_metrics(metrics):
|
|
for v in metrics:
|
|
f.write(struct.pack('!h', v))
|
|
|
|
def write_kerns(kerns):
|
|
if not kerns:
|
|
f.write(struct.pack('!H', 0))
|
|
return
|
|
f.write(struct.pack('!H', len(kerns)))
|
|
sorted_kerns = chain(
|
|
partial(map, lambda x: (glyph_name_id_map[x[0]], x[1],)),
|
|
partial(sorted, key=lambda x: x[0])
|
|
)(kerns)
|
|
for (glyph, value,) in sorted_kerns:
|
|
f.write(struct.pack('!Hh', glyph, value))
|
|
|
|
def write_variants(variants):
|
|
length = 0 if not variants else len(variants)
|
|
f.write(struct.pack('!H', length))
|
|
if length == 0:
|
|
return
|
|
ids = map(lambda x: glyph_name_id_map[x], variants)
|
|
for i in ids:
|
|
f.write(struct.pack('!H', i))
|
|
|
|
def write_glyph_assembly(assembly):
|
|
if not assembly or not assembly[1]:
|
|
f.write(struct.pack('?', False))
|
|
return
|
|
f.write(struct.pack('?', True))
|
|
f.write(struct.pack('!H', len(assembly[1])))
|
|
f.write(struct.pack('!h', assembly[0])) # italics correction
|
|
for part in assembly[1]:
|
|
f.write(struct.pack('!H', glyph_name_id_map[part[0]]))
|
|
for v in part[1:]:
|
|
f.write(struct.pack('!H', v))
|
|
|
|
def write_math_kern(math_kerns):
|
|
for i in math_kerns:
|
|
if not i:
|
|
f.write(struct.pack('!H', 0))
|
|
continue
|
|
f.write(struct.pack('!H', len(i)))
|
|
for (correction_height, value,) in i:
|
|
f.write(struct.pack('!hh', correction_height, value))
|
|
|
|
def write_math(math):
|
|
f.write(struct.pack('!h', math[0])) # italics correction
|
|
f.write(struct.pack('!h', math[1])) # topaccent attachment
|
|
write_variants(math[2]) # horizontal variants
|
|
write_variants(math[3]) # vertical variants
|
|
write_glyph_assembly(math[4]) # horizontal assembly
|
|
write_glyph_assembly(math[5]) # vertical assembly
|
|
write_math_kern(math[6]) # math kern
|
|
|
|
for glyph in glyphs:
|
|
write_metrics(glyph[0])
|
|
write_kerns(glyph[1])
|
|
if is_math_font:
|
|
write_math(glyph[2])
|
|
|
|
|
|
def parse_otf(file_path, is_math_font, output_file_path):
|
|
font = fontforge.open(file_path)
|
|
|
|
# read math constants
|
|
math_consts = []
|
|
if is_math_font:
|
|
math_consts = read_math_consts(font)
|
|
|
|
# read kern subtables
|
|
kern_subtable_names = read_kern_subtables(font)
|
|
# read ligature subtables
|
|
liga_subtable_names = read_ligature_subtables(font)
|
|
# read kern class tables
|
|
kern_class_tables = read_kerning_class(font)
|
|
|
|
unicode_glyph_map = []
|
|
glyph_name_id_map = {}
|
|
glyphs = []
|
|
ligas = []
|
|
|
|
# read glyphs in GID order
|
|
for glyph_name in font:
|
|
glyph = font[glyph_name]
|
|
|
|
# unicode-glyph map
|
|
if glyph.unicode != -1:
|
|
unicode_glyph_map.append((glyph.unicode, glyph.originalgid,))
|
|
|
|
print(glyph.originalgid, glyph_name)
|
|
glyph_name_id_map[glyph_name] = glyph.originalgid
|
|
# glyph info
|
|
glyphs.append(read_glyph(glyph, is_math_font, kern_subtable_names))
|
|
|
|
# read ligature
|
|
liga_info = read_ligatures(glyph, liga_subtable_names)
|
|
for l in liga_info:
|
|
ligas.append(l)
|
|
|
|
em = font.em
|
|
xheight = font.xHeight
|
|
font.close()
|
|
|
|
with open(output_file_path, 'wb') as f:
|
|
f.write(struct.pack('?', is_math_font))
|
|
f.write(struct.pack('!H', em))
|
|
f.write(struct.pack('!H', xheight))
|
|
write_clm_unicode_glyph_map(f, unicode_glyph_map)
|
|
write_clm_kerning_class(f, kern_class_tables, glyph_name_id_map)
|
|
write_ligas(f, ligas, glyph_name_id_map)
|
|
if is_math_font:
|
|
write_math_consts(f, math_consts)
|
|
write_glyphs(f, glyphs, glyph_name_id_map, is_math_font)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parse_otf(sys.argv[1], sys.argv[2] == 'true', sys.argv[3])
|