Code
from __future__ import print_function
import os
import sys
from collections import OrderedDict
from fontTools import ttLib
from fontTools.ttLib.tables._c_m_a_p import CmapSubtable
import bpy
import math
from mathutils import Vector
# insert path to font here
# otf and ttf only.
font_path = "C:\\Users\\Roblox\\Desktop\\dump-py\\SourceSansPro-Regular.otf"
KernFeatureTag = "kern"
cmapTableName = "cmap"
hmtxTableName = "hmtx"
headTableName = "head"
GPOSTableName = "GPOS"
# This is a rather... obscure codepoint. The only way to get the
# .notdef glyph (box with X in most fonts) to render is by having
# it render something that isn't in the font.
notdef_code = 78008
def is_included_codepoint(cp):
return cp >= 32 and cp <= 126
class MyLeftClass:
def __init__(self):
self.glyphs = []
self.class_1_record = 0
class MyRightClass:
def __init__(self):
self.glyphs = []
self.class_2_record = 0
def collect_unique_kern_lookup_list_indexes(feature_record):
unique_kern_lookup_index_list = []
for feat_rec_item in feature_record:
# print(feat_rec_item.FeatureTag)
# GPOS feature tags (e.g. kern, mark, mkmk, size) of each ScriptRecord
if feat_rec_item.FeatureTag == KernFeatureTag:
feature = feat_rec_item.Feature
for feat_lookup_item in feature.LookupListIndex:
if feat_lookup_item not in unique_kern_lookup_index_list:
unique_kern_lookup_index_list.append(feat_lookup_item)
return unique_kern_lookup_index_list
class OTFReader(object):
def __init__(self, fontPath):
self.font = ttLib.TTFont(fontPath)
self.kerning_pairs = {}
self.single_pairs = {}
self.class_pairs = {}
self.pair_pos_list = []
self.all_left_classes = {}
self.all_right_classes = {}
self.codepoint_to_glyph_map = {}
if GPOSTableName not in self.font:
print("The font has no %s table" % GPOSTableName, file=sys.stderr)
self.goodbye()
if cmapTableName not in self.font:
print("The font has no %s table" % cmapTableName, file=sys.stderr)
self.goodbye()
if hmtxTableName not in self.font:
print("The font has no %s table" % hmtxTableName, file=sys.stderr)
self.goodbye()
if headTableName not in self.font:
print("The font has no %s table" % headTableName, file=sys.stderr)
self.goodbye()
else:
self.analyze_font()
self.get_glyph_map()
self.find_kerning_lookups()
self.get_pair_pos()
self.get_single_pairs()
self.get_class_pairs()
def get_glyph_map(self):
outtables = []
for table in self.cmap.tables:
if table.format in [4, 12, 13, 14]:
outtables.append(table)
newtable = CmapSubtable.newSubtable(4)
newtable.platformID = table.platformID
newtable.platEncID = table.platEncID
newtable.language = table.language
newtable.cmap = table.cmap
newtable.format = table.format
outtables.append(newtable)
cmap_entry_to_use = None
best_subtable_format = 0
for table in outtables:
if table.platformID == 0 and table.format > best_subtable_format:
cmap_entry_to_use = table
best_subtable_format = table.format
for cp, glyphname in cmap_entry_to_use.cmap.items():
self.codepoint_to_glyph_map[cp] = glyphname
def goodbye(self):
print("The fun ends here.", file=sys.stderr)
return
def analyze_font(self):
self.cmap = self.font[cmapTableName]
self.hmtx_metrics = self.font[hmtxTableName].metrics
self.gpos_table = self.font[GPOSTableName].table
self.script_list = self.gpos_table.ScriptList
self.feature_list = self.gpos_table.FeatureList
self.feature_count = self.feature_list.FeatureCount
self.feature_record = self.feature_list.FeatureRecord
self.unique_kern_lookup_index_list = collect_unique_kern_lookup_list_indexes(
self.feature_record)
self.head = self.font[headTableName]
self.unitsPerEm = self.head.unitsPerEm
self.yMin = self.head.yMin
self.yMax = self.head.yMax
def find_kerning_lookups(self):
if not len(self.unique_kern_lookup_index_list):
print("The font has no %s feature." % KernFeatureTag, file=sys.stderr)
self.goodbye()
"LookupList:"
self.lookup_list = self.gpos_table.LookupList
self.lookups = []
for kernLookupIndex in sorted(self.unique_kern_lookup_index_list):
lookup = self.lookup_list.Lookup[kernLookupIndex]
# Confirm this is a GPOS LookupType 2; or
# using an extension table (GPOS LookupType 9):
"""
Lookup types:
1 Single adjustment Adjust position of a single glyph
2 Pair adjustment Adjust position of a pair of glyphs
3 Cursive attachment Attach cursive glyphs
4 MarkToBase attachment Attach a combining mark to a base glyph
5 MarkToLigature attachment Attach a combining mark to a ligature
6 MarkToMark attachment Attach a combining mark to another mark
7 Context positioning Position one or more glyphs in context
8 Chained Context positioning Position one or more glyphs in chained context
9 Extension positioning Extension mechanism for other positionings
10+ Reserved for future use
"""
if lookup.LookupType not in [2, 9]:
print("""
Info: GPOS LookupType %s found.
This type is neither a pair adjustment positioning lookup (GPOS LookupType 2),
nor using an extension table (GPOS LookupType 9), which are the only ones supported.
""" % lookup.LookupType, file=sys.stderr)
continue
self.lookups.append(lookup)
def get_pair_pos(self):
for lookup in self.lookups:
for subtable_item in lookup.SubTable:
if subtable_item.LookupType == 9: # extension table
if subtable_item.ExtensionLookupType == 8: # contextual
print("Contextual Kerning not (yet?) supported.", file=sys.stderr)
continue
elif subtable_item.ExtensionLookupType == 2:
subtable_item = subtable_item.ExtSubTable
# if subtable_item.Coverage.Format not in [1, 2]: # previous fontTools
if subtable_item.Format not in [1, 2]:
print("WARNING: Coverage format %d is not yet supported." % subtable_item.Coverage.Format, file=sys.stderr)
if subtable_item.ValueFormat1 not in [0, 4, 5]:
print("WARNING: ValueFormat1 format %d is not yet supported." % subtable_item.ValueFormat1, file=sys.stderr)
if subtable_item.ValueFormat2 not in [0]:
print("WARNING: ValueFormat2 format %d is not yet supported." % subtable_item.ValueFormat2, file=sys.stderr)
self.pair_pos_list.append(subtable_item)
# Each glyph in this list will have a corresponding PairSet
# which will contain all the second glyphs and the kerning
# value in the form of PairValueRecord(s)
# self.first_glyphsList.extend(subtable_item.Coverage.glyphs)
def get_single_pairs(self):
for pair_pos in self.pair_pos_list:
if pair_pos.Format == 1:
# single pair adjustment
first_glyphsList = pair_pos.Coverage.glyphs
# This iteration is done by index so we have a way
# to reference the first_glyphsList:
for pair_set_index, _ in enumerate(pair_pos.PairSet):
for pair_value_record_item in pair_pos.PairSet[pair_set_index].PairValueRecord:
second_glyph = pair_value_record_item.SecondGlyph
value_format = pair_pos.ValueFormat1
if value_format == 5: # RTL kerning
kern_value = "<%d 0 %d 0>" % (
pair_value_record_item.Value1.XPlacement,
pair_value_record_item.Value1.XAdvance)
elif value_format == 0: # RTL pair with value <0 0 0 0>
kern_value = "<0 0 0 0>"
elif value_format == 4: # LTR kerning
kern_value = pair_value_record_item.Value1.XAdvance
else:
print("\tValueFormat1 = %d" % value_format, file=sys.stdout)
continue # skip the rest
first_glyph = first_glyphsList[pair_set_index]
self.kerning_pairs[(
first_glyph, second_glyph)] = kern_value
self.single_pairs[(
first_glyph, second_glyph)] = kern_value
def get_class_pairs(self):
for loop, pair_pos in enumerate(self.pair_pos_list):
if pair_pos.Format == 2:
left_classes = {}
right_classes = {}
# Find left class with the Class1Record index="0".
# This first class is mixed into the "Coverage" table
# (e.g. all left glyphs) and has no class="X" property
# that is why we have to find the glyphs in that way.
lg0 = MyLeftClass()
# list of all glyphs kerned to the left of a pair:
all_left_glyphs = pair_pos.Coverage.glyphs
# list of all glyphs contained within left-sided kerning classes:
# allLeftClassGlyphs = pair_pos.ClassDef1.classDefs.keys()
single_glyphs = []
class_glyphs = []
for g_name, class_id in pair_pos.ClassDef1.classDefs.items():
if class_id == 0:
single_glyphs.append(g_name)
else:
class_glyphs.append(g_name)
# lg0.glyphs = list(set(all_left_glyphs) - set(allLeftClassGlyphs)) # coverage glyphs minus glyphs in a class (including class 0)
# coverage glyphs minus glyphs in real class (without class 0)
lg0.glyphs = list(set(all_left_glyphs) - set(class_glyphs))
lg0.glyphs.sort()
left_classes[lg0.class_1_record] = lg0
classname = "class_%s_%s" % (loop, lg0.class_1_record)
self.all_left_classes[classname] = lg0.glyphs
# Find all the remaining left classes:
for left_glyph in pair_pos.ClassDef1.classDefs:
class_1_record = pair_pos.ClassDef1.classDefs[left_glyph]
if class_1_record != 0: # this was the crucial line.
lg = MyLeftClass()
lg.class_1_record = class_1_record
left_classes.setdefault(
class_1_record, lg).glyphs.append(left_glyph)
self.all_left_classes.setdefault(
"class_%s_%s" % (loop, lg.class_1_record), lg.glyphs)
# Same for the right classes:
for right_glyph in pair_pos.ClassDef2.classDefs:
class_2_record = pair_pos.ClassDef2.classDefs[right_glyph]
rg = MyRightClass()
rg.class_2_record = class_2_record
right_classes.setdefault(
class_2_record, rg).glyphs.append(right_glyph)
self.all_right_classes.setdefault(
"class_%s_%s" % (loop, rg.class_2_record), rg.glyphs)
for record_l in left_classes:
for record_r in right_classes:
if pair_pos.Class1Record[record_l].Class2Record[record_r]:
value_format = pair_pos.ValueFormat1
if value_format in [4, 5]:
kern_value = pair_pos.Class1Record[record_l].Class2Record[record_r].Value1.XAdvance
elif value_format == 0:
# value_format zero is caused by a value of <0 0 0 0> on a class-class pair; skip these
continue
else:
print("\tValueFormat1 = %d" % value_format, file=sys.stdout)
continue # skip the rest
if kern_value != 0:
left_classname = "class_%s_%s" % (
loop, left_classes[record_l].class_1_record)
right_classname = "class_%s_%s" % (
loop, right_classes[record_r].class_2_record)
self.class_pairs[(
left_classname, right_classname)] = kern_value
for l in left_classes[record_l].glyphs:
for r in right_classes[record_r].glyphs:
if (l, r) in self.kerning_pairs:
# if the kerning pair has already been assigned in pair-to-pair kerning
continue
else:
if value_format == 5: # RTL kerning
kern_value = "<%d 0 %d 0>" % (
pair_pos.Class1Record[record_l].Class2Record[record_r].Value1.XPlacement, pair_pos.Class1Record[record_l].Class2Record[record_r].Value1.XAdvance)
self.kerning_pairs[(
l, r)] = kern_value
else:
print("ERROR", file=sys.stderr)
def get_center(o):
local_bbox_center = 0.125 * sum((Vector(b) for b in o.bound_box), Vector())
global_bbox_center = o.matrix_world * local_bbox_center
return global_bbox_center
if __name__ == "__main__":
assumedFontPath = font_path
if os.path.exists(assumedFontPath) and os.path.splitext(assumedFontPath)[1].lower() in [".otf", ".ttf"]:
fontPath = assumedFontPath
f = OTFReader(fontPath)
kerning_pairs = f.kerning_pairs
codepoint_to_glyph_map = f.codepoint_to_glyph_map
glyph_to_codepoint_map = {}
for codepoint, glyph in codepoint_to_glyph_map.items():
glyph_to_codepoint_map[glyph] = codepoint
# output lua files:
# kern.lua is a dump of kerning_table, containing then kerning between each character (non-contextual)
# cmap.lua is a dump of codepoint_to_glyph_map, containing the mapping of the codepoint to glyph (useful for naming)
# width.lua is a dump of width_table, containing the advance width of each codepoint
# width.lua is a dump of a few metrics. Namely, yMin and yMax.
with open("kern.lua", "w") as kern:
kerning_table = {}
for pair, kerning in f.kerning_pairs.items():
l, r = pair[0], pair[1]
try:
lcp, rcp = glyph_to_codepoint_map[l], glyph_to_codepoint_map[r]
if is_included_codepoint(lcp) and is_included_codepoint(rcp):
try:
kerning_table[l][r] = kerning
except KeyError:
kerning_table[l] = {r: kerning}
except KeyError:
pass
kern.write("return {\n")
for l, kernings in sorted(kerning_table.items()):
kern.write("\t[\"{}\"] = {{\n".format(l))
for r, kerning in sorted(kernings.items()):
kern.write("\t\t[\"{}\"] = {},\n".format(r, kerning))
kern.write("\t},\n")
kern.write("}")
with open("cmap.lua", "w") as cmap:
cmap.write("return {\n")
for codepoint, glyph in sorted(codepoint_to_glyph_map.items()):
if is_included_codepoint(codepoint):
cmap.write("\t[{}] = \"{}\",\n".format(codepoint, glyph))
cmap.write("}")
with open("width.lua", "w") as width:
width_table = {}
for glyph, metrics in f.hmtx_metrics.items():
try:
cp = glyph_to_codepoint_map[glyph]
if is_included_codepoint(cp):
width_table[glyph] = metrics[0]
except KeyError:
if glyph == ".notdef":
width_table[glyph] = metrics[0]
width.write("return {\n")
for glyph, width_n in sorted(width_table.items()):
width.write("\t[\"{}\"] = {},\n".format(glyph, width_n))
width.write("}")
with open("metrics.lua", "w") as metrics:
metrics.write("return {\n")
metrics.write("\tyMin = {},\n".format(f.yMin))
metrics.write("\tyMax = {},\n".format(f.yMax))
metrics.write("}")
offsets = {}
x_sizes = {}
y_sizes = {}
fnt = bpy.data.fonts.load(font_path)
# Dump the glyphs into blender.
# Manually select all and export w/ a scale of 0.01 afterwards. Roblox's mesh splitting will handle the rest.
for codepoint, _ in sorted(codepoint_to_glyph_map.items()):
if is_included_codepoint(codepoint):
bpy.ops.object.text_add()
tx = bpy.context.object
tx.data.font = fnt
tx.data.body = chr(codepoint)
tx.data.extrude = 0.5
glyph = codepoint_to_glyph_map[codepoint]
tx.name = "{}_{}".format(codepoint, glyph)
tx.data.name = "{}_{}".format(codepoint, glyph)
tx.data.resolution_u = 6
tx.rotation_euler[0] = math.radians(90)
tx.rotation_euler[2] = math.radians(180)
bpy.ops.object.modifier_add(type="EDGE_SPLIT")
offset_from_center = get_center(tx)
offsets[glyph] = offset_from_center
x_sizes[glyph] = tx.dimensions.x
y_sizes[glyph] = tx.dimensions.y
# Create .notdef glyph
bpy.ops.object.text_add()
tx = bpy.context.object
tx.data.font = fnt
tx.data.body = chr(notdef_code)
tx.data.extrude = 0.5
tx.name = ".notdef"
tx.data.name = ".notdef"
tx.data.resolution_u = 6
tx.rotation_euler[0] = math.radians(90)
tx.rotation_euler[2] = math.radians(180)
bpy.ops.object.modifier_add(type="EDGE_SPLIT")
offset_from_center = get_center(tx)
offsets[".notdef"] = offset_from_center
x_sizes[".notdef"] = tx.dimensions.x
y_sizes[".notdef"] = tx.dimensions.y
# dump a bunch of offset/size tables
with open("xOffsets.lua", "w") as xOffsets:
xOffsets.write("return {\n")
for glyph, offset in sorted(offsets.items()):
xOffsets.write(
"\t[\"{}\"] = {:.6f},\n".format(glyph, offset.x))
xOffsets.write("}")
with open("yOffsets.lua", "w") as yOffsets:
yOffsets.write("return {\n")
for glyph, offset in sorted(offsets.items()):
yOffsets.write(
"\t[\"{}\"] = {:.6f},\n".format(glyph, offset.z))
yOffsets.write("}")
with open("xSizes.lua", "w") as xSizes:
xSizes.write("return {\n")
for glyph, offset in sorted(offsets.items()):
xSizes.write("\t[\"{}\"] = {:.6f},\n".format(
glyph, x_sizes[glyph]))
xSizes.write("}")
with open("ySizes.lua", "w") as ySizes:
ySizes.write("return {\n")
for glyph, offset in sorted(offsets.items()):
ySizes.write("\t[\"{}\"] = {:.6f},\n".format(
glyph, y_sizes[glyph]))
ySizes.write("}")
else:
print("That is not a valid font.", file=sys.stderr)