Skip to content
Permalink
main
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
executable file 534 lines (452 sloc) 17.8 KB
#!/usr/bin/env python
#
# Copyright 2014 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Dumps the OpenType Layout tables in a font in a readable format."""
__author__ = "roozbeh@google.com (Roozbeh Pournader)"
import sys
from fontTools import ttLib
def internal_font_name(font):
"""Returns the font name according to the data in the name table."""
name_table = font["name"]
for record in name_table.names:
identifier = (record.nameID, record.platformID, record.platEncID, record.langID)
if identifier in [(4, 3, 1, 0x409), (4, 3, 0, 0x409)]:
return record.string.decode("UTF-16BE")
def print_indented(output_string, indents=1):
"""Prints a string indented with a specified number of spaces."""
print(" " * indents + output_string)
def printable_glyph_class(glyph_list, quote=False):
"""Returns a printable form for a class of glyphs."""
if quote:
suffix = "'"
else:
suffix = ""
if len(glyph_list) == 1:
return list(glyph_list)[0] + suffix
else:
return "[" + " ".join(glyph_list) + "]" + suffix
def printable_glyph_list(glyph_list, quote=False):
"""Returns a printable form for a list of glyphs."""
if quote:
suffix = "'"
else:
suffix = ""
if len(glyph_list) == 1:
return glyph_list[0] + suffix
else:
glyph_list = [glyph + suffix for glyph in glyph_list]
return " ".join(glyph_list)
def dump_lang_sys(script, lang, lang_sys):
"""Dumps a language system."""
print("%s %s:" % (script, lang),)
assert lang_sys.LookupOrder is None
if lang_sys.ReqFeatureIndex != 65535:
print("<%s>" % lang_sys.ReqFeatureIndex,)
print(lang_sys.FeatureIndex)
def extract_glyphs_from_coverage(coverage):
"""Return a list of glyphs from a coverage."""
if isinstance(coverage, str):
return [coverage]
else:
return coverage.glyphs
def print_contextual_substitution(backtrack, center, lookahead, subs, reverse=False):
"""Prints out a contextual substitution rule."""
if reverse:
rule_name = "rsub"
else:
rule_name = "sub"
if not subs:
rule_name = "ignore " + rule_name
output_list = []
for coverage in center:
glyphs = extract_glyphs_from_coverage(coverage)
output_list.append(printable_glyph_class(glyphs, quote=True))
if subs:
if isinstance(subs[0], str):
output_list.append("by")
output_list.append(printable_glyph_class(subs))
else:
records_added = 0
for record in subs:
output_list.insert(
record.SequenceIndex + 1 + records_added,
"lookup %d" % record.LookupListIndex,
)
records_added += 1
for coverage in backtrack:
glyphs = extract_glyphs_from_coverage(coverage)
output_list.insert(0, printable_glyph_class(glyphs))
for coverage in lookahead:
glyphs = extract_glyphs_from_coverage(coverage)
output_list.append(printable_glyph_class(glyphs))
print_indented("%s %s;" % (rule_name, " ".join(output_list)))
def dump_gsub_subtable(lookup_type, subtable):
"""Dumps a GSUB subtable."""
if lookup_type == 7:
# Extension. Decompile and pass down as another lookup type
lookup_type = subtable.ExtensionLookupType
print_indented("# (extension for %d)" % lookup_type)
ext_sub_table = subtable.ExtSubTable
subtable = ext_sub_table
# Proceed to the actual type
if lookup_type == 1:
for key in sorted(subtable.mapping.keys()):
print_indented("sub %s by %s;" % (key, subtable.mapping[key]))
elif lookup_type == 2:
for key, value in sorted(subtable.mapping.items()):
print_indented("sub %s by %s;" % (key, " ".join(value)))
elif lookup_type == 3:
for key in sorted(subtable.alternates.keys()):
print_indented(
"sub %s from %s;"
% (key, printable_glyph_class(subtable.alternates[key]))
)
elif lookup_type == 4:
for key in sorted(subtable.ligatures.keys()):
for ligature in subtable.ligatures[key]:
print_indented(
"sub %s by %s;"
% (
printable_glyph_list([key] + ligature.Component),
ligature.LigGlyph,
)
)
elif lookup_type == 5 and subtable.Format == 1:
for index in range(subtable.SubRuleSetCount):
glyph = subtable.Coverage.glyphs[index]
for subrule in subtable.SubRuleSet[index].SubRule:
whole_input = [glyph] + subrule.Input
print_contextual_substitution(
[], whole_input, [], subrule.SubstLookupRecord
)
elif lookup_type == 5 and subtable.Format == 2:
print_indented(
"type=%d format=%d not supported yet" % (lookup_type, subtable.Format)
)
# assert (max(subtable.ClassDef.classDefs.values())
# <= subtable.SubClassSetCount)
# assert len(subtable.SubClassSet) == subtable.SubClassSetCount
# print(subtable.Coverage.glyphs)
# print(subtable.ClassDef.classDefs)
# for index in range(subtable.SubClassSetCount):
# sub_class = subtable.SubClassSet[index]
# if sub_class:
# print(index, vars(sub_class))
# else:
# print(index, sub_class)
elif lookup_type == 5 and subtable.Format == 3:
print_contextual_substitution(
[], subtable.Coverage, [], subtable.SubstLookupRecord
)
elif lookup_type in [6, 8]:
is_reverse = lookup_type == 8
if subtable.Format == 1:
print_contextual_substitution(
subtable.BacktrackCoverage,
[subtable.Coverage],
subtable.LookAheadCoverage,
subtable.Substitute,
reverse=is_reverse,
)
elif subtable.Format == 3:
print_contextual_substitution(
subtable.BacktrackCoverage,
subtable.InputCoverage,
subtable.LookAheadCoverage,
subtable.SubstLookupRecord,
reverse=is_reverse,
)
else:
print_indented(
"# type=%d format=%d not supported yet" % (lookup_type, subtable.Format)
)
else:
print_indented(
"# type=%d format=%d not supported yet" % (lookup_type, subtable.Format)
)
# print(vars(subtable))
def printable_value_record(value_record):
"""Prints a GPOS ValueRecord."""
if value_record is None:
return "<NULL>"
if vars(value_record).keys() == ["XAdvance"]:
return "%d" % value_record.XAdvance
output_list = []
for key in ["XPlacement", "YPlacement", "XAdvance", "YAdvance"]:
output_list.append(vars(value_record).get(key, 0))
return "<%d %d %d %d>" % tuple(output_list)
def printable_device(device):
"""Returns a printable form of a device record."""
if device is None:
return "<device NULL>"
output_list = []
assert device.StartSize + len(device.DeltaValue) - 1 == device.EndSize
for index in range(len(device.DeltaValue)):
output_list.append(
"%d %d" % (device.StartSize + index, device.DeltaValue[index])
)
return "<device %s>" % (", ".join(output_list))
def printable_anchor(anchor):
"""Returns a printable form of an anchor."""
if anchor is None:
return "<anchor NULL>"
if anchor.Format == 1:
return "<anchor %d %d>" % (anchor.XCoordinate, anchor.YCoordinate)
elif anchor.Format == 2:
return "<anchor %d %d contourpoint %d>" % (
anchor.XCoordinate,
anchor.YCoordinate,
anchor.AnchorPoint,
)
elif anchor.Format == 3:
return "<anchor %d %d %s %s>" % (
anchor.XCoordinate,
anchor.YCoordinate,
printable_device(anchor.XDeviceTable),
printable_device(anchor.YDeviceTable),
)
else:
print(vars(anchor))
assert False, "don't know about anchor format"
def dump_marks(glyphs, records):
"""Prints marks with their classes."""
index = 0
for glyph in glyphs:
record = records[index]
print_indented(
"%s: class=%d %s"
% (glyph, record.Class, printable_anchor(record.MarkAnchor)),
indents=2,
)
index += 1
def dump_bases(glyphs, records, printable_function):
"""Prints bases with their classes."""
index = 0
for glyph in glyphs:
record = records[index]
print_indented("%s: %s" % (glyph, printable_function(record)), indents=2)
index += 1
def reverse_class_def(class_def_dict):
"""Reverses a ClassDef dictionary."""
reverse = {}
for key in class_def_dict:
value = class_def_dict[key]
try:
reverse[value].add(key)
except KeyError:
reverse[value] = {key}
return reverse
def dump_gpos_subtable(lookup_type, subtable):
"""Prints a GPOS subtable."""
if lookup_type == 9:
# Extension. Decompile and pass down as another lookup type
lookup_type = subtable.ExtensionLookupType
print_indented("# (extension for %d)" % lookup_type)
ext_sub_table = subtable.ExtSubTable
subtable = ext_sub_table
if lookup_type == 1 and subtable.Format == 1:
print_indented(
"position %s %s;"
% (
printable_glyph_class(subtable.Coverage.glyphs),
printable_value_record(subtable.Value),
)
)
elif lookup_type == 1 and subtable.Format == 2:
for index, value_record in enumerate(subtable.Value):
print_indented(
"position %s %s;"
% (
subtable.Coverage.glyphs[index],
printable_value_record(value_record),
)
)
elif lookup_type == 2 and subtable.Format == 1:
first_glyphs = subtable.Coverage.glyphs
for index, pair_set in enumerate(subtable.PairSet):
first_glyph = first_glyphs[index]
for pair_value_record in pair_set.PairValueRecord:
if pair_value_record.Value2 is None:
print_indented(
"position %s %s %s;"
% (
first_glyph,
pair_value_record.SecondGlyph,
printable_value_record(pair_value_record.Value1),
)
)
else:
print_indented(
"position %s %s %s %s;"
% (
first_glyph,
printable_value_record(pair_value_record.Value1),
pair_value_record.SecondGlyph,
printable_value_record(pair_value_record.Value2),
)
)
elif lookup_type == 2 and subtable.Format == 2:
class1_reverse = reverse_class_def(subtable.ClassDef1.classDefs)
class2_reverse = reverse_class_def(subtable.ClassDef2.classDefs)
for index1, class1_record in enumerate(subtable.Class1Record):
class1 = class1_reverse.get(index1)
if class1 is None:
continue
for index2, class2_record in enumerate(class1_record.Class2Record):
class2 = class2_reverse.get(index2)
if class2 is None:
continue
if class2_record.Value2 is None:
value_record1 = printable_value_record(class2_record.Value1)
if value_record1 == "0":
continue
print_indented(
"position %s %s %s;"
% (
printable_glyph_class(class1),
printable_glyph_class(class2),
value_record1,
)
)
else:
print_indented(
"position %s %s %s %s;"
% (
printable_glyph_class(class1),
printable_value_record(class2_record.Value1),
printable_glyph_class(class2),
printable_value_record(class2_record.Value2),
)
)
elif lookup_type == 3 and subtable.Format == 1:
for index, entry_exit_record in enumerate(subtable.EntryExitRecord):
print_indented(
"position cursive %s %s %s;"
% (
subtable.Coverage.glyphs[index],
printable_anchor(entry_exit_record.EntryAnchor),
printable_anchor(entry_exit_record.ExitAnchor),
)
)
elif lookup_type == 4:
print_indented("Mark:")
dump_marks(subtable.MarkCoverage.glyphs, subtable.MarkArray.MarkRecord)
print_indented("Base:")
dump_bases(
subtable.BaseCoverage.glyphs,
subtable.BaseArray.BaseRecord,
lambda record: " ".join(
[printable_anchor(anchor) for anchor in record.BaseAnchor]
),
)
elif lookup_type == 5:
print_indented("Mark:")
dump_marks(subtable.MarkCoverage.glyphs, subtable.MarkArray.MarkRecord)
def printable_ligature_attach(attach):
"""Output routine for LigatureAttach."""
output_list = []
for record in attach.ComponentRecord:
anchor_list = []
for anchor in record.LigatureAnchor:
anchor_list.append(printable_anchor(anchor))
output_list.append(" ".join(anchor_list))
return ", ".join(output_list)
print_indented("Liga:")
dump_bases(
subtable.LigatureCoverage.glyphs,
subtable.LigatureArray.LigatureAttach,
printable_ligature_attach,
)
elif lookup_type == 6:
print_indented("Mark1:")
dump_marks(subtable.Mark1Coverage.glyphs, subtable.Mark1Array.MarkRecord)
print_indented("Mark2:")
dump_bases(
subtable.Mark2Coverage.glyphs,
subtable.Mark2Array.Mark2Record,
lambda record: " ".join(
[printable_anchor(anchor) for anchor in record.Mark2Anchor]
),
)
elif lookup_type == 8 and subtable.Format == 3:
print_contextual_substitution(
subtable.BacktrackCoverage,
subtable.InputCoverage,
subtable.LookAheadCoverage,
subtable.PosLookupRecord,
)
else:
print_indented(
"type=%d format=%d not supported yet" % (lookup_type, subtable.Format)
)
def dump_script_record(script_record):
"""Prints out scripts records."""
for script in script_record:
script_tag = script.ScriptTag
default_lang_sys = script.Script.DefaultLangSys
if default_lang_sys:
dump_lang_sys(script_tag, "dflt", default_lang_sys)
for lang_sys_record in script.Script.LangSysRecord:
dump_lang_sys(
script_tag, lang_sys_record.LangSysTag, lang_sys_record.LangSys
)
def dump_feature_record(feature_record):
"""Prints out feature records."""
for index in range(len(feature_record)):
record = feature_record[index]
tag = record.FeatureTag
feature = record.Feature
print(index, tag, feature.LookupListIndex)
if feature.FeatureParams is not None:
print_indented("# name <%s>;" % feature.FeatureParams.UINameID)
def dump_lookup_list(lookup_list, table_name):
"""Prints out a lookup list."""
for index in range(len(lookup_list)):
lookup = lookup_list[index]
print(
"lookup %d { # type=%d flag=0x%X"
% (index, lookup.LookupType, lookup.LookupFlag)
)
for subtable in lookup.SubTable:
if table_name == "GSUB":
dump_gsub_subtable(lookup.LookupType, subtable)
elif table_name == "GPOS":
dump_gpos_subtable(lookup.LookupType, subtable)
print("}")
def dump_otl_table(font, table_name):
"""Prints out an OpenType Layout table."""
if table_name not in font:
print("no %s table" % table_name)
print()
return
else:
print("%s" % table_name)
print("----")
table = font[table_name].table
dump_script_record(table.ScriptList.ScriptRecord)
print()
dump_feature_record(table.FeatureList.FeatureRecord)
print()
dump_lookup_list(table.LookupList.Lookup, table_name)
print()
def main():
"""Dump the OpenType Layout tables for all input arguments."""
for font_file_name in sys.argv[1:]:
font = ttLib.TTFont(font_file_name)
print("%s: %s" % (font_file_name, internal_font_name(font)))
dump_otl_table(font, "GPOS")
dump_otl_table(font, "GSUB")
if __name__ == "__main__":
main()