ctai/3rdparty/MicroTeX/prebuilt/otf_clm.py
2025-03-07 22:11:10 +08:00

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])