Opentype GSUB processing. Development history at branches/cibu/adv_layout. Merged at r236.

git-svn-id: http://sfntly.googlecode.com/svn/trunk@237 672e30a5-4c29-85ac-ac6d-611c735e0a51
diff --git a/java/src/com/google/typography/font/sfntly/data/FontData.java b/java/src/com/google/typography/font/sfntly/data/FontData.java
index 805496b..79c8851 100644
--- a/java/src/com/google/typography/font/sfntly/data/FontData.java
+++ b/java/src/com/google/typography/font/sfntly/data/FontData.java
@@ -178,10 +178,19 @@
   }
 
   /**
+   * Returns the offset in the underlying data taking into account any bounds on
+   * the data.
+   */
+  public final int dataOffset() {
+    return this.boundOffset;
+  }
+
+  /**
    * Gets the offset in the underlying data taking into account any bounds on
    * the data.
    *
-   * @param offset the offset to get the bound compensated offset for
+   * @param offset
+   *          the offset to get the bound compensated offset for
    * @return the bound compensated offset
    */
   protected final int boundOffset(int offset) {
diff --git a/java/src/com/google/typography/font/sfntly/sample/build.xml b/java/src/com/google/typography/font/sfntly/sample/build.xml
index 1d54bb1..09f8abb 100644
--- a/java/src/com/google/typography/font/sfntly/sample/build.xml
+++ b/java/src/com/google/typography/font/sfntly/sample/build.xml
@@ -2,6 +2,18 @@
 
   <import file="../../../../../../../common.xml" />
 
+  <target name="sfview" depends="sfntly-jar">
+    <mkdir dir="${dist_sfview.dir}" />
+    <jar destfile="${dist_sfview.dir}/sfview.jar" basedir="${classes.dir}" includes="com/google/typography/font/sfntly/sample/sfview/**">
+      <zipfileset src="${dist_lib.dir}/sfntly.jar" />
+      <zipfileset src="${lib.dir}/icu4j-charset-4_8_1_1.jar" />
+      <zipfileset src="${lib.dir}/icu4j-4_8_1_1.jar" />
+      <manifest>
+        <attribute name="Main-Class" value="com.google.typography.font.sfntly.sample.sfview.SFView"/>
+      </manifest>
+    </jar>
+  </target>
+
   <target name="sflint" depends="sfntly-jar">
     <mkdir dir="${dist_sflint.dir}" />
     <jar destfile="${dist_sflint.dir}/sflint.jar" basedir="${classes.dir}" includes="com/google/typography/font/sfntly/sample/sflint/**">
@@ -26,6 +38,6 @@
     </jar>
   </target>
 
-  <target name="all" depends="sflint, sfntdump" />
+  <target name="all" depends="sfview, sflint, sfntdump" />
 
 </project>
diff --git a/java/src/com/google/typography/font/sfntly/sample/sfview/GsubRulesDump.java b/java/src/com/google/typography/font/sfntly/sample/sfview/GsubRulesDump.java
new file mode 100644
index 0000000..dc46705
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/sample/sfview/GsubRulesDump.java
@@ -0,0 +1,46 @@
+package com.google.typography.font.sfntly.sample.sfview;
+
+import com.google.typography.font.sfntly.Font;
+import com.google.typography.font.sfntly.FontFactory;
+import com.google.typography.font.sfntly.Tag;
+import com.google.typography.font.sfntly.table.core.PostScriptTable;
+import com.google.typography.font.sfntly.table.opentype.component.GlyphGroup;
+import com.google.typography.font.sfntly.table.opentype.component.Rule;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+public class GsubRulesDump {
+  public static void main(String[] args) throws IOException {
+    String fontName = args[0];
+    String txt = args[1];
+
+    System.out.println("Rules from font: " + fontName);
+    Font[] fonts = loadFont(new File(fontName));
+    if (fonts == null) {
+      throw new IllegalArgumentException("No font found");
+    }
+
+    Font font = fonts[0];
+    GlyphGroup ruleClosure = Rule.charGlyphClosure(font, txt);
+    PostScriptTable post = font.getTable(Tag.post);
+    Rule.dumpLookups(font);
+    System.out.println("Closure: " + ruleClosure.toString(post));
+  }
+
+  private static Font[] loadFont(File file) throws IOException {
+    FontFactory fontFactory = FontFactory.getInstance();
+    fontFactory.fingerprintFont(true);
+    FileInputStream is = new FileInputStream(file);
+    try {
+      return fontFactory.loadFonts(is);
+    } catch (FileNotFoundException e) {
+      System.err.println("Could not load the font: " + file.getName());
+      return null;
+    } finally {
+      is.close();
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/sample/sfview/HtmlViewer.java b/java/src/com/google/typography/font/sfntly/sample/sfview/HtmlViewer.java
new file mode 100644
index 0000000..f30b9e6
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/sample/sfview/HtmlViewer.java
@@ -0,0 +1,54 @@
+package com.google.typography.font.sfntly.sample.sfview;
+
+import com.google.typography.font.sfntly.Font;
+import com.google.typography.font.sfntly.FontFactory;
+import com.google.typography.font.sfntly.Tag;
+import com.google.typography.font.sfntly.table.opentype.GSubTable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+
+public class HtmlViewer {
+//  private static final String fileName = "/home/build/google3/googledata/third_party/" +
+//      "fonts/ascender/arial.ttf";
+
+  public static void main(String[] args) throws IOException {
+
+    Font[] fonts = loadFont(new File(args[0]));
+    GSubTable gsub = fonts[0].getTable(Tag.GSUB);
+    tag(gsub, args[1]);
+
+  }
+  public static void tag(GSubTable gsub, String outFileName) throws FileNotFoundException, UnsupportedEncodingException {
+    PrintWriter writer = new PrintWriter(outFileName, "UTF-8");
+    writer.println("<html>");
+    writer.println("  <head>");
+    writer.println("    <link href=special.css rel=stylesheet type=text/css>");
+    writer.println("  </head>");
+    writer.println("  <body>");
+//    writer.println(gsub.scriptList().toHtml());
+//    writer.println(gsub.featureList().toHtml());
+//    writer.println(gsub.lookupList().toHtml());
+    writer.println("  </body>");
+    writer.println("</html>");
+    writer.close();
+  }
+
+  public static Font[] loadFont(File file) throws IOException {
+    FontFactory fontFactory = FontFactory.getInstance();
+    fontFactory.fingerprintFont(true);
+    FileInputStream is = null;
+    try {
+      is = new FileInputStream(file);
+      return fontFactory.loadFonts(is);
+    } finally {
+      if (is != null) {
+        is.close();
+      }
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/sample/sfview/OtTableTagger.java b/java/src/com/google/typography/font/sfntly/sample/sfview/OtTableTagger.java
new file mode 100644
index 0000000..971cb23
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/sample/sfview/OtTableTagger.java
@@ -0,0 +1,747 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.sample.sfview;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.sample.sfview.TaggedData.FieldType;
+import com.google.typography.font.sfntly.table.FontDataTable;
+import com.google.typography.font.sfntly.table.opentype.AlternateSubst;
+import com.google.typography.font.sfntly.table.opentype.ChainContextSubst;
+import com.google.typography.font.sfntly.table.opentype.ClassDefTable;
+import com.google.typography.font.sfntly.table.opentype.ContextSubst;
+import com.google.typography.font.sfntly.table.opentype.CoverageTable;
+import com.google.typography.font.sfntly.table.opentype.ExtensionSubst;
+import com.google.typography.font.sfntly.table.opentype.FeatureListTable;
+import com.google.typography.font.sfntly.table.opentype.FeatureTable;
+import com.google.typography.font.sfntly.table.opentype.GSubTable;
+import com.google.typography.font.sfntly.table.opentype.LangSysTable;
+import com.google.typography.font.sfntly.table.opentype.LigatureSubst;
+import com.google.typography.font.sfntly.table.opentype.LookupListTable;
+import com.google.typography.font.sfntly.table.opentype.LookupTable;
+import com.google.typography.font.sfntly.table.opentype.MultipleSubst;
+import com.google.typography.font.sfntly.table.opentype.NullTable;
+import com.google.typography.font.sfntly.table.opentype.ReverseChainSingleSubst;
+import com.google.typography.font.sfntly.table.opentype.ScriptListTable;
+import com.google.typography.font.sfntly.table.opentype.ScriptTable;
+import com.google.typography.font.sfntly.table.opentype.SingleSubst;
+import com.google.typography.font.sfntly.table.opentype.SubstSubtable;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubClassRule;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubClassSet;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubGenericRuleSet;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubRule;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubRuleSet;
+import com.google.typography.font.sfntly.table.opentype.classdef.InnerArrayFmt1;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.OneToManySubst;
+import com.google.typography.font.sfntly.table.opentype.component.RangeRecordTable;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.DoubleRecordTable;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubClassRule;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubClassSet;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubGenericRuleSet;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubRule;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubRuleSet;
+import com.google.typography.font.sfntly.table.opentype.ligaturesubst.Ligature;
+import com.google.typography.font.sfntly.table.opentype.ligaturesubst.LigatureSet;
+import com.google.typography.font.sfntly.table.opentype.singlesubst.HeaderFmt1;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+class OtTableTagger {
+  private final TaggedData td;
+  private final Map<Class<? extends FontDataTable>, TagMethod> tagMethodRegistry;
+
+  OtTableTagger(TaggedData tdata) {
+    this.td = tdata;
+    this.tagMethodRegistry = new HashMap<Class<? extends FontDataTable>, TagMethod>();
+
+    registerTagMethods();
+  }
+
+  void tag(GSubTable gsub) {
+    if (gsub == null) {
+      return;
+    }
+
+    tagTable(gsub.scriptList());
+    tagTable(gsub.featureList());
+    tagTable(gsub.lookupList());
+  }
+
+  private final List<String> tableCache = new ArrayList<String>();
+
+  private void tagTable(FontDataTable table) {
+    if (table == null) {
+      return;
+    }
+    ReadableFontData data = table.readFontData();
+    if (data == null) {
+      return;
+    }
+
+    if (tableCache.contains(table.toString())) {
+      return;
+    }
+    tableCache.add(table.toString());
+
+    TagMethod tm = getTagMethod(table);
+    if (tm == null) {
+      td.pushRange(table.getClass().getSimpleName(), data);
+    } else {
+      td.pushRange(tm.tableLabel(table), data);
+      tm.tag(table);
+    }
+    td.popRange();
+  }
+
+  abstract class TagMethod {
+    private final Class<? extends FontDataTable> clzz;
+
+    private TagMethod(Class<? extends FontDataTable> clzz) {
+      this.clzz = clzz;
+    }
+
+    private String tableLabel(FontDataTable table) {
+      Class<?> clzz = table.getClass();
+      Class<?> encl = clzz.getEnclosingClass();
+      if (encl == null) {
+        return clzz.getSimpleName();
+      }
+      return encl.getSimpleName() + "." + clzz.getSimpleName();
+    }
+
+    protected abstract void tag(FontDataTable table);
+  }
+
+  private void register(TagMethod m) {
+    tagMethodRegistry.put(m.clzz, m);
+  }
+
+  @SafeVarargs
+  private final void register(TagMethod m, Class<? extends FontDataTable>... clzzes) {
+    tagMethodRegistry.put(m.clzz, m);
+    for (Class<? extends FontDataTable> clzz : clzzes) {
+      tagMethodRegistry.put(clzz, m);
+    }
+  }
+
+  void registerTagMethods() {
+    register(new TagMethod(ScriptListTable.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        ScriptListTable table = (ScriptListTable) fdt;
+        int scriptCount = td.tagRangeField(FieldType.SHORT, "script count");
+        for (int i = 0; i < scriptCount; ++i) {
+          td.tagRangeField(FieldType.TAG, null);
+          td.tagRangeField(FieldType.OFFSET, null);
+        }
+        for (ScriptTable st : table) {
+          tagTable(st);
+        }
+      }
+    });
+
+    register(new TagMethod(ScriptTable.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        ScriptTable table = (ScriptTable) fdt;
+        td.tagRangeField(FieldType.OFFSET_NONZERO, "default lang sys");
+        int langCount = td.tagRangeField(FieldType.SHORT, "language count");
+        for (int i = 0; i < langCount; ++i) {
+          td.tagRangeField(FieldType.TAG, null);
+          td.tagRangeField(FieldType.OFFSET, null);
+        }
+        for (LangSysTable lst : table) {
+          tagTable(lst);
+        }
+        tagTable(table.defaultLangSysTable());
+      }
+    });
+
+    register(new TagMethod(LangSysTable.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        LangSysTable table = (LangSysTable) fdt;
+        td.tagRangeField(FieldType.SHORT_IGNORED, "lookup order");
+        td.tagRangeField(FieldType.SHORT_IGNORED_FFFF, "required feature");
+        td.tagRangeField(FieldType.SHORT, "feature count");
+        for (int i = 0; i < table.recordList.count(); ++i) {
+          td.tagRangeField(FieldType.SHORT, null);
+        }
+      }
+    });
+
+    register(new TagMethod(FeatureListTable.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        FeatureListTable table = (FeatureListTable) fdt;
+        int featureCount = td.tagRangeField(FieldType.SHORT, "feature count");
+        for (int i = 0; i < featureCount; ++i) {
+          td.tagRangeField(FieldType.TAG, "index: " + i);
+          td.tagRangeField(FieldType.OFFSET, null);
+        }
+        for (FeatureTable ft : table) {
+          tagTable(ft);
+        }
+      }
+    });
+
+    register(new TagMethod(FeatureTable.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        FeatureTable table = (FeatureTable) fdt;
+        td.tagRangeField(FieldType.OFFSET_NONZERO, "feature params");
+        td.tagRangeField(FieldType.SHORT, "lookup count");
+        for (int i = 0; i < table.recordList.count(); ++i) {
+          td.tagRangeField(FieldType.SHORT, null);
+        }
+      }
+    });
+
+    register(new TagMethod(LookupListTable.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        LookupListTable table = (LookupListTable) fdt;
+        int lookupCount = td.tagRangeField(FieldType.SHORT, "lookup count");
+        for (int i = 0; i < lookupCount; ++i) {
+          td.tagRangeField(FieldType.OFFSET, "index: " + i);
+        }
+        for (int i = 0; i < lookupCount; ++i) {
+          LookupTable lookup = table.subTableAt(i);
+          if (lookup != null) {
+            tagTable(lookup);
+          }
+        }
+      }
+    });
+
+    register(new TagMethod(LookupTable.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        LookupTable table = (LookupTable) fdt;
+        td.tagRangeField(FieldType.SHORT, "lookup type");
+        td.tagRangeField(FieldType.SHORT, "lookup flags");
+        int subTableCount = td.tagRangeField(FieldType.SHORT, "subtable count");
+        for (int i = 0; i < subTableCount; ++i) {
+          td.tagRangeField(FieldType.OFFSET, null);
+        }
+        for (int i = 0; i < subTableCount; ++i) {
+          SubstSubtable subTable = table.subTableAt(i);
+          tagTable(subTable);
+        }
+      }
+    });
+
+    register(new TagMethod(LigatureSubst.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        LigatureSubst table = (LigatureSubst) fdt;
+        td.tagRangeField(FieldType.SHORT, "subst format");
+        td.tagRangeField(FieldType.OFFSET_NONZERO, "coverage offset");
+        tagTable(table.coverage());
+        td.tagRangeField(FieldType.SHORT, "subtable count");
+
+        int subTableCount = table.subTableCount();
+        for (int i = 0; i < subTableCount; ++i) {
+          td.tagRangeField(FieldType.OFFSET, null);
+        }
+
+        for (int i = 0; i < subTableCount; ++i) {
+          LigatureSet subTable = table.subTableAt(i);
+          tagTable(subTable);
+        }
+      }
+    });
+
+    register(new TagMethod(LigatureSet.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        LigatureSet table = (LigatureSet) fdt;
+        td.tagRangeField(FieldType.SHORT, "lookup count");
+        for (int i = 0; i < table.recordList.count(); ++i) {
+          td.tagRangeField(FieldType.OFFSET, null);
+        }
+        for (int i = 0; i < table.recordList.count(); ++i) {
+          Ligature lookup = table.subTableAt(i);
+          if (lookup != null) {
+            tagTable(lookup);
+          }
+        }
+      }
+    });
+
+    register(new TagMethod(Ligature.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        Ligature table = (Ligature) fdt;
+        td.tagRangeField(FieldType.GLYPH, "lig glyph");
+        td.tagRangeField(FieldType.SHORT, "glyph count + 1");
+        for (int i = 0; i < table.recordList.count(); ++i) {
+          td.tagRangeField(FieldType.GLYPH, null);
+        }
+      }
+    });
+
+    register(new TagMethod(SingleSubst.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        SingleSubst table = (SingleSubst) fdt;
+        td.tagRangeField(FieldType.SHORT, "format");
+        switch (table.format) {
+        case 1:
+          HeaderFmt1 tableFmt1 = table.fmt1Table();
+          td.tagRangeField(FieldType.OFFSET_NONZERO, "coverage offset");
+          tagTable(tableFmt1.coverage);
+          td.tagRangeField(FieldType.SHORT, "delta glyph id");
+          break;
+        case 2:
+          com.google.typography.font.sfntly.table.opentype.singlesubst.InnerArrayFmt2 tableFmt2 =
+              table.fmt2Table();
+          td.tagRangeField(FieldType.OFFSET_NONZERO, "coverage offset");
+          tagTable(tableFmt2.coverage);
+          td.tagRangeField(FieldType.SHORT, "glyph count");
+          for (int i = 0; i < tableFmt2.recordList.count(); ++i) {
+            td.tagRangeField(FieldType.GLYPH, null);
+          }
+          break;
+        }
+      }
+    });
+
+    register(new TagMethod(MultipleSubst.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        OneToManySubst table = (OneToManySubst) fdt;
+        td.tagRangeField(FieldType.SHORT, "subst format");
+        td.tagRangeField(FieldType.OFFSET_NONZERO, "coverage offset");
+        tagTable(table.coverage());
+        td.tagRangeField(FieldType.SHORT, "sequence count");
+
+        int subTableCount = table.recordList().count();
+        for (int i = 0; i < subTableCount; ++i) {
+          td.tagRangeField(FieldType.OFFSET, null);
+        }
+
+        for (int i = 0; i < subTableCount; ++i) {
+          NumRecordTable subTable = table.subTableAt(i);
+          tagTable(subTable);
+        }
+      }
+    }, AlternateSubst.class);
+
+    register(new TagMethod(NumRecordTable.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        NumRecordTable table = (NumRecordTable) fdt;
+        td.tagRangeField(FieldType.SHORT, "glyph count");
+        for (int i = 0; i < table.recordList.count(); ++i) {
+          td.tagRangeField(FieldType.GLYPH, null);
+        }
+      }
+    });
+
+    register(new TagMethod(ContextSubst.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        ContextSubst table = (ContextSubst) fdt;
+        td.tagRangeField(FieldType.SHORT, "subst format");
+        td.tagRangeField(FieldType.OFFSET_NONZERO, "coverage offset");
+        tagTable(table.coverage());
+        if (table.format == 2) {
+          td.tagRangeField(FieldType.OFFSET_NONZERO, "class def offset");
+          tagTable(table.classDef());
+        }
+        td.tagRangeField(FieldType.SHORT, "sub rule set count");
+
+        int subTableCount = table.recordList().count();
+        for (int i = 0; i < subTableCount; ++i) {
+          td.tagRangeField(FieldType.OFFSET_NONZERO, "for inital class: " + i);
+        }
+        for (int i = 0; i < subTableCount; ++i) {
+          SubGenericRuleSet<?> subTable = table.subTableAt(i);
+          if (subTable != null) {
+            tagTable(subTable);
+          }
+        }
+      }
+    });
+
+    register(new TagMethod(SubRuleSet.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        SubGenericRuleSet<?> table = (SubGenericRuleSet<?>) fdt;
+        td.tagRangeField(FieldType.SHORT, "sub rule count");
+        int subTableCount = table.recordList.count();
+        for (int i = 0; i < subTableCount; ++i) {
+          td.tagRangeField(FieldType.OFFSET, null);
+        }
+        for (int i = 0; i < subTableCount; ++i) {
+          DoubleRecordTable subTable = table.subTableAt(i);
+          tagTable(subTable);
+        }
+      }
+    }, SubClassSet.class);
+
+    register(new TagMethod(SubRule.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        SubRule table = (SubRule) fdt;
+        td.tagRangeField(FieldType.SHORT, "input glyph count + 1");
+        td.tagRangeField(FieldType.SHORT, "subst lookup record count");
+        int glyphCount = table.inputGlyphs.count();
+        for (int i = 0; i < glyphCount; ++i) {
+          td.tagRangeField(FieldType.GLYPH, "glyph id");
+        }
+        int lookupCount = table.lookupRecords.count();
+        for (int i = 0; i < lookupCount; ++i) {
+          td.tagRangeField(FieldType.SHORT, "sequence index");
+          td.tagRangeField(FieldType.SHORT, "lookup list index");
+        }
+      }
+    });
+
+    register(new TagMethod(SubClassRule.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        SubClassRule table = (SubClassRule) fdt;
+        td.tagRangeField(FieldType.SHORT, "input class count + 1");
+        td.tagRangeField(FieldType.SHORT, "subst lookup record count");
+        int glyphCount = table.inputGlyphs.count();
+        for (int i = 0; i < glyphCount; ++i) {
+          td.tagRangeField(FieldType.SHORT, "class id");
+        }
+        int lookupCount = table.lookupRecords.count();
+        for (int i = 0; i < lookupCount; ++i) {
+          td.tagRangeField(FieldType.SHORT, "sequence index");
+          td.tagRangeField(FieldType.SHORT, "lookup list index");
+        }
+      }
+    });
+
+    register(new TagMethod(ChainContextSubst.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        ChainContextSubst table = (ChainContextSubst) fdt;
+        td.tagRangeField(FieldType.SHORT, "subst format");
+        if (table.format == 1 || table.format == 2) {
+          td.tagRangeField(FieldType.OFFSET_NONZERO, "coverage offset");
+          tagTable(table.coverage());
+          int subTableCount = table.recordList().count();
+          if (table.format == 1) {
+            td.tagRangeField(FieldType.SHORT, "chain sub rule set count");
+          }
+          if (table.format == 2) {
+            td.tagRangeField(FieldType.OFFSET_NONZERO, "backtrack class def offset");
+            tagTable(table.backtrackClassDef());
+            td.tagRangeField(FieldType.OFFSET_NONZERO, "input class def offset");
+            tagTable(table.inputClassDef());
+            td.tagRangeField(FieldType.OFFSET_NONZERO, "look ahead class def offset");
+            tagTable(table.lookAheadClassDef());
+            td.tagRangeField(FieldType.SHORT, "chain sub class set count");
+          }
+          for (int i = 0; i < subTableCount; ++i) {
+            td.tagRangeField(FieldType.OFFSET_NONZERO, null);
+          }
+          for (int i = 0; i < subTableCount; ++i) {
+            ChainSubGenericRuleSet<?> subTable = table.subTableAt(i);
+            if (subTable != null) {
+              tagTable(subTable);
+            }
+          }
+        }
+        if (table.format == 3) {
+          td.tagRangeField(FieldType.SHORT, "backtrackGlyphs coverage count");
+          int subTableCount = table.fmt3Array.backtrackGlyphs.recordList.count();
+          for (int i = 0; i < subTableCount; ++i) {
+            td.tagRangeField(FieldType.OFFSET_NONZERO, null);
+            CoverageTable subTable = table.fmt3Array.backtrackGlyphs.subTableAt(i);
+            if (subTable != null) {
+              tagTable(subTable);
+            }
+          }
+
+          td.tagRangeField(FieldType.SHORT, "input glyphs coverage count");
+          subTableCount = table.fmt3Array.inputGlyphs.recordList.count();
+          for (int i = 0; i < subTableCount; ++i) {
+            td.tagRangeField(FieldType.OFFSET_NONZERO, null);
+            CoverageTable subTable = table.fmt3Array.inputGlyphs.subTableAt(i);
+            if (subTable != null) {
+              tagTable(subTable);
+            }
+          }
+
+          td.tagRangeField(FieldType.SHORT, "lookahead glyphs coverage count");
+          subTableCount = table.fmt3Array.lookAheadGlyphs.recordList.count();
+          for (int i = 0; i < subTableCount; ++i) {
+            td.tagRangeField(FieldType.OFFSET_NONZERO, null);
+            CoverageTable subTable = table.fmt3Array.lookAheadGlyphs.subTableAt(i);
+            if (subTable != null) {
+              tagTable(subTable);
+            }
+          }
+
+          td.tagRangeField(FieldType.SHORT, "subst lookup record count");
+          int lookupCount = table.fmt3Array.lookupRecords.count();
+          for (int i = 0; i < lookupCount; ++i) {
+            td.tagRangeField(FieldType.SHORT, "sequence index");
+            td.tagRangeField(FieldType.SHORT, "lookup list index");
+          }
+        }
+      }
+    });
+
+    register(new TagMethod(ChainSubRuleSet.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        ChainSubRuleSet table = (ChainSubRuleSet) fdt;
+        td.tagRangeField(FieldType.SHORT, "sub rule count");
+        int subTableCount = table.recordList.count();
+        for (int i = 0; i < subTableCount; ++i) {
+          td.tagRangeField(FieldType.OFFSET, null);
+        }
+        for (int i = 0; i < subTableCount; ++i) {
+          ChainSubRule subTable = table.subTableAt(i);
+          tagTable(subTable);
+        }
+      }
+    });
+
+    register(new TagMethod(ChainSubRule.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        ChainSubRule table = (ChainSubRule) fdt;
+        td.tagRangeField(FieldType.SHORT, "backtrack glyph count");
+        int glyphCount = table.backtrackGlyphs.count();
+        for (int i = 0; i < glyphCount; ++i) {
+          td.tagRangeField(FieldType.GLYPH, null);
+        }
+
+        td.tagRangeField(FieldType.SHORT, "input glyph count");
+        glyphCount = table.inputClasses.count();
+        for (int i = 0; i < glyphCount; ++i) {
+          td.tagRangeField(FieldType.GLYPH, null);
+        }
+
+        td.tagRangeField(FieldType.SHORT, "look ahead glyph count");
+        glyphCount = table.lookAheadGlyphs.count();
+        for (int i = 0; i < glyphCount; ++i) {
+          td.tagRangeField(FieldType.GLYPH, null);
+        }
+
+        td.tagRangeField(FieldType.SHORT, "subst lookup record count");
+        int lookupCount = table.lookupRecords.count();
+        for (int i = 0; i < lookupCount; ++i) {
+          td.tagRangeField(FieldType.SHORT, "sequence index");
+          td.tagRangeField(FieldType.SHORT, "lookup list index");
+        }
+      }
+    });
+
+    register(new TagMethod(ChainSubClassSet.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        ChainSubClassSet table = (ChainSubClassSet) fdt;
+        td.tagRangeField(FieldType.SHORT, "sub class count");
+        int subTableCount = table.recordList.count();
+        for (int i = 0; i < subTableCount; ++i) {
+          td.tagRangeField(FieldType.OFFSET, null);
+        }
+        for (int i = 0; i < subTableCount; ++i) {
+          ChainSubClassRule subTable = table.subTableAt(i);
+          tagTable(subTable);
+        }
+      }
+    });
+
+    register(new TagMethod(ChainSubClassRule.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        ChainSubClassRule table = (ChainSubClassRule) fdt;
+        td.tagRangeField(FieldType.SHORT, "backtrack glyph class count");
+        int glyphCount = table.backtrackGlyphs.count();
+        for (int i = 0; i < glyphCount; ++i) {
+          td.tagRangeField(FieldType.SHORT, "class id");
+        }
+
+        td.tagRangeField(FieldType.SHORT, "input glyph class count");
+        glyphCount = table.inputClasses.count();
+        for (int i = 0; i < glyphCount; ++i) {
+          td.tagRangeField(FieldType.SHORT, "class id");
+        }
+
+        td.tagRangeField(FieldType.SHORT, "look ahead glyph class count");
+        glyphCount = table.lookAheadGlyphs.count();
+        for (int i = 0; i < glyphCount; ++i) {
+          td.tagRangeField(FieldType.SHORT, "class id");
+        }
+
+        td.tagRangeField(FieldType.SHORT, "subst lookup record count");
+        int lookupCount = table.lookupRecords.count();
+        for (int i = 0; i < lookupCount; ++i) {
+          td.tagRangeField(FieldType.SHORT, "sequence index");
+          td.tagRangeField(FieldType.SHORT, "lookup list index");
+        }
+      }
+    });
+
+    register(new TagMethod(ExtensionSubst.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        ExtensionSubst table = (ExtensionSubst) fdt;
+        td.tagRangeField(FieldType.SHORT, "format");
+        td.tagRangeField(FieldType.SHORT, "lookup type");
+        td.tagRangeField(FieldType.OFFSET32, "lookup offset");
+        SubstSubtable subTable = table.subTable();
+        tagTable(subTable);
+      }
+    });
+
+    register(new TagMethod(ReverseChainSingleSubst.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        ReverseChainSingleSubst table = (ReverseChainSingleSubst) fdt;
+        td.tagRangeField(FieldType.SHORT, "subst format");
+        td.tagRangeField(FieldType.OFFSET_NONZERO, "input coverage offset");
+        tagTable(table.coverage);
+
+        td.tagRangeField(FieldType.SHORT, "backtrack glyphs coverages count");
+        int subTableCount = table.backtrackGlyphs.recordList.count();
+        for (int i = 0; i < subTableCount; ++i) {
+          td.tagRangeField(FieldType.OFFSET_NONZERO, null);
+          CoverageTable subTable = table.backtrackGlyphs.subTableAt(i);
+          if (subTable != null) {
+            tagTable(subTable);
+          }
+        }
+
+        td.tagRangeField(FieldType.SHORT, "lookahead glyphs coverages count");
+        subTableCount = table.lookAheadGlyphs.recordList.count();
+        for (int i = 0; i < subTableCount; ++i) {
+          td.tagRangeField(FieldType.OFFSET_NONZERO, null);
+          CoverageTable subTable = table.lookAheadGlyphs.subTableAt(i);
+          if (subTable != null) {
+            tagTable(subTable);
+          }
+        }
+
+        td.tagRangeField(FieldType.SHORT, "subst glyph count");
+        for (int i = 0; i < table.substitutes.recordList.count(); ++i) {
+          td.tagRangeField(FieldType.GLYPH, null);
+        }
+      }
+    });
+
+    register(new TagMethod(CoverageTable.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        CoverageTable table = (CoverageTable) fdt;
+        td.tagRangeField(FieldType.SHORT, "format");
+        if (table.format == 1) {
+          NumRecordTable tableFmt1 = (NumRecordTable) table.array;
+          td.tagRangeField(FieldType.SHORT, "glyph count");
+          for (int i = 0; i < tableFmt1.recordList.count(); ++i) {
+            td.tagRangeField(FieldType.GLYPH, null);
+          }
+        }
+        if (table.format == 2) {
+          RangeRecordTable tableFmt2 = (RangeRecordTable) table.array;
+          td.tagRangeField(FieldType.SHORT, "range count");
+          for (int i = 0; i < tableFmt2.recordList.count(); ++i) {
+            td.tagRangeField(FieldType.SHORT, "start");
+            td.tagRangeField(FieldType.SHORT, "end");
+            td.tagRangeField(FieldType.SHORT, "offset");
+          }
+        }
+      }
+    });
+
+    register(new TagMethod(ClassDefTable.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+        ClassDefTable table = (ClassDefTable) fdt;
+        td.tagRangeField(FieldType.SHORT, "format");
+        if (table.format == 1) {
+          InnerArrayFmt1 tableFmt1 = (InnerArrayFmt1) table.array;
+          td.tagRangeField(FieldType.SHORT, "start glyph");
+          td.tagRangeField(FieldType.SHORT, "glyph count");
+          for (int i = 0; i < tableFmt1.recordList.count(); ++i) {
+            td.tagRangeField(FieldType.SHORT, null);
+          }
+        }
+        if (table.format == 2) {
+          RangeRecordTable tableFmt2 = (RangeRecordTable) table.array;
+          td.tagRangeField(FieldType.SHORT, "class range count");
+          for (int i = 0; i < tableFmt2.recordList.count(); ++i) {
+            td.tagRangeField(FieldType.SHORT, "start");
+            td.tagRangeField(FieldType.SHORT, "end");
+            td.tagRangeField(FieldType.SHORT, "class");
+          }
+        }
+      }
+    });
+
+    register(new TagMethod(NullTable.class) {
+      @Override
+      protected
+      void tag(FontDataTable fdt) {
+      }
+    });
+  }
+
+  private static final Comparator<Class<? extends FontDataTable>> CLASS_NAME_COMPARATOR =
+      new Comparator<Class<? extends FontDataTable>>() {
+    @Override
+    public int compare(Class<? extends FontDataTable> o1, Class<? extends FontDataTable> o2) {
+      return o1.getName().compareTo(o2.getName());
+    }
+  };
+
+  private static Set<Class<? extends FontDataTable>>
+  missedClasses = new TreeSet<Class<? extends FontDataTable>>(CLASS_NAME_COMPARATOR);
+
+  private TagMethod getTagMethod(FontDataTable table) {
+    Class<? extends FontDataTable> clzz = table.getClass();
+    TagMethod tm = tagMethodRegistry.get(clzz);
+    if (tm == null) {
+      if (!missedClasses.contains(clzz)) {
+        missedClasses.add(clzz);
+        System.out.println("unregistered class: " + clzz.getName());
+      }
+    }
+    return tm;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/sample/sfview/RuleDump.java b/java/src/com/google/typography/font/sfntly/sample/sfview/RuleDump.java
new file mode 100644
index 0000000..ecfd1b0
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/sample/sfview/RuleDump.java
@@ -0,0 +1,42 @@
+package com.google.typography.font.sfntly.sample.sfview;
+
+import com.google.typography.font.sfntly.Font;
+import com.google.typography.font.sfntly.FontFactory;
+import com.google.typography.font.sfntly.table.opentype.component.GlyphGroup;
+import com.google.typography.font.sfntly.table.opentype.component.Rule;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+public class RuleDump {
+  public static void main(String[] args) throws IOException {
+
+    String fontName = args[0];
+    String txt = args[1];
+
+    System.out.println("Rules from font: " + fontName);
+    Font[] fonts = loadFont(new File(fontName));
+    if (fonts == null) {
+      throw new IllegalArgumentException("No font found");
+    }
+
+    Font font = fonts[0];
+    GlyphGroup ruleClosure = Rule.charGlyphClosure(txt, font);
+  }
+
+  public static Font[] loadFont(File file) throws IOException {
+    FontFactory fontFactory = FontFactory.getInstance();
+    fontFactory.fingerprintFont(true);
+    FileInputStream is = new FileInputStream(file);
+    try {
+      return fontFactory.loadFonts(is);
+    } catch (FileNotFoundException e) {
+      System.err.println("Could not load the font: " + file.getName());
+      return null;
+    } finally {
+      is.close();
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/sample/sfview/SFFontView.java b/java/src/com/google/typography/font/sfntly/sample/sfview/SFFontView.java
new file mode 100644
index 0000000..671b615
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/sample/sfview/SFFontView.java
@@ -0,0 +1,89 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.sample.sfview;
+
+import com.google.typography.font.sfntly.Font;
+import com.google.typography.font.sfntly.Tag;
+import com.google.typography.font.sfntly.sample.sfview.ViewableTaggedData.TaggedDataImpl;
+import com.google.typography.font.sfntly.table.core.PostScriptTable;
+import com.google.typography.font.sfntly.table.opentype.GSubTable;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Rectangle;
+
+import javax.swing.JComponent;
+import javax.swing.Scrollable;
+import javax.swing.SwingConstants;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+class SFFontView extends JComponent implements Scrollable {
+  private static final long serialVersionUID = 1L;
+  private final ViewableTaggedData viewer;
+
+  SFFontView(Font font) {
+    setBackground(Color.WHITE);
+
+    PostScriptTable post = font.getTable(Tag.post);
+    TaggedDataImpl tdata = new ViewableTaggedData.TaggedDataImpl(post);
+    OtTableTagger tagger = new OtTableTagger(tdata);
+    GSubTable gsub = font.getTable(Tag.GSUB);
+    tagger.tag(gsub);
+    viewer = new ViewableTaggedData(tdata.getMarkers());
+
+    Dimension dimensions = viewer.measure(true);
+
+    Dimension minimumSize = new Dimension(400, 400);
+    setMinimumSize(minimumSize);
+    setPreferredSize(dimensions);
+  }
+
+  @Override
+  public Dimension getPreferredScrollableViewportSize() {
+    int width = Math.min(500, viewer.totalWidth());
+    int height = 25 * viewer.lineHeight();
+    return new Dimension(width, height);
+  }
+
+  @Override
+  public void paintComponent(Graphics g) {
+    super.paintComponent(g);
+    g.setColor(getBackground());
+    g.fillRect(0, 0, getWidth(), getHeight());
+
+    viewer.draw(g, 0, 0);
+  }
+
+  @Override
+  public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
+    if (orientation == SwingConstants.HORIZONTAL) {
+      return 50;
+    }
+    return viewer.lineHeight();
+  }
+
+  @Override
+  public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
+    if (orientation == SwingConstants.HORIZONTAL) {
+      return viewer.totalWidth();
+    }
+    int lines = visibleRect.height / viewer.lineHeight() - 2;
+    if (lines < 1) {
+      lines = 1;
+    }
+    return lines * viewer.lineHeight();
+  }
+
+  @Override
+  public boolean getScrollableTracksViewportWidth() {
+    return false;
+  }
+
+  @Override
+  public boolean getScrollableTracksViewportHeight() {
+    return false;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/sample/sfview/SFView.java b/java/src/com/google/typography/font/sfntly/sample/sfview/SFView.java
new file mode 100644
index 0000000..257ddb0
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/sample/sfview/SFView.java
@@ -0,0 +1,50 @@
+package com.google.typography.font.sfntly.sample.sfview;
+
+import com.google.typography.font.sfntly.Font;
+import com.google.typography.font.sfntly.FontFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import javax.swing.JFrame;
+import javax.swing.JScrollPane;
+
+public class SFView {
+  public static void main(String[] args) throws IOException {
+    for (String fontName : args) {
+      System.out.println("Displaying font: " + fontName);
+      Font[] fonts = loadFont(new File(fontName));
+      if (fonts == null) {
+        continue;
+      }
+      for (Font font : fonts) {
+        JFrame jf = new JFrame("Sfntly Table Viewer");
+        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        SFFontView view = new SFFontView(font);
+        JScrollPane sp = new JScrollPane(view);
+        jf.add(sp);
+        jf.pack();
+        jf.setVisible(true);
+      }
+    }
+  }
+
+  private static Font[] loadFont(File file) throws IOException {
+    FontFactory fontFactory = FontFactory.getInstance();
+    fontFactory.fingerprintFont(true);
+    FileInputStream is = null;
+    try {
+      is = new FileInputStream(file);
+      return fontFactory.loadFonts(is);
+    } catch (FileNotFoundException e) {
+      System.err.println("Could not load the font: " + file.getName());
+      return null;
+    } finally {
+      if (is != null) {
+        is.close();
+      }
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/sample/sfview/TaggedData.java b/java/src/com/google/typography/font/sfntly/sample/sfview/TaggedData.java
new file mode 100644
index 0000000..ecd9347
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/sample/sfview/TaggedData.java
@@ -0,0 +1,62 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.sample.sfview;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+interface TaggedData {
+  /**
+   * @param string
+   *          label
+   * @param start
+   *          start of range to tag
+   * @param length
+   *          length of range to tag
+   * @param depth
+   *          nesting depth of range
+   */
+  void tagRange(String string, int start, int length, int depth);
+
+  /**
+   * @param position
+   *          the position of the field
+   * @param width
+   *          number of bytes for the field at position
+   * @param value
+   *          the value in those bytes
+   * @param alt
+   *          an alternate presentation of the value (in decimal, a tag)
+   * @param label
+   *          the label of this field
+   */
+  void tagField(int position, int width, int value, String alt, String label);
+
+  /**
+   * @param position
+   *          the position of the reference to target
+   * @param value
+   *          the raw value of the field
+   * @param targetPosition
+   *          the target position;
+   * @param label
+   *          name for this reference, or null
+   */
+  void tagTarget(int position, int value, int targetPosition, String label);
+
+  void pushRange(String string, ReadableFontData data);
+
+  void pushRangeAtOffset(String label, int base);
+
+  int tagRangeField(FieldType ft, String label);
+
+  void setRangePosition(int rangePosition);
+
+  void popRange();
+
+  static enum FieldType {
+    TAG, SHORT, SHORT_IGNORED, SHORT_IGNORED_FFFF, OFFSET, OFFSET_NONZERO, OFFSET32, GLYPH;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/sample/sfview/ViewableTaggedData.java b/java/src/com/google/typography/font/sfntly/sample/sfview/ViewableTaggedData.java
new file mode 100644
index 0000000..40240b2
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/sample/sfview/ViewableTaggedData.java
@@ -0,0 +1,864 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.sample.sfview;
+
+import com.google.typography.font.sfntly.Tag;
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.core.PostScriptTable;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.font.FontRenderContext;
+import java.awt.font.LineMetrics;
+import java.awt.geom.Rectangle2D;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+class ViewableTaggedData {
+  private List<Marker> markers = new ArrayList<Marker>();
+  private final Style style;
+  private final Metrics metrics;
+
+  ViewableTaggedData(List<Marker> markers) {
+    this(markers, new Style(), new Metrics());
+  }
+
+  private ViewableTaggedData(List<Marker> markers, Style style, Metrics metrics) {
+    this.markers = markers;
+    this.style = style;
+    this.metrics = metrics;
+  }
+
+  private static class Style {
+    private int marginScale;
+    private int marginOffset;
+    private int marginPad;
+    private int columnPad;
+    private Font dataFont;
+    private Font labelFont;
+    private Color positionColor;
+    private Color dataColor;
+    private Color altColor;
+    private Color labelColor;
+    // shades of blue hue 221.8 saturation 5% to 35% value 93.5 (yafla)
+    // E3E6EE, D7DEEE, CBD6EE, BFCDEE, B3C5EE, A7BDEE, 9BB4EE
+    private Color[] depthColors = {
+        new Color(0x9BB4EE), new Color(0xB3C5EE), new Color(0xCBD6EE), new Color(0xE3E6EE) };
+
+    private Style() {
+      marginScale = 4; // distance between lines
+      marginOffset = 1; // distance from origin to tip of arrow/base
+      marginPad = 4; // distance from marginOffset to first line
+      columnPad = 15; // extra padding for column widths
+      dataFont = new Font("monospaced", Font.PLAIN, 13);
+      labelFont = new Font("serif", Font.PLAIN, 13);
+      positionColor = Color.BLACK;
+      dataColor = Color.BLACK;
+      altColor = new Color(0x8B0000);
+      labelColor = Color.BLUE;
+    }
+  }
+
+  private static class Metrics {
+    private int lineHeight; // total line height
+    private int baseline; // distance from bottom up to baseline
+    private int xHeight; // distance from baseline up to xHeight
+    private int marginWidth;
+    private int positionWidth;
+    private int dataWidth;
+    private int altWidth;
+    private int labelWidth;
+    private int headerWidth;
+    private int totalWidth;
+
+    private Metrics() {
+      lineHeight = 15;
+      xHeight = 5;
+      marginWidth = 50;
+      positionWidth = 50;
+      dataWidth = 70;
+      altWidth = 30;
+      labelWidth = 100;
+      updateTotalWidth();
+    }
+
+    private void zero() {
+      lineHeight = baseline = xHeight = 0;
+      marginWidth = positionWidth = dataWidth = altWidth = labelWidth = headerWidth = totalWidth = 0;
+    }
+
+    private void updateTotalWidth() {
+      totalWidth = marginWidth
+          + Math.max(positionWidth + dataWidth + altWidth + labelWidth, headerWidth);
+    }
+  }
+
+  int lineHeight() {
+    return metrics.lineHeight;
+  }
+
+  int totalWidth() {
+    return metrics.totalWidth;
+  }
+
+  void draw(Graphics g, int x, int y) {
+    DrawContext context = new DrawContext(style, metrics, g, x, y);
+    for (Marker m : markers) {
+      m.draw(context);
+    }
+  }
+
+  /**
+   * Compute metrics and return the dimensions.
+   *
+   * @param zeroMetrics
+   *          zero the metrics before computing (otherwise use existing cell
+   *          widths and line height as minimums).
+   * @return the dimensions
+   */
+  Dimension measure(boolean zeroMetrics) {
+    if (zeroMetrics) {
+      metrics.zero();
+    }
+    DrawContext context = new DrawContext(style, metrics, null, 0, 0);
+    context.measureLineHeight();
+
+    for (Marker m : markers) {
+      m.draw(context);
+    }
+    return context.dimension();
+  }
+
+  public static class TaggedDataImpl implements TaggedData {
+    private final List<Marker> markers = new ArrayList<Marker>();
+    private RangeNode rangeStack;
+    private final PostScriptTable post;
+
+    TaggedDataImpl(PostScriptTable post) {
+      this.post = post;
+    }
+
+    @Override
+    public
+    void tagRange(String string, int start, int length, int depth) {
+      boolean hasEnd = (length & ~0xffff) == 0;
+      if (!hasEnd) {
+        depth = -1;
+      }
+      Range range = new Range(string, start, length, depth);
+      markers.add(new RangeStart(range));
+      if (hasEnd) {
+        markers.add(new RangeEnd(range));
+      }
+    }
+
+    @Override
+    public void tagField(int position, int width, int value, String alt, String label) {
+      markers.add(new Field(position, width, value, alt, label));
+    }
+
+    @Override
+    public void tagTarget(int position, int value, int targetPosition, String label) {
+      Reference reference = new Reference(position, targetPosition);
+      markers.add(new ReferenceSource(reference, value, label));
+      markers.add(new ReferenceTarget(reference));
+    }
+
+    List<Marker> getMarkers() {
+      Collections.sort(markers);
+      return markers;
+    }
+
+    // Range-related apis
+
+    private static class RangeNode {
+      String label;
+      ReadableFontData data;
+      RangeNode next;
+      int depth;
+      int base; // offset from absolute data start
+      int pos; // offset from base, where we next read a field
+
+      /**
+       * Represent a range.
+       *
+       * @param label
+       *          label to use for the range
+       * @param data
+       *          the data in the range
+       * @param next
+       *          the next node in the change
+       * @param base
+       *          the base of this node as an absolute position in the data
+       *          (includes data.boundOffset())
+       */
+      RangeNode(String label, ReadableFontData data, RangeNode next, int base) {
+        this.label = label;
+        this.data = data;
+        this.next = next;
+        this.depth = next == null ? 0 : next.depth + 1;
+        this.base = base;
+      }
+    }
+
+    @Override
+    public void pushRange(String label, ReadableFontData data) {
+      rangeStack = new RangeNode(label, data, rangeStack, data.dataOffset());
+    }
+
+    @Override
+    public void pushRangeAtOffset(String label, int base) {
+      if (rangeStack == null) {
+        throw new IllegalStateException("can't push offset range without data");
+      }
+      rangeStack = new RangeNode(label, rangeStack.data, rangeStack, base);
+    }
+
+    @Override
+    public void popRange() {
+      if (rangeStack == null) {
+        throw new IllegalStateException("not in a range");
+      }
+      tagRange(rangeStack.label, rangeStack.base, rangeStack.pos, rangeStack.depth);
+      rangeStack = rangeStack.next;
+    }
+
+    @Override
+    public void setRangePosition(int rangePosition) {
+      if (rangeStack == null) {
+        throw new IllegalStateException("not in a range");
+      }
+      rangeStack.pos = rangePosition;
+    }
+
+    @Override
+    public int tagRangeField(FieldType ft, String label) {
+      if (rangeStack == null) {
+        throw new IllegalStateException("not in a range");
+      }
+      ReadableFontData data = rangeStack.data;
+      int base = rangeStack.base;
+      int pos = rangeStack.pos;
+
+      int position = base + pos;
+      int width;
+      int value;
+      String alt;
+      switch (ft) {
+      case OFFSET_NONZERO:
+        value = data.readUShort(pos);
+        if (value == 0) {
+          alt = "NULL";
+          width = 2;
+          break;
+        }
+        // fall through
+      case OFFSET:
+        value = data.readUShort(pos);
+        alt = String.format("#%04x", base + value);
+        width = 2;
+        tagTarget(position, value, base + value, null);
+        break;
+      case OFFSET32:
+        value = data.readULongAsInt(pos);
+        alt = String.format("#%04x", base + value);
+        width = 4;
+        tagTarget(position, value, base + value, null);
+        break;
+      case SHORT:
+        value = data.readUShort(pos);
+        alt = String.valueOf(value);
+        width = 2;
+        break;
+      case SHORT_IGNORED:
+        value = data.readUShort(pos);
+        alt = null;
+        width = 2;
+        break;
+      case SHORT_IGNORED_FFFF:
+        value = data.readUShort(pos);
+        alt = value == 0xffff ? null : String.valueOf(value);
+        width = 2;
+        break;
+      case TAG:
+        value = data.readULongAsInt(pos);
+        alt = Tag.stringValue(value);
+        width = 4;
+        break;
+      case GLYPH:
+        value = data.readUShort(pos);
+        alt = String.valueOf(value);
+        String glyphName = post.glyphName(value);
+        if (glyphName != null) {
+          if (label == null) {
+            label = "glyph name";
+          }
+          label = label + ": " + glyphName;
+        }
+        width = 2;
+        break;
+      default:
+        throw new IllegalStateException("unimplemented field type");
+      }
+      tagField(position, width, value, alt, label);
+      rangeStack.pos += width;
+
+      switch (ft) {
+      case OFFSET:
+      case OFFSET32:
+        value += base;
+        break;
+      case OFFSET_NONZERO:
+        if (value != 0) {
+          value += base;
+        }
+        break;
+      default:
+        break;
+      }
+      return value;
+    }
+  }
+
+  private static class DrawContext {
+    private final Style style;
+    private final Metrics metrics;
+    private final Graphics g; // if null, we are measuring
+    private FontRenderContext frc; // used when measuring
+    private final int x; // current position of 'position' column (margin is to
+    // left)
+    private int y; // current base of line
+    private int lc; // line count
+    private int rangeDepth;
+    private int lastMarkedPosition;
+    private int lastRenderedPosition;
+    private int expectedPosition = -1;
+
+    private DrawContext(Style style, Metrics metrics, Graphics g, int x, int y) {
+      this.style = style;
+      this.metrics = metrics;
+      this.g = g;
+      this.x = x;
+      this.y = y;
+      if (g != null) {
+        frc = ((Graphics2D) g).getFontRenderContext();
+      } else {
+        frc = new FontRenderContext(null, true, false);
+      }
+      this.lc = 0;
+    }
+
+    private void measureLineHeight() {
+      LineMetrics dataMetrics = style.dataFont.getLineMetrics("0123456789abcdef", frc);
+      LineMetrics labelMetrics = style.labelFont.getLineMetrics("ABC", frc);
+
+      int lineHeight = (int) Math.ceil(Math.max(dataMetrics.getHeight(), labelMetrics.getHeight()));
+      int baseline = (int) Math.ceil(Math.max(dataMetrics.getDescent() + dataMetrics.getLeading(),
+          labelMetrics.getDescent() + labelMetrics.getLeading()));
+      int xHeight = (int) Math.ceil(Math.max(dataMetrics.getAscent() - dataMetrics.getLeading(),
+          labelMetrics.getAscent() - labelMetrics.getLeading()) / 2.0 - baseline);
+
+      metrics.lineHeight = lineHeight;
+      metrics.baseline = baseline;
+      metrics.xHeight = xHeight - 3; // this is just not coming out right
+    }
+
+    private Dimension dimension() {
+      metrics.marginWidth += style.columnPad;
+      metrics.positionWidth += style.columnPad;
+      metrics.dataWidth += style.columnPad;
+      metrics.altWidth += style.columnPad;
+      metrics.labelWidth += style.columnPad;
+      metrics.updateTotalWidth();
+
+      int width = metrics.totalWidth + style.columnPad;
+      int height = lc * metrics.lineHeight + style.columnPad;
+
+      return new Dimension(width, height);
+    }
+
+    private void newLine() {
+      lc += 1;
+      y += metrics.lineHeight;
+    }
+
+    private void srcRef(Reference ref) {
+      ref.setSrc(x, y);
+      if (ref.sourcePosition < ref.targetPosition) {
+        return;
+      }
+      drawRef(ref);
+    }
+
+    private void trgRef(Reference ref) {
+      ref.setTrg(x, y);
+      if (ref.sourcePosition > ref.targetPosition) {
+        return;
+      }
+      drawRef(ref);
+    }
+
+    private boolean measuring() {
+      return g == null;
+    }
+
+    private static final Color[] REF_COLORS = { Color.BLUE,
+      Color.RED,
+      Color.BLACK,
+      Color.GREEN,
+      Color.LIGHT_GRAY,
+      Color.PINK,
+      Color.CYAN,
+      Color.DARK_GRAY,
+      Color.MAGENTA,
+      Color.ORANGE };
+
+    private Color colorForM(int m) {
+      return REF_COLORS[m % REF_COLORS.length];
+    }
+
+    private RefWidthFinder refWidthFinder = new RefWidthFinder();
+
+    private void drawRef(Reference ref) {
+      int m = refWidthFinder.add(ref);
+
+      int srcx = ref.srcx - style.marginOffset;
+      int srcy = ref.srcy - metrics.baseline - metrics.xHeight;
+      int trgx = ref.trgx - style.marginOffset;
+      int trgy = ref.trgy - metrics.baseline - metrics.xHeight;
+
+      int margin = -m * style.marginScale;
+      int mx = Math.min(srcx, trgx) - style.marginPad + margin;
+      if (measuring()) {
+        if (-mx > metrics.marginWidth) {
+          metrics.marginWidth = -mx;
+        }
+        return;
+      }
+
+      srcx += metrics.marginWidth;
+      trgx += metrics.marginWidth;
+      mx += metrics.marginWidth;
+
+      g.setColor(colorForM(m));
+      // Debug: g.drawString(ref.sourcePosition + " => " + ref.targetPosition, 0, srcy);
+      g.drawLine(srcx, srcy, mx, srcy);
+      g.drawLine(mx, srcy, mx, trgy);
+      g.drawLine(mx, trgy, trgx, trgy);
+      int[] xpts = { trgx, trgx - 3, trgx - 3 };
+      int[] ypts = { trgy, trgy - 2, trgy + 2 };
+      g.fillPolygon(xpts, ypts, 3);
+    }
+
+    private int updateWidth(String s, Font f, int w) {
+      Rectangle2D bounds = style.dataFont.getStringBounds(s, frc);
+      int width = (int) Math.ceil(bounds.getWidth());
+      if (width > w) {
+        return width;
+      }
+      return w;
+    }
+
+    private void markPosition(int position) {
+      if (position == lastMarkedPosition) {
+        return;
+      }
+      lastMarkedPosition = position;
+      if (position > expectedPosition && expectedPosition != -1) {
+        newLine();
+        String s = "...";
+        if (measuring()) {
+          metrics.positionWidth = updateWidth(s, style.dataFont, metrics.positionWidth);
+        } else {
+          int x = this.x + metrics.marginWidth;
+          int y = this.y - metrics.baseline;
+          g.setFont(style.dataFont);
+          g.setColor(style.positionColor);
+          g.drawString(s, x, y);
+        }
+        expectedPosition = position;
+      }
+      newLine();
+    }
+
+    private void drawRangeBackground() {
+      if (measuring()) {
+        return;
+      }
+      Color[] colors = style.depthColors;
+      int colorIndex = rangeDepth % colors.length;
+      g.setColor(rangeDepth == -1 ? Color.WHITE : colors[colorIndex]);
+      g.fillRect(metrics.marginWidth, y - metrics.lineHeight,
+          metrics.totalWidth - metrics.marginWidth, metrics.lineHeight);
+    }
+
+    private void drawLine(int position, int value, int width, String alt, String label) {
+      markPosition(position);
+      if (lastRenderedPosition == position) {
+        if (alt == null && label == null) {
+          return;
+        }
+        newLine();
+      } else {
+        lastRenderedPosition = position;
+      }
+      drawRangeBackground();
+      int x = this.x + metrics.marginWidth;
+      int y = this.y - metrics.baseline;
+
+      String s = String.format("%04x", position);
+      if (measuring()) {
+        metrics.positionWidth = updateWidth(s, style.dataFont, metrics.positionWidth);
+      } else {
+        g.setFont(style.dataFont);
+        g.setColor(style.positionColor);
+        g.drawString(s, x, y);
+      }
+      x += metrics.positionWidth;
+
+      if (width > 0) {
+        s = String.format("%0" + width * 2 + "x", value);
+        if (measuring()) {
+          metrics.dataWidth = updateWidth(s, style.dataFont, metrics.dataWidth);
+        } else {
+          g.setColor(style.dataColor);
+          g.drawString(s, x, y);
+        }
+      }
+      x += metrics.dataWidth;
+
+      if (alt != null) {
+        if (measuring()) {
+          metrics.altWidth = updateWidth(alt, style.labelFont, metrics.altWidth);
+        } else {
+          g.setFont(style.labelFont);
+          g.setColor(style.altColor);
+          g.drawString(alt, x, y);
+        }
+      }
+      x += metrics.altWidth;
+
+      if (label != null) {
+        if (measuring()) {
+          metrics.labelWidth = updateWidth(label, style.labelFont, metrics.labelWidth);
+        } else {
+          g.setFont(style.labelFont);
+          g.setColor(style.labelColor);
+          g.drawString(label, x, y);
+        }
+      }
+      x += metrics.labelWidth;
+
+      expectedPosition = position + width;
+    }
+
+    private void drawHeader(int position, String header) {
+      markPosition(position);
+      if (lastRenderedPosition == position) {
+        newLine();
+      } else {
+        lastRenderedPosition = position;
+      }
+      if (measuring()) {
+        metrics.headerWidth = updateWidth(header, style.labelFont, metrics.headerWidth);
+      } else {
+        g.setFont(style.labelFont);
+        g.setColor(style.labelColor);
+        g.drawString(header, x + metrics.marginWidth, y - metrics.baseline);
+      }
+    }
+
+    private void rangeTransition(Range range, boolean start) {
+      if (range.length >= 0) {
+        rangeDepth = start ? range.depth : -1;
+      }
+    }
+  }
+
+  private static class Range {
+    private final String name;
+    private final int start;
+    private final int length;
+    private int depth;
+
+    private Range(String name, int start, int length, int depth) {
+      this.name = name;
+      this.start = start;
+      this.length = length;
+      this.depth = depth;
+    }
+
+    private int start() {
+      return start;
+    }
+
+    private int limit() {
+      return start + length;
+    }
+  }
+
+  private static class Reference {
+    private final int sourcePosition;
+    private final int targetPosition;
+    private int srcx, srcy, trgx, trgy;
+    private Reference(int sourcePosition, int targetPosition) {
+      this.sourcePosition = sourcePosition;
+      this.targetPosition = targetPosition;
+    }
+
+    private void setSrc(int x, int y) {
+      srcx = x - 1;
+      srcy = y - 5;
+    }
+
+    private void setTrg(int x, int y) {
+      trgx = x;
+      trgy = y - 5;
+    }
+  }
+
+  private static class WidthUsageRecord {
+    private final Map<Integer, Integer> widthUsage = new HashMap<Integer, Integer>();
+    private int width;
+    private int src;
+    static private final WidthUsageRecord EMPTY = new WidthUsageRecord();
+
+    private static WidthUsageRecord copyWithWidthAdded(WidthUsageRecord other, int width, int src) {
+      WidthUsageRecord current = new WidthUsageRecord();
+      current.width = width;
+      current.src = src;
+
+      current.widthUsage.putAll(other.widthUsage);
+      int count = 0;
+      if (current.widthUsage.containsKey(width)) {
+        count = current.widthUsage.get(width);
+      }
+      current.widthUsage.put(width, count + 1);
+
+      return current;
+    }
+
+    private int lowestEquality(WidthUsageRecord other) {
+      for (int i = 0; this.widthUsage.containsKey(i); i++) {
+        if (other.widthUsage.get(i) == this.widthUsage.get(i)) {
+          return i;
+        }
+      }
+      return other.widthUsage.keySet().size();
+    }
+
+    private int width() {
+      return width;
+    }
+
+    private int src() {
+      return src;
+    }
+
+    @Override
+    public String toString() {
+      return "{width=" + width + " src=" + src + widthUsage.toString() + "}";
+    }
+  }
+
+  private static class RefWidthFinder {
+    private final TreeMap<Integer, WidthUsageRecord> tgt2widthUsage;
+    static private final int MAX_WIDTH = 600;
+
+    private RefWidthFinder() {
+      tgt2widthUsage = new TreeMap<Integer, WidthUsageRecord>();
+    }
+
+    private int add(Reference ref) {
+      int src = ref.sourcePosition;
+      int trg = ref.targetPosition;
+      WidthUsageRecord match = null;
+      if (tgt2widthUsage.containsKey(trg)) {
+        match = tgt2widthUsage.get(trg);
+        if (match.src() <= src) {
+          return match.width();
+        }
+        // Now there is a previous entry with same target position but higher source position value.
+      }
+
+      Entry<Integer, WidthUsageRecord> entry = tgt2widthUsage.floorEntry(src);
+      WidthUsageRecord srcWidthUsage = entry != null ? entry.getValue() : WidthUsageRecord.EMPTY;
+
+      // If there is a match and we haven't returned means there is already shorter range with
+      // same target has been encountered. So ignore that entry and check for the penultimate target.
+      // It is ok to overlap with the matched range.
+      // Remember we are always going in the increasing order of target position.
+      entry = match != null ? tgt2widthUsage.floorEntry(trg - 1) : tgt2widthUsage.lastEntry();
+      WidthUsageRecord lastWidthUsage = entry != null ? entry.getValue() : WidthUsageRecord.EMPTY;
+
+      int width = srcWidthUsage.lowestEquality(lastWidthUsage);
+      if (width > MAX_WIDTH) {
+        width = MAX_WIDTH;
+      }
+      WidthUsageRecord trgWidthUsage = WidthUsageRecord.copyWithWidthAdded(lastWidthUsage, width, src);
+
+      tgt2widthUsage.put(trg, trgWidthUsage);
+      return width;
+    }
+  }
+
+  private static abstract class Marker implements Comparable<Marker> {
+    final int position;
+
+    private Marker(int position) {
+      this.position = position;
+    }
+
+    abstract int order(Marker rhs);
+
+    abstract void draw(DrawContext c);
+
+    @Override
+    public int compareTo(Marker rhs) {
+      int result = this.position - rhs.position;
+      if (result != 0) {
+        return result;
+      }
+      Class<? extends Marker> thisClass = this.getClass();
+      Class<? extends Marker> thatClass = rhs.getClass();
+      result = classOrder(thisClass) - classOrder(thatClass);
+      if (result != 0) {
+        return result;
+      }
+      return order(rhs);
+    }
+
+    private static final Object[] classOrder = { RangeEnd.class, RangeStart.class,
+        ReferenceTarget.class,
+      Field.class, ReferenceSource.class };
+
+    private static int classOrder(Class<? extends Marker> clzz) {
+      for (int i = 0; i < classOrder.length; ++i) {
+        if (classOrder[i] == clzz) {
+          return i;
+        }
+      }
+      throw new IllegalStateException("No order for class: " + clzz);
+    }
+  }
+
+  private static class RangeStart extends Marker {
+    private final Range range;
+
+    private RangeStart(Range range) {
+      super(range.start());
+      this.range = range;
+    }
+
+    @Override
+    void draw(DrawContext c) {
+      c.rangeTransition(range, true);
+      c.drawHeader(range.start(), range.name);
+    }
+
+    @Override
+    int order(Marker rhs) {
+      return range.depth - ((RangeStart) rhs).range.depth;
+    }
+  }
+
+  private static class RangeEnd extends Marker {
+    private final Range range;
+
+    private RangeEnd(Range range) {
+      super(range.limit());
+      this.range = range;
+    }
+
+    @Override
+    void draw(DrawContext c) {
+    }
+
+    @Override
+    int order(Marker rhs) {
+      return ((RangeEnd) rhs).range.depth - range.depth;
+    }
+  }
+
+  private static class Field extends Marker {
+    private final int width;
+    private final int value;
+    private final String alt;
+    private final String label;
+
+    private Field(int position, int width, int value, String alt, String label) {
+      super(position);
+      this.width = width;
+      this.value = value;
+      this.alt = alt;
+      this.label = label;
+    }
+
+    @Override
+    void draw(DrawContext c) {
+      c.drawLine(position, value, width, alt, label);
+    }
+
+    @Override
+    int order(Marker rhs) {
+      return 0; // no default ordering for two fields at same position
+    }
+  }
+
+  private static class ReferenceSource extends Marker {
+    private final Reference ref;
+    private final int value;
+    private final String label;
+
+    private ReferenceSource(Reference ref, int value, String label) {
+      super(ref.sourcePosition);
+      this.ref = ref;
+      this.value = value;
+      this.label = label;
+    }
+
+    @Override
+    void draw(DrawContext c) {
+      c.drawLine(position, value, 2, null, label);
+      c.srcRef(ref);
+    }
+
+    @Override
+    int order(Marker rhs) {
+      // the one with the larger target comes first
+      return ((ReferenceSource) rhs).ref.targetPosition - ref.targetPosition;
+    }
+  }
+
+  private static class ReferenceTarget extends Marker {
+    private final Reference ref;
+
+    private ReferenceTarget(Reference ref) {
+      super(ref.targetPosition);
+      this.ref = ref;
+    }
+
+    @Override
+    void draw(DrawContext c) {
+      c.markPosition(position);
+      c.trgRef(ref);
+    }
+
+    @Override
+    int order(Marker rhs) {
+      // The one with the larger source comes first
+      return ((ReferenceTarget) rhs).ref.sourcePosition - ref.sourcePosition;
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/sample/sfview/package-info.java b/java/src/com/google/typography/font/sfntly/sample/sfview/package-info.java
new file mode 100644
index 0000000..1ce47cc
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/sample/sfview/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * This package contain, the font display tool SFView to display the GSUB sub-tables and
+ * there interdependencies. This is an experimental package. Please treat this code as an
+ * alpha code.
+ *  
+ * @author Cibu Johny
+ */
+package com.google.typography.font.sfntly.sample.sfview;
\ No newline at end of file
diff --git a/java/src/com/google/typography/font/sfntly/table/Table.java b/java/src/com/google/typography/font/sfntly/table/Table.java
index 2873442..3c5b182 100644
--- a/java/src/com/google/typography/font/sfntly/table/Table.java
+++ b/java/src/com/google/typography/font/sfntly/table/Table.java
@@ -31,6 +31,7 @@
 import com.google.typography.font.sfntly.table.core.NameTable;
 import com.google.typography.font.sfntly.table.core.OS2Table;
 import com.google.typography.font.sfntly.table.core.PostScriptTable;
+import com.google.typography.font.sfntly.table.opentype.GSubTable;
 import com.google.typography.font.sfntly.table.truetype.ControlProgramTable;
 import com.google.typography.font.sfntly.table.truetype.ControlValueTable;
 import com.google.typography.font.sfntly.table.truetype.GlyphTable;
@@ -226,7 +227,8 @@
         // break;
         // } else if (tag == GPOS) {
         // break;
-        // } else if (tag == GSUB) {
+      } else if (tag == Tag.GSUB) {
+        return GSubTable.Builder.createBuilder(header, tableData);
         // break;
         // } else if (tag == JSTF) {
         // break;
@@ -234,8 +236,8 @@
         // break;
         // } else if (tag == gasp) {
         // break;
-       } else if (tag == Tag.hdmx) {
-         return HorizontalDeviceMetricsTable.Builder.createBuilder(header, tableData);
+      } else if (tag == Tag.hdmx) {
+        return HorizontalDeviceMetricsTable.Builder.createBuilder(header, tableData);
         // break;
         // } else if (tag == kern) {
         // break;
diff --git a/java/src/com/google/typography/font/sfntly/table/core/PostScriptTable.java b/java/src/com/google/typography/font/sfntly/table/core/PostScriptTable.java
index 1bb8c79..e30db3f 100644
--- a/java/src/com/google/typography/font/sfntly/table/core/PostScriptTable.java
+++ b/java/src/com/google/typography/font/sfntly/table/core/PostScriptTable.java
@@ -384,7 +384,8 @@
   }
   
   public String glyphName(int glyphNum) {
-    if (glyphNum < 0 || glyphNum >= numberOfGlyphs()) {
+    int numberOfGlyphs = numberOfGlyphs();
+    if (numberOfGlyphs > 0 && (glyphNum < 0 || glyphNum >= numberOfGlyphs)) {
       throw new IndexOutOfBoundsException();
     }
     int glyphNameIndex = 0;
@@ -392,6 +393,8 @@
       glyphNameIndex = glyphNum;
     } else if (version() == VERSION_2) {
       glyphNameIndex = this.data.readUShort(Offset.glyphNameIndex.offset + 2 * glyphNum);
+    } else {
+      return null;
     }
     if (glyphNameIndex < NUM_STANDARD_NAMES) {
       return STANDARD_NAMES[glyphNameIndex];
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/AlternateSubst.java b/java/src/com/google/typography/font/sfntly/table/opentype/AlternateSubst.java
new file mode 100644
index 0000000..7e80b14
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/AlternateSubst.java
@@ -0,0 +1,10 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.OneToManySubst;
+
+public class AlternateSubst extends OneToManySubst {
+  AlternateSubst(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/ChainContextSubst.java b/java/src/com/google/typography/font/sfntly/table/opentype/ChainContextSubst.java
new file mode 100644
index 0000000..59a1131
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/ChainContextSubst.java
@@ -0,0 +1,171 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubClassSetArray;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubGenericRuleSet;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubRuleSetArray;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.InnerArraysFmt3;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordList;
+
+public class ChainContextSubst extends SubstSubtable {
+  private final ChainSubRuleSetArray ruleSets;
+  private final ChainSubClassSetArray classSets;
+  public final InnerArraysFmt3 fmt3Array;
+
+  // //////////////
+  // Constructors
+
+  ChainContextSubst(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    switch (format) {
+    case 1:
+      ruleSets = new ChainSubRuleSetArray(data, headerSize(), dataIsCanonical);
+      classSets = null;
+      fmt3Array = null;
+      break;
+    case 2:
+      ruleSets = null;
+      classSets = new ChainSubClassSetArray(data, headerSize(), dataIsCanonical);
+      fmt3Array = null;
+      break;
+    case 3:
+      ruleSets = null;
+      classSets = null;
+      fmt3Array = new InnerArraysFmt3(data, headerSize(), dataIsCanonical);
+      break;
+    default:
+      throw new IllegalStateException("Subt format value is " + format + " (should be 1 or 2).");
+    }
+  }
+
+  // //////////////////////////////////
+  // Methods redirected to the array
+
+  public ChainSubRuleSetArray fmt1Table() {
+    switch (format) {
+    case 1:
+      return ruleSets;
+    default:
+      throw new IllegalArgumentException("unexpected format table requested: " + format);
+    }
+  }
+
+  public ChainSubClassSetArray fmt2Table() {
+    switch (format) {
+    case 2:
+      return classSets;
+    default:
+      throw new IllegalArgumentException("unexpected format table requested: " + format);
+    }
+  }
+
+  public InnerArraysFmt3 fmt3Table() {
+    switch (format) {
+    case 3:
+      return fmt3Array;
+    default:
+      throw new IllegalArgumentException("unexpected format table requested: " + format);
+    }
+  }
+
+  public NumRecordList recordList() {
+    switch (format) {
+    case 1:
+      return ruleSets.recordList;
+    case 2:
+      return classSets.recordList;
+    default:
+      return null;
+    }
+  }
+
+  public ChainSubGenericRuleSet<?> subTableAt(int index) {
+    switch (format) {
+    case 1:
+      return ruleSets.subTableAt(index);
+    case 2:
+      return classSets.subTableAt(index);
+    default:
+      return null;
+    }
+  }
+
+
+
+  // //////////////////////////////////
+  // Methods specific to this class
+
+  public CoverageTable coverage() {
+    switch (format) {
+    case 1:
+      return ruleSets.coverage;
+    case 2:
+      return classSets.coverage;
+    default:
+      return null;
+    }
+  }
+
+  public ClassDefTable backtrackClassDef() {
+    return (format == 2) ? classSets.backtrackClassDef : null;
+  }
+
+  public ClassDefTable inputClassDef() {
+    return (format == 2) ? classSets.inputClassDef : null;
+  }
+
+  public ClassDefTable lookAheadClassDef() {
+    return (format == 2) ? classSets.lookAheadClassDef : null;
+  }
+
+  protected static class Builder extends SubstSubtable.Builder<SubstSubtable> {
+    private final ChainSubRuleSetArray.Builder arrayBuilder;
+
+    protected Builder() {
+      super();
+      arrayBuilder = new ChainSubRuleSetArray.Builder();
+    }
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+      arrayBuilder = new ChainSubRuleSetArray.Builder(data, dataIsCanonical);
+    }
+
+    protected Builder(SubstSubtable subTable) {
+      ChainContextSubst ligSubst = (ChainContextSubst) subTable;
+      arrayBuilder = new ChainSubRuleSetArray.Builder(ligSubst.ruleSets);
+    }
+
+    // ///////////////////////////////
+    // Public methods to serialize
+
+    @Override
+    public int subDataSizeToSerialize() {
+      return arrayBuilder.subDataSizeToSerialize();
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      return arrayBuilder.subSerialize(newData);
+    }
+
+    // /////////////////////////////////
+    // must implement abstract methods
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    @Override
+    public void subDataSet() {
+      arrayBuilder.subDataSet();
+    }
+
+    @Override
+    public ChainContextSubst subBuildTable(ReadableFontData data) {
+      return new ChainContextSubst(data, 0, true);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/ClassDefTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/ClassDefTable.java
new file mode 100644
index 0000000..7df3832
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/ClassDefTable.java
@@ -0,0 +1,104 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.opentype.classdef.InnerArrayFmt1;
+import com.google.typography.font.sfntly.table.opentype.component.RangeRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.RecordsTable;
+
+public class ClassDefTable extends SubstSubtable {
+  public final RecordsTable<?> array;
+  private boolean dataIsCanonical;
+
+  // //////////////
+  // Constructors
+
+  public ClassDefTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    this.dataIsCanonical = dataIsCanonical;
+
+    switch (format) {
+    case 1:
+      array = new InnerArrayFmt1(data, headerSize(), dataIsCanonical);
+      break;
+    case 2:
+      array = new RangeRecordTable(data, headerSize(), dataIsCanonical);
+      break;
+    default:
+      throw new IllegalArgumentException("class def format " + format + " unexpected");
+    }
+  }
+
+  // ////////////////////////////////////////
+  // Utility methods specific to this class
+
+  public InnerArrayFmt1 fmt1Table() {
+    switch (format) {
+    case 1:
+      return (InnerArrayFmt1) array;
+    default:
+      throw new IllegalArgumentException("unexpected format table requested: " + format);
+    }
+  }
+
+  public RangeRecordTable fmt2Table() {
+    switch (format) {
+    case 2:
+      return (RangeRecordTable) array;
+    default:
+      throw new IllegalArgumentException("unexpected format table requested: " + format);
+    }
+  }
+
+  public static class Builder extends SubstSubtable.Builder<ClassDefTable> {
+    private final RecordsTable.Builder<?, ?> arrayBuilder;
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+      switch (format) {
+      case 1:
+        arrayBuilder = new InnerArrayFmt1.Builder(data, headerSize(), dataIsCanonical);
+        break;
+      case 2:
+        arrayBuilder = new RangeRecordTable.Builder(data, headerSize(), dataIsCanonical);
+        break;
+      default:
+        throw new IllegalArgumentException("class def format " + format + " unexpected");
+      }
+    }
+
+    protected Builder(ClassDefTable table) {
+      this(table.readFontData(), table.dataIsCanonical);
+    }
+
+    @Override
+    public int subDataSizeToSerialize() {
+      return super.subDataSizeToSerialize() + arrayBuilder.subDataSizeToSerialize();
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      int newOffset = super.subSerialize(newData);
+      return arrayBuilder.subSerialize(newData.slice(newOffset));
+    }
+
+    // ///////////////////
+    // Overriden methods
+
+    @Override
+    public ClassDefTable subBuildTable(ReadableFontData data) {
+      return new ClassDefTable(data, 0, false);
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return super.subReadyToSerialize() && true;
+    }
+
+    @Override
+    public void subDataSet() {
+      super.subDataSet();
+      arrayBuilder.subDataSet();
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/ContextSubst.java b/java/src/com/google/typography/font/sfntly/table/opentype/ContextSubst.java
new file mode 100644
index 0000000..301f52b
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/ContextSubst.java
@@ -0,0 +1,125 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordList;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.DoubleRecordTable;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubClassSetArray;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubGenericRuleSet;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubRuleSetArray;
+
+public class ContextSubst extends SubstSubtable {
+  private final SubRuleSetArray ruleSets;
+  private SubClassSetArray classSets;
+
+  // //////////////
+  // Constructors
+
+  ContextSubst(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    switch (format) {
+    case 1:
+      ruleSets = new SubRuleSetArray(data, headerSize(), dataIsCanonical);
+      classSets = null;
+      break;
+    case 2:
+      ruleSets = null;
+      classSets = new SubClassSetArray(data, headerSize(), dataIsCanonical);
+      break;
+    default:
+      throw new IllegalStateException("Subt format value is " + format + " (should be 1 or 2).");
+    }
+  }
+
+  // //////////////////////////////////
+  // Methods redirected to the array
+
+  public SubRuleSetArray fmt1Table() {
+    switch (format) {
+    case 1:
+      return ruleSets;
+    default:
+      throw new IllegalArgumentException("unexpected format table requested: " + format);
+    }
+  }
+
+  public SubClassSetArray fmt2Table() {
+    switch (format) {
+    case 2:
+      return classSets;
+    default:
+      throw new IllegalArgumentException("unexpected format table requested: " + format);
+    }
+  }
+
+  public NumRecordList recordList() {
+    return (format == 1) ? ruleSets.recordList : classSets.recordList;
+  }
+
+  public SubGenericRuleSet<? extends DoubleRecordTable> subTableAt(int index) {
+    return (format == 1) ? ruleSets.subTableAt(index) : classSets.subTableAt(index);
+  }
+
+  // //////////////////////////////////
+  // Methods specific to this class
+
+  public CoverageTable coverage() {
+    return (format == 1) ? ruleSets.coverage : classSets.coverage;
+  }
+
+  public ClassDefTable classDef() {
+    return (format == 2) ? classSets.classDef : null;
+  }
+
+  public static class Builder extends SubstSubtable.Builder<SubstSubtable> {
+    private final SubRuleSetArray.Builder arrayBuilder;
+
+    protected Builder() {
+      super();
+      arrayBuilder = new SubRuleSetArray.Builder();
+    }
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+      arrayBuilder = new SubRuleSetArray.Builder(data, dataIsCanonical);
+    }
+
+    protected Builder(SubstSubtable subTable) {
+      ContextSubst ligSubst = (ContextSubst) subTable;
+      arrayBuilder = new SubRuleSetArray.Builder(ligSubst.ruleSets);
+    }
+
+    /**
+     * Even though public, not to be used by the end users. Made public only
+     * make it available to packages under
+     * {@code com.google.typography.font.sfntly.table.opentype}.
+     */
+    @Override
+    public int subDataSizeToSerialize() {
+      return arrayBuilder.subDataSizeToSerialize();
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      return arrayBuilder.subSerialize(newData);
+    }
+
+    // /////////////////////////////////
+    // must implement abstract methods
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    @Override
+    public void subDataSet() {
+      arrayBuilder.subDataSet();
+    }
+
+    @Override
+    public ContextSubst subBuildTable(ReadableFontData data) {
+      return new ContextSubst(data, 0, true);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/CoverageTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/CoverageTable.java
new file mode 100644
index 0000000..23e4a07
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/CoverageTable.java
@@ -0,0 +1,103 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.RangeRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.RecordsTable;
+
+public class CoverageTable extends SubstSubtable {
+  public final RecordsTable<?> array;
+
+  // //////////////
+  // Constructors
+
+  public CoverageTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    switch (format) {
+    case 1:
+      array = new NumRecordTable(data, headerSize(), dataIsCanonical);
+      break;
+    case 2:
+      array = new RangeRecordTable(data, headerSize(), dataIsCanonical);
+      break;
+    default:
+      throw new IllegalArgumentException("coverage format " + format + " unexpected");
+    }
+  }
+
+  // ////////////////////////////////////////
+  // Utility methods specific to this class
+
+  public NumRecordTable fmt1Table() {
+    switch (format) {
+    case 1:
+      return (NumRecordTable) array;
+    default:
+      throw new IllegalArgumentException("unexpected format table requested: " + format);
+    }
+  }
+
+  public RangeRecordTable fmt2Table() {
+    switch (format) {
+    case 2:
+      return (RangeRecordTable) array;
+    default:
+      throw new IllegalArgumentException("unexpected format table requested: " + format);
+    }
+  }
+
+  public static class Builder extends SubstSubtable.Builder<CoverageTable> {
+    private final RecordsTable.Builder<?, ?> arrayBuilder;
+
+    public Builder() {
+      super();
+      arrayBuilder = new NumRecordTable.Builder();
+    }
+
+    public Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+      switch (format) {
+      case 1:
+        arrayBuilder = new NumRecordTable.Builder(data, headerSize(), dataIsCanonical);
+        break;
+      case 2:
+        arrayBuilder = new RangeRecordTable.Builder(data, headerSize(), dataIsCanonical);
+        break;
+      default:
+        throw new IllegalArgumentException("coverage format " + format + " unexpected");
+      }
+    }
+
+    public Builder(CoverageTable table) {
+      this(table.readFontData(), table.dataIsCanonical);
+    }
+
+    @Override
+    public int subDataSizeToSerialize() {
+      return super.subDataSizeToSerialize() + arrayBuilder.subDataSizeToSerialize();
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      int newOffset = super.subSerialize(newData);
+      return arrayBuilder.subSerialize(newData.slice(newOffset));
+    }
+
+    @Override
+    protected CoverageTable subBuildTable(ReadableFontData data) {
+      return new CoverageTable(data, 0, false);
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return super.subReadyToSerialize();
+    }
+
+    @Override
+    public void subDataSet() {
+      super.subDataSet();
+      arrayBuilder.subDataSet();
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/ExtensionSubst.java b/java/src/com/google/typography/font/sfntly/table/opentype/ExtensionSubst.java
new file mode 100644
index 0000000..969e088
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/ExtensionSubst.java
@@ -0,0 +1,63 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.GsubLookupType;
+
+public class ExtensionSubst extends SubstSubtable {
+  private static final int LOOKUP_TYPE_OFFSET = 0;
+  private static final int LOOKUP_OFFSET_OFFSET = 2;
+
+  final GsubLookupType lookupType;
+  final int lookupOffset;
+
+  ExtensionSubst(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    if (format != 1) {
+      throw new IllegalArgumentException("illegal extension format " + format);
+    }
+    lookupType = GsubLookupType.forTypeNum(
+        data.readUShort(base + headerSize() + LOOKUP_TYPE_OFFSET));
+    lookupOffset = data.readULongAsInt(base + headerSize() + LOOKUP_OFFSET_OFFSET);
+  }
+
+  public GsubLookupType lookupType() {
+    return lookupType;
+  }
+
+  public SubstSubtable subTable() {
+    ReadableFontData data = this.data.slice(lookupOffset);
+    switch (lookupType) {
+    case GSUB_LIGATURE:
+      return new LigatureSubst(data, 0, dataIsCanonical);
+    case GSUB_SINGLE:
+      return new SingleSubst(data, 0, dataIsCanonical);
+    case GSUB_MULTIPLE:
+      return new MultipleSubst(data, 0, dataIsCanonical);
+    case GSUB_ALTERNATE:
+      return new AlternateSubst(data, 0, dataIsCanonical);
+    case GSUB_CONTEXTUAL:
+      return new ContextSubst(data, 0, dataIsCanonical);
+    case GSUB_CHAINING_CONTEXTUAL:
+      return new ChainContextSubst(data, 0, dataIsCanonical);
+    case GSUB_REVERSE_CHAINING_CONTEXTUAL_SINGLE:
+      return new ReverseChainSingleSubst(data, 0, dataIsCanonical);
+    default:
+      throw new IllegalArgumentException("LookupType is " + lookupType);
+    }
+  }
+
+  public static class Builder extends SubstSubtable.Builder<SubstSubtable> {
+    protected Builder() {
+      super();
+    }
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    @Override
+    public SubstSubtable subBuildTable(ReadableFontData data) {
+      return null;
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/FeatureListTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/FeatureListTable.java
new file mode 100644
index 0000000..c5f79f0
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/FeatureListTable.java
@@ -0,0 +1,59 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.TagOffsetsTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class FeatureListTable extends TagOffsetsTable<FeatureTable> {
+
+  FeatureListTable(ReadableFontData data, boolean dataIsCanonical) {
+    super(data, dataIsCanonical);
+  }
+
+  @Override
+  protected FeatureTable readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new FeatureTable(data, dataIsCanonical);
+  }
+
+  static class Builder extends TagOffsetsTable.Builder<FeatureListTable, FeatureTable> {
+
+    protected Builder() {
+      super();
+    }
+
+    protected Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data, 0, false);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<FeatureTable> createSubTableBuilder(
+        ReadableFontData data, int tag, boolean dataIsCanonical) {
+      return new FeatureTable.Builder(data, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<FeatureTable> createSubTableBuilder() {
+      return new FeatureTable.Builder();
+    }
+
+    @Override
+    protected FeatureListTable readTable(
+        ReadableFontData data, int baseUnused, boolean dataIsCanonical) {
+      return new FeatureListTable(data, dataIsCanonical);
+    }
+
+    @Override
+    protected void initFields() {
+    }
+
+    @Override
+    public int fieldCount() {
+      return 0;
+    }
+  }
+
+  @Override
+  public int fieldCount() {
+    return 0;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/FeatureTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/FeatureTable.java
new file mode 100644
index 0000000..88ceb23
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/FeatureTable.java
@@ -0,0 +1,66 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecord;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.RecordList;
+import com.google.typography.font.sfntly.table.opentype.component.RecordsTable;
+
+public class FeatureTable extends RecordsTable<NumRecord> {
+  private static final int FIELD_COUNT = 1;
+  private static final int FEATURE_PARAMS_INDEX = 0;
+  private static final int FEATURE_PARAMS_DEFAULT = 0;
+
+  FeatureTable(ReadableFontData data, boolean dataIsCanonical) {
+    super(data, dataIsCanonical);
+  }
+
+  @Override
+  protected RecordList<NumRecord> createRecordList(ReadableFontData data) {
+    return new NumRecordList(data);
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+
+  static class Builder extends
+  RecordsTable.Builder<FeatureTable, NumRecord> {
+
+    Builder() {
+      super();
+    }
+
+    Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    @Override
+    protected FeatureTable readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      if (base != 0) {
+        throw new UnsupportedOperationException();
+      }
+      return new FeatureTable(data, dataIsCanonical);
+    }
+
+    @Override
+    protected RecordList<NumRecord> readRecordList(ReadableFontData data, int base) {
+      if (base != 0) {
+        throw new UnsupportedOperationException();
+      }      
+      return new NumRecordList(data);
+    }
+
+    @Override
+    public int fieldCount() {
+      return FIELD_COUNT;
+    }
+
+    @Override
+    protected void initFields() {
+      setField(FEATURE_PARAMS_INDEX, FEATURE_PARAMS_DEFAULT);
+    }
+  }
+}
+
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/FeatureTag.java b/java/src/com/google/typography/font/sfntly/table/opentype/FeatureTag.java
new file mode 100644
index 0000000..044f927
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/FeatureTag.java
@@ -0,0 +1,196 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.Tag;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+enum FeatureTag {
+  aalt("Access All Alternates"),
+  abvf("Above-base Forms"),
+  abvm("Above-base Mark Positioning"),
+  abvs("Above-base Substitutions"),
+  afrc("Alternative Fractions"),
+  akhn("Akhands"),
+  blwf("Below-base Forms"),
+  blwm("Below-base Mark Positioning"),
+  blws("Below-base Substitutions"),
+  calt("Contextual Alternates"),
+  // Note, 'case' collides with a reserved word in java,
+  // so the enum constant has a trailing underscore
+  case_("case", "Case-Sensitive Forms"),
+  ccmp("Glyph Composition / Decomposition"),
+  cfar("Conjunct Form After Ro"),
+  cjct("Conjunct Forms"),
+  clig("Contextual Ligatures"),
+  cpct("Centered CJK Punctuation"),
+  cpsp("Capital Spacing"),
+  cswh("Contextual Swash"),
+  curs("Cursive Positioning"),
+  cv01("Character Variants 1"),
+  cv02("Character Variants 2"),
+  cv03("Character Variants 3"),
+  cv04("Character Variants 4"),
+  cv05("Character Variants 5"),
+  cv06("Character Variants 6"),
+  cv07("Character Variants 7"),
+  cv08("Character Variants 8"),
+  cv09("Character Variants 9"),
+  cv10("Character Variants 10"),
+  // continues to cv99, omitted here
+  c2pc("Petite Capitals From Capitals"),
+  c2sc("Small Capitals From Capitals"),
+  dist("Distances"),
+  dlig("Discretionary Ligatures"),
+  dnom("Denominators"),
+  expt("Expert Forms"),
+  falt("Final Glyph on Line Alternates"),
+  fin2("Terminal Forms #2"),
+  fin3("Terminal Forms #3"),
+  fina("Terminal Forms"),
+  frac("Fractions"),
+  fwid("Full Widths"),
+  half("Half Forms"),
+  haln("Halant Forms"),
+  halt("Alternate Half Widths"),
+  hist("Historical Forms"),
+  hkna("Horizontal Kana Alternates"),
+  hlig("Historical Ligatures"),
+  hngl("Hangul"),
+  hojo("Hojo Kanji Forms (JIS X 0212-1990 Kanji Forms)"),
+  hwid("Half Widths"),
+  init("Initial Forms"),
+  isol("Isolated Forms"),
+  ital("Italics"),
+  jalt("Justification Alternates"),
+  jp78("JIS78 Forms"),
+  jp83("JIS83 Forms"),
+  jp90("JIS90 Forms"),
+  jp04("JIS2004 Forms"),
+  kern("Kerning"),
+  lfbd("Left Bounds"),
+  liga("Standard Ligatures"),
+  ljmo("Leading Jamo Forms"),
+  lnum("Lining Figures"),
+  locl("Localized Forms"),
+  ltra("Left-to-right alternates"),
+  ltrm("Left-to-right mirrored forms"),
+  mark("Mark Positioning"),
+  med2("Medial Forms #2"),
+  medi("Medial Forms"),
+  mgrk("Mathematical Greek"),
+  mkmk("Mark to Mark Positioning"),
+  mset("Mark Positioning via Substitution"),
+  nalt("Alternate Annotation Forms"),
+  nlck("NLC Kanji Forms"),
+  nukt("Nukta Forms"),
+  numr("Numerators"),
+  onum("Oldstyle Figures"),
+  opbd("Optical Bounds"),
+  ordn("Ordinals"),
+  ornm("Ornaments"),
+  palt("Proportional Alternate Widths"),
+  pcap("Petite Capitals"),
+  pkna("Proportional Kana"),
+  pnum("Proportional Figures"),
+  pref("Pre-Base Forms"),
+  pres("Pre-base Substitutions"),
+  pstf("Post-base Forms"),
+  psts("Post-base Substitutions"),
+  pwid("Proportional Widths"),
+  qwid("Quarter Widths"),
+  rand("Randomize"),
+  rkrf("Rakar Forms"),
+  rlig("Required Ligatures"),
+  rphf("Reph Forms"),
+  rtbd("Right Bounds"),
+  rtla("Right-to-left alternates"),
+  rtlm("Right-to-left mirrored forms"),
+  ruby("Ruby Notation Forms"),
+  salt("Stylistic Alternates"),
+  sinf("Scientific Inferiors"),
+  size("Optical size"),
+  smcp("Small Capitals"),
+  smpl("Simplified Forms"),
+  ss01("Stylistic Set 1"),
+  ss02("Stylistic Set 2"),
+  ss03("Stylistic Set 3"),
+  ss04("Stylistic Set 4"),
+  ss05("Stylistic Set 5"),
+  ss06("Stylistic Set 6"),
+  ss07("Stylistic Set 7"),
+  ss08("Stylistic Set 8"),
+  ss09("Stylistic Set 9"),
+  ss10("Stylistic Set 10"),
+  ss11("Stylistic Set 11"),
+  ss12("Stylistic Set 12"),
+  ss13("Stylistic Set 13"),
+  ss14("Stylistic Set 14"),
+  ss15("Stylistic Set 15"),
+  ss16("Stylistic Set 16"),
+  ss17("Stylistic Set 17"),
+  ss18("Stylistic Set 18"),
+  ss19("Stylistic Set 19"),
+  ss20("Stylistic Set 20"),
+  subs("Subscript"),
+  sups("Superscript"),
+  swsh("Swash"),
+  titl("Titling"),
+  tjmo("Trailing Jamo Forms"),
+  tnam("Traditional Name Forms"),
+  tnum("Tabular Figures"),
+  trad("Traditional Forms"),
+  twid("Third Widths"),
+  unic("Unicase"),
+  valt("Alternate Vertical Metrics"),
+  vatu("Vattu Variants"),
+  vert("Vertical Writing"),
+  vhal("Alternate Vertical Half Metrics"),
+  vjmo("Vowel Jamo Forms"),
+  vkna("Vertical Kana Alternates"),
+  vkrn("Vertical Kerning"),
+  vpal("Proportional Alternate Vertical Metrics"),
+  vrt2("Vertical Alternates and Rotation"),
+  zero("Slashed Zero");
+
+  private static Map<Integer, FeatureTag> tagMap;
+
+  private FeatureTag(String name) {
+    this.tag = Tag.intValue(name());
+    this.name = name;
+  }
+
+  private FeatureTag(String tagName, String name) {
+    this.tag = Tag.intValue(tagName);
+    this.name = name;
+  }
+
+  public static FeatureTag forTagValue(int value) {
+    synchronized (FeatureTag.class) {
+      if (tagMap == null) {
+        Map<Integer, FeatureTag> map = new HashMap<Integer, FeatureTag>();
+        for (FeatureTag tag : values()) {
+          map.put(tag.tag(), tag);
+        }
+        tagMap = map;
+      }
+      return tagMap.get(value);
+    }
+  }
+
+  private int tag() {
+    return tag;
+  }
+
+  public String longName() {
+    return name;
+  }
+
+  private final int tag;
+  private final String name;
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/GSubTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/GSubTable.java
new file mode 100644
index 0000000..a2fd0f4
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/GSubTable.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2010 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.
+ */
+
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.Header;
+import com.google.typography.font.sfntly.table.Table;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A GSub table.
+ */
+public class GSubTable extends Table {
+  private final GsubCommonTable gsub;
+  private final AtomicReference<ScriptListTable>
+      scriptListTable = new AtomicReference<ScriptListTable>();
+  private final AtomicReference<FeatureListTable>
+      featureListTable = new AtomicReference<FeatureListTable>();
+  private final AtomicReference<LookupListTable>
+      lookupListTable = new AtomicReference<LookupListTable>();
+
+  /**
+   * Constructor.
+   *
+   * @param header
+   *          header for the table
+   * @param data
+   *          data for the table
+   */
+  private GSubTable(Header header, ReadableFontData data, boolean dataIsCanonical) {
+    super(header, data);
+    gsub = new GsubCommonTable(data, dataIsCanonical);
+  }
+
+  /**
+   * Return information about the script tables in this GSUB table.
+   *
+   * @return the ScriptList
+   */
+  public ScriptListTable scriptList() {
+    if (scriptListTable.get() == null) {
+      scriptListTable.compareAndSet(null, gsub.createScriptList());
+    }
+    return scriptListTable.get();
+  }
+
+  /**
+   * Return information about the feature tables in this GSUB table.
+   *
+   * @return the FeatureList
+   */
+  public FeatureListTable featureList() {
+    if (featureListTable.get() == null) {
+      featureListTable.compareAndSet(null, gsub.createFeatureList());
+    }
+    return featureListTable.get();
+  }
+
+  /**
+   * Return information about the lookup tables in this GSUB table.
+   *
+   * @return the LookupList
+   */
+  public LookupListTable lookupList() {
+    if (lookupListTable.get() == null) {
+      lookupListTable.compareAndSet(null, gsub.createLookupList());
+    }
+    return lookupListTable.get();
+  }
+
+  /**
+   * GSUB Table Builder.
+   */
+  public static class Builder extends Table.Builder<GSubTable> {
+    private final GsubCommonTable.Builder gsub;
+
+    /**
+     * Creates a new builder using the header information and data provided.
+     *
+     * @param header
+     *          the header information
+     * @param data
+     *          the data holding the table
+     * @return a new builder
+     */
+    public static Builder createBuilder(Header header, WritableFontData data) {
+      return new Builder(header, data);
+    }
+
+    /**
+     * Constructor. This constructor will try to maintain the data as readable
+     * but if editing operations are attempted then a writable copy will be made
+     * the readable data will be discarded.
+     *
+     * @param header
+     *          the table header
+     * @param data
+     *          the readable data for the table
+     */
+    private Builder(Header header, ReadableFontData data) {
+      super(header, data);
+      gsub = new GsubCommonTable.Builder(data, false);
+    }
+
+    @Override
+    protected int subSerialize(WritableFontData newData) {
+      return gsub.subSerialize(newData);
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return gsub.subReadyToSerialize();
+    }
+
+    @Override
+    protected int subDataSizeToSerialize() {
+      return 0; // TODO(cibu): need to implement using gsub
+    }
+
+    @Override
+    protected void subDataSet() {
+      // TODO(cibu): need to implement using gsub
+    }
+
+    @Override
+    protected GSubTable subBuildTable(ReadableFontData data) {
+      return new GSubTable(this.header(), data, false);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/GsubCommonTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/GsubCommonTable.java
new file mode 100644
index 0000000..8100ae1
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/GsubCommonTable.java
@@ -0,0 +1,58 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+
+class GsubCommonTable extends LayoutCommonTable<GsubLookupTable> {
+
+  GsubCommonTable(ReadableFontData data, boolean dataIsCanonical) {
+    super(data, dataIsCanonical);
+  }
+
+  @Override
+  protected LookupListTable createLookupList() {
+    return super.createLookupList();
+  }
+
+  @Override
+  protected LookupListTable handleCreateLookupList(ReadableFontData data, boolean dataIsCanonical) {
+    return new LookupListTable(data, dataIsCanonical);
+  }
+
+  static class Builder extends LayoutCommonTable.Builder<GsubLookupTable> {
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    protected Builder() {
+      super(null, false);
+    }
+
+    @Override
+    protected LookupListTable handleCreateLookupList(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new LookupListTable(data, dataIsCanonical);
+    }
+
+    @Override
+    protected GsubCommonTable subBuildTable(ReadableFontData data) {
+      return new GsubCommonTable(data, true);
+    }
+
+    @Override
+    protected LookupListTable.Builder createLookupListBuilder() {
+      return new LookupListTable.Builder();
+    }
+
+    @Override
+    protected int subDataSizeToSerialize() {
+      // TODO(cibu): do real implementation
+      return 0;
+    }
+
+    @Override
+    protected void subDataSet() {
+      // TODO(cibu): do real implementation
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/GsubLookupSubTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/GsubLookupSubTable.java
new file mode 100644
index 0000000..44acdc0
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/GsubLookupSubTable.java
@@ -0,0 +1,37 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.GsubLookupType;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+abstract class GsubLookupSubTable extends LookupSubTable {
+
+  protected GsubLookupSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    super(data, dataIsCanonical);
+  }
+
+  @Override
+  public abstract Builder<? extends GsubLookupSubTable> builder();
+
+  @Override
+  public abstract GsubLookupType lookupType();
+
+  static abstract class Builder<T extends GsubLookupSubTable>
+      extends LookupSubTable.Builder<T> {
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    protected Builder(T table) {
+      super(table);
+    }
+
+    @Override
+    public abstract GsubLookupType lookupType();
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/GsubLookupTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/GsubLookupTable.java
new file mode 100644
index 0000000..e8835ec
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/GsubLookupTable.java
@@ -0,0 +1,29 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+abstract class GsubLookupTable extends LookupTable {
+
+  protected GsubLookupTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  static abstract class Builder<T extends GsubLookupTable> extends LookupTable.Builder {
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    protected Builder() {
+    }
+
+    protected Builder(T table) {
+      super(table);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/LangSysTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/LangSysTable.java
new file mode 100644
index 0000000..c4eb8cc
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/LangSysTable.java
@@ -0,0 +1,74 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecord;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.RecordList;
+import com.google.typography.font.sfntly.table.opentype.component.RecordsTable;
+
+public class LangSysTable extends RecordsTable<NumRecord> {
+  private static final int FIELD_COUNT = 2;
+
+  private static final int LOOKUP_ORDER_INDEX = 0;
+  private static final int LOOKUP_ORDER_CONST = 0;
+
+  private static final int REQ_FEATURE_INDEX_INDEX = 1;
+  private static final int NO_REQ_FEATURE = 0xffff;
+
+  LangSysTable(ReadableFontData data, boolean dataIsCanonical) {
+    super(data, dataIsCanonical);
+    if (getField(LOOKUP_ORDER_INDEX) != LOOKUP_ORDER_CONST) {
+      throw new IllegalArgumentException();
+    }
+  }
+
+  @Override
+  protected RecordList<NumRecord> createRecordList(ReadableFontData data) {
+    return new NumRecordList(data);
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+
+  static class Builder extends RecordsTable.Builder<LangSysTable, NumRecord> {
+    Builder() {
+      super();
+    }
+
+    Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    // //////////////////////////////
+    // private methods to update
+
+    @Override
+    protected void initFields() {
+      setField(LOOKUP_ORDER_INDEX, LOOKUP_ORDER_CONST);
+      setField(REQ_FEATURE_INDEX_INDEX, NO_REQ_FEATURE);
+    }
+
+    @Override
+    protected LangSysTable readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      if (base != 0) {
+        throw new UnsupportedOperationException();
+      }
+      return new LangSysTable(data, dataIsCanonical);
+    }
+
+    @Override
+    protected RecordList<NumRecord> readRecordList(ReadableFontData data, int base) {
+      if (base != 0) {
+        throw new UnsupportedOperationException();
+      }
+      return new NumRecordList(data);
+    }
+
+    @Override
+    public int fieldCount() {
+      return FIELD_COUNT;
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/LanguageTag.java b/java/src/com/google/typography/font/sfntly/table/opentype/LanguageTag.java
new file mode 100644
index 0000000..01b2952
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/LanguageTag.java
@@ -0,0 +1,453 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.Tag;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+enum LanguageTag {
+  ABA("Abaza", "abq"),
+  ABK("Abkhazian", "abk"),
+  ADY("Adyghe", "ady"),
+  AFK("Afrikaans", "afr"),
+  AFR("Afar", "aar"),
+  AGW("Agaw", "ahg"),
+  ALS("Alsatian", "gsw"),
+  ALT("Altai", "atv,alt"),
+  AMH("Amharic", "amh"),
+  APPH("Phonetic transcription—Americanist conventions", ""),
+  ARA("Arabic", "ara"),
+  ARI("Aari", "aiw"),
+  ARK("Arakanese", "mhv,rmz,rki"),
+  ASM("Assamese", "asm"),
+  ATH("Athapaskan",
+      "apk,apj,apl,apm,apw,nav,bea,sek,bcr,caf,crx,clc,gwi,haa,chp,dgr,scs,xsl,srs,ing,hoi,koy,hup,ktw,mvb,wlk,coq,ctc,gce,tol,tuu,kkz,tgx,tht,aht,tfn,taa,tau,tcb,kuu,tce,ttm,txc"),
+  AVR("Avar", "ava"),
+  AWA("Awadhi", "awa"),
+  AYM("Aymara", "aym"),
+  AZE("Azeri", "aze"),
+  BAD("Badaga", "bfq"),
+  BAG("Baghelkhandi", "bfy"),
+  BAL("Balkar", "krc"),
+  BAU("Baule", "bci"),
+  BBR("Berber", ""),
+  BCH("Bench", "bcq"),
+  BCR("Bible Cree", ""),
+  BEL("Belarussian", "bel"),
+  BEM("Bemba", "bem"),
+  BEN("Bengali", "ben"),
+  BGR("Bulgarian", "bul"),
+  BHI("Bhili", "bhi,bhb"),
+  BHO("Bhojpuri", "bho"),
+  BIK("Bikol", "bik"),
+  BIL("Bilen", "byn"),
+  BKF("Blackfoot", "bla"),
+  BLI("Balochi", "bal"),
+  BLN("Balante", "bjt,ble"),
+  BLT("Balti", "bft"),
+  BMB("Bambara", "bam"),
+  BML("Bamileke", ""),
+  BOS("Bosnian", "bos"),
+  BRE("Breton", "bre"),
+  BRH("Brahui", "brh"),
+  BRI("Braj Bhasha", "bra"),
+  BRM("Burmese", "mya"),
+  BSH("Bashkir", "bak"),
+  BTI("Beti", "btb"),
+  CAT("Catalan", "cat"),
+  CEB("Cebuano", "ceb"),
+  CHE("Chechen", "che"),
+  CHG("Chaha Gurage", "sgw"),
+  CHH("Chattisgarhi", "hne"),
+  CHI("Chichewa", "nya"),
+  CHK("Chukchi", "ckt"),
+  CHN("Chinese -- as seen in win7 kaiu.ttf", "zho"),
+  CHP("Chipewyan", "chp"),
+  CHR("Cherokee", "chr"),
+  CHU("Chuvash", "chv"),
+  CMR("Comorian", "swb,wlc,wni,zdj"),
+  COP("Coptic", "cop"),
+  COS("Corsican", "cos"),
+  CRE("Cree", "cre"),
+  CRR("Carrier", "crx,caf"),
+  CRT("Crimean Tatar", "crh"),
+  CSL("Church Slavonic", "chu"),
+  CSY("Czech", "ces"),
+  DAN("Danish", "dan"),
+  DAR("Dargwa", "dar"),
+  DCR("Woods Cree", "cwd"),
+  DEU("German", "deu"),
+  DFLT("default", ""),
+  DGR("Dogri", "doi"),
+  DHV("Dhivehi", "div"), // deprecated
+  DIV("Dhivehi", "div"),
+  DJR("Djerma", "dje"),
+  DNG("Dangme", "ada"),
+  DNK("Dinka", "din"),
+  DRI("Dari", "prs"),
+  DUN("Dungan", "dng"),
+  DZN("Dzongkha", "dzo"),
+  EBI("Ebira", "igb"),
+  ECR("Eastern Cree", "crj,crl"),
+  EDO("Edo", "bin"),
+  EFI("Efik", "efi"),
+  ELL("Greek", "ell"),
+  ENG("English", "eng"),
+  ERZ("Erzya", "myv"),
+  ESP("Spanish", "spa"),
+  ETI("Estonian", "est"),
+  EUQ("Basque", "eus"),
+  EVK("Evenki", "evn"),
+  EVN("Even", "eve"),
+  EWE("Ewe", "ewe"),
+  FAN("French Antillean", "acf"),
+  FAR("Farsi", "fas"),
+  FIN("Finnish", "fin"),
+  FJI("Fijian", "fij"),
+  FLE("Flemish", "vls"),
+  FNE("Forest Nenets", "enf"),
+  FON("Fon", "fon"),
+  FOS("Faroese", "fao"),
+  FRA("French", "fra"),
+  FRI("Frisian", "fry"),
+  FRL("Friulian", "fur"),
+  FTA("Futa", "fuf"),
+  FUL("Fulani", "ful"),
+  GAD("Ga", "gaa"),
+  GAE("Gaelic", "gla"),
+  GAG("Gagauz", "gag"),
+  GAL("Galician", "glg"),
+  GAR("Garshuni", ""),
+  GAW("Garhwali", "gbm"),
+  GEZ("Ge'ez", "gez"),
+  GIL("Gilyak", "niv"),
+  GMZ("Gumuz", "guk"),
+  GON("Gondi", "gon"),
+  GRN("Greenlandic", "kal"),
+  GRO("Garo", "grt"),
+  GUA("Guarani", "grn"),
+  GUJ("Gujarati", "guj"),
+  HAI("Haitian", "hat"),
+  HAL("Halam", "flm"),
+  HAR("Harauti", "hoj"),
+  HAU("Hausa", "hau"),
+  HAW("Hawaiin", "haw"),
+  HBN("Hammer-Banna", "amf"),
+  HIL("Hiligaynon", "hil"),
+  HIN("Hindi", "hin"),
+  Mari("High", "HMA     mrj"),
+  HND("Hindko", "hno,hnd"),
+  HO("Ho", "hoc"),
+  HRI("Harari", "har"),
+  HRV("Croatian", "hrv"),
+  HUN("Hungarian", "hun"),
+  HYE("Armenian", "hye"),
+  IBO("Igbo", "ibo"),
+  IJO("Ijo", "ijc"),
+  ILO("Ilokano", "ilo"),
+  IND("Indonesian", "ind"),
+  ING("Ingush", "inh"),
+  INU("Inuktitut", "iku"),
+  IPPH("Phonetic transcription—IPA conventions", ""),
+  IRI("Irish", "gle"),
+  IRT("Irish Traditional", "gle"),
+  ISL("Icelandic", "isl"),
+  ISM("Inari Sami", "smn"),
+  ITA("Italian", "ita"),
+  IWR("Hebrew", "heb"),
+  JAV("Javanese", "jav"),
+  JII("Yiddish", "yid"),
+  JAN("Japanese", "jpn"),
+  JUD("Judezmo", "lad"),
+  JUL("Jula", "dyu"),
+  KAB("Kabardian", "kbd"),
+  KAC("Kachchi", "kfr"),
+  KAL("Kalenjin", "kln"),
+  KAN("Kannada", "kan"),
+  KAR("Karachay", "krc"),
+  KAT("Georgian", "kat"),
+  KAZ("Kazakh", "kaz"),
+  KEB("Kebena", "ktb"),
+  KGE("Khutsuri Georgian", "kat"),
+  KHA("Khakass", "kjh"),
+  KHK("Khanty-Kazim", "kca"),
+  KHM("Khmer", "khm"),
+  KHN("Khun(?)", "kkh"),
+  KHS("Khanty-Shurishkar", "kca"),
+  KHV("Khanty-Vakhi", "kca"),
+  KHW("Khowar", "khw"),
+  KIK("Kikuyu", "kik"),
+  KIR("Kirghiz", "kir"),
+  KIS("Kisii", "kqs,kss"),
+  KKN("Kokni", "kex"),
+  KLM("Kalmyk", "xal"),
+  KMB("Kamba", "kam"),
+  KMN("Kumaoni", "kfy"),
+  KMO("Komo", "kmw"),
+  KMS("Komso", "kxc"),
+  KNR("Kanuri", "kau"),
+  KOD("Kodagu", "kfa"),
+  KOH("Korean Old Hangul", "okm"),
+  KOK("Konkani", "kok"),
+  KON("Kikongo", "ktu"),
+  KOP("Komi-Permyak", "koi"),
+  KOR("Korean", "kor"),
+  KOZ("Komi-Zyrian", "kpv"),
+  KPL("Kpelle", "kpe"),
+  KRI("Krio", "kri"),
+  KRK("Karakalpak", "kaa"),
+  KRL("Karelian", "krl"),
+  KRM("Karaim", "kdr"),
+  KRN("Karen", "kar"),
+  KRT("Koorete", "kqy"),
+  KSH("Kashmiri", "kas"),
+  KSI("Khasi", "kha"),
+  KSM("Kildin Sami", "sjd"),
+  KUI("Kui", "kxu"),
+  KUL("Kulvi", "kfx"),
+  KUM("Kumyk", "kum"),
+  KUR("Kurdish", "kur"),
+  KUU("Kurukh", "kru"),
+  KUY("Kuy", "kdt"),
+  KYK("Koryak", "kpy"),
+  LAD("Ladin", "lld"),
+  LAH("Lahuli", "bfu"),
+  LAK("Lak", "lbe"),
+  LAM("Lambani", "lmn"),
+  LAO("Lao", "lao"),
+  LAT("Latin", "lat"),
+  LAZ("Laz", "lzz"),
+  LCR("L-Cree", "crm"),
+  LDK("Ladakhi", "lbj"),
+  LEZ("Lezgi", "lez"),
+  LIN("Lingala", "lin"),
+  LMA("Low Mari", "mhr"),
+  LMB("Limbu", "lif"),
+  LMW("Lomwe", "ngl"),
+  LSB("Lower Sorbian", "dsb"),
+  LSM("Lule Sami", "smj"),
+  LTH("Lithuanian", "lit"),
+  LTZ("Luxembourgish", "ltz"),
+  LUB("Luba", "lua,lub"),
+  LUG("Luganda", "lug"),
+  LUH("Luhya", "luy"),
+  LUO("Luo", "luo"),
+  LVI("Latvian", "lav"),
+  MAJ("Majang", "mpe"),
+  MAK("Makua", "vmw"),
+  MAL("Malayalam Traditional", "mal"),
+  MAN("Mansi", "mns"),
+  MAP("Mapudungun", "arn"),
+  MAR("Marathi", "mar"),
+  MAW("Marwari", "mwr"),
+  MBN("Mbundu", "kmb"),
+  MCH("Manchu", "mnc"),
+  MCR("Moose Cree", "crm"),
+  MDE("Mende", "men"),
+  MEN("Me'en", "mym"),
+  MIZ("Mizo", "lus"),
+  MKD("Macedonian", "mkd"),
+  MLE("Male", "mdy"),
+  MLG("Malagasy", "mlg"),
+  MLN("Malinke", "mlq"),
+  MLR("Malayalam Reformed", "mal"),
+  MLY("Malay", "msa"),
+  MND("Mandinka", "mnk"),
+  MNG("Mongolian", "mon"),
+  MNI("Manipuri", "mni"),
+  MNK("Maninka", "man"),
+  MNX("Manx Gaelic", "glv"),
+  MOH("Mohawk", "moh"),
+  MOK("Moksha", "mdf"),
+  MOL("Moldavian", "mol"),
+  MON("Mon", "mnw"),
+  MOR("Moroccan", ""),
+  MRI("Maori", "mri"),
+  MTH("Maithili", "mai"),
+  MTS("Maltese", "mlt"),
+  MUN("Mundari", "unr"),
+  NAG("Naga-Assamese", "nag"),
+  NAN("Nanai", "gld"),
+  NAS("Naskapi", "nsk"),
+  NCR("N-Cree", "csw"),
+  NDB("Ndebele", "nbl,nde"),
+  NDG("Ndonga", "ndo"),
+  NEP("Nepali", "nep"),
+  NEW("Newari", "new"),
+  NGR("Nagari", ""),
+  NHC("Norway House Cree", "csw"),
+  NIS("Nisi", "dap"),
+  NIU("Niuean", "niu"),
+  NKL("Nkole", "nyn"),
+  NKO("N'Ko", "nqo"),
+  NLD("Dutch", "nld"),
+  NOG("Nogai", "nog"),
+  NOR("Norwegian", "nob"),
+  NSM("Northern Sami", "sme"),
+  NTA("Northern Tai", "nod"),
+  NTO("Esperanto", "epo"),
+  NYN("Nynorsk", "nno"),
+  OCI("Occitan", "oci"),
+  OCR("Oji-Cree", "ojs"),
+  OJB("Ojibway", "oji"),
+  ORI("Odia (formerly Oriya)", "ori"),
+  ORO("Oromo", "orm"),
+  OSS("Ossetian", "oss"),
+  PAA("Palestinian Aramaic", "sam"),
+  PAL("Pali", "pli"),
+  PAN("Punjabi", "pan"),
+  PAP("Palpa", "plp"),
+  PAS("Pashto", "pus"),
+  PGR("Polytonic Greek", "ell"),
+  PIL("Filipino", "fil"),
+  PLG("Palaung", "pce,rbb,pll"),
+  PLK("Polish", "pol"),
+  PRO("Provencal", "pro"),
+  PTG("Portuguese", "por"),
+  QIN("Chin",
+      "bgr,cnh,cnw,czt,sez,tcp,csy,ctd,flm,pck,tcz,zom,cmr,dao,hlt,cka,cnk,mrh,mwg,cbl,cnb,csh"),
+  RAJ("Rajasthani", "raj"),
+  RCR("R-Cree", "atj"),
+  RBU("Russian Buriat", "bxr"),
+  RIA("Riang", "ria"),
+  RMS("Rhaeto-Romanic", "roh"),
+  ROM("Romanian", "ron"),
+  ROY("Romany", "rom"),
+  RSY("Rusyn", "rue"),
+  RUA("Ruanda", "kin"),
+  RUS("Russian", "rus"),
+  SAD("Sadri", "sck"),
+  SAN("Sanskrit", "san"),
+  SAT("Santali", "sat"),
+  SAY("Sayisi", "chp"),
+  SEK("Sekota", "xan"),
+  SEL("Selkup", "sel"),
+  SGO("Sango", "sag"),
+  SHN("Shan", "shn"),
+  SIB("Sibe", "sjo"),
+  SID("Sidamo", "sid"),
+  SIG("Silte Gurage", "xst"),
+  SKS("Skolt Sami", "sms"),
+  SKY("Slovak", "slk"),
+  SLA("Slavey", "scs"),
+  SLV("Slovenian", "slv"),
+  SML("Somali", "som"),
+  SMO("Samoan", "smo"),
+  SNA("Sena", "she"),
+  SND("Sindhi", "snd"),
+  SNH("Sinhalese", "sin"),
+  SNK("Soninke", "snk"),
+  SOG("Sodo Gurage", "gru"),
+  SOT("Sotho", "nso,sot"),
+  SQI("Albanian", "sqi"),
+  SRB("Serbian", "srp"),
+  SRK("Saraiki", "skr"),
+  SRR("Serer", "srr"),
+  SSL("South Slavey", "xsl"),
+  SSM("Southern Sami", "sma"),
+  SUR("Suri", "suq"),
+  SVA("Svan", "sva"),
+  SVE("Swedish", "swe"),
+  SWA("Swadaya Aramaic", "aii"),
+  SWK("Swahili", "swa"),
+  SWZ("Swazi", "ssw"),
+  SXT("Sutu", "ngo"),
+  SYR("Syriac", "syr"),
+  TAB("Tabasaran", "tab"),
+  TAJ("Tajiki", "tgk"),
+  TAM("Tamil", "tam"),
+  TAT("Tatar", "tat"),
+  TCR("TH-Cree", "cwd"),
+  TEL("Telugu", "tel"),
+  TGN("Tongan", "ton"),
+  TGR("Tigre", "tig"),
+  TGY("Tigrinya", "tir"),
+  THA("Thai", "tha"),
+  THT("Tahitian", "tah"),
+  TIB("Tibetan", "bod"),
+  TKM("Turkmen", "tuk"),
+  TMN("Temne", "tem"),
+  TNA("Tswana", "tsn"),
+  TNE("Tundra Nenets", "enh"),
+  TNG("Tonga", "toi"),
+  TOD("Todo", "xal"),
+  TRK("Turkish", "tur"),
+  TSG("Tsonga", "tso"),
+  TUA("Turoyo Aramaic", "tru"),
+  TUL("Tulu", "tcy"),
+  TUV("Tuvin", "tyv"),
+  TWI("Twi", "aka"),
+  UDM("Udmurt", "udm"),
+  UKR("Ukrainian", "ukr"),
+  URD("Urdu", "urd"),
+  USB("Upper Sorbian", "hsb"),
+  UYG("Uyghur", "uig"),
+  UZB("Uzbek", "uzb"),
+  VEN("Venda", "ven"),
+  VIT("Vietnamese", "vie"),
+  WA("Wa", "wbm"),
+  WAG("Wagdi", "wbr"),
+  WCR("West-Cree", "crk"),
+  WEL("Welsh", "cym"),
+  WLF("Wolof", "wol"),
+  XBD("Tai Lue", "khb"),
+  XHS("Xhosa", "xho"),
+  YAK("Sakha", "sah"),
+  YBA("Yoruba", "yor"),
+  YCR("Y-Cree", ""),
+  YIC("Yi Classic", ""),
+  YIM("Yi Modern", "iii"),
+  ZHH("Chinese, Hong Kong SAR", "zho"),
+  ZHP("Chinese Phonetic", "zho"),
+  ZHS("Chinese Simplified", "zho"),
+  ZHT("Chinese Traditional", "zho"),
+  ZND("Zande", "zne"),
+  ZUL("Zulu", "zul"),
+  de("German found in FreeSerif.ttf", "deu"),
+  nl("Dutch found in FreeSansBoldOblique.ttf", "nld"),
+  tmh("Tamashek found in ebrimabd.ttf", "tmh");
+
+  private LanguageTag(String name, String iso3List) {
+    String tag = name();
+    while (tag.length() < 4) {
+      tag += ' ';
+    }
+    this.tag = Tag.intValue(tag);
+    this.name = name;
+    this.iso3List = iso3List;
+  }
+
+  public int tag() {
+    return tag;
+  }
+
+  public String longName() {
+    return name;
+  }
+
+  public boolean isDeprecated() {
+    return this == DHV;
+  }
+
+  public List<String> iso3List() {
+    return Arrays.asList(iso3List.split(","));
+  }
+
+  static LanguageTag fromTag(int tag) {
+    for (LanguageTag script : LanguageTag.values()) {
+      if (script.tag == tag) {
+        return script;
+      }
+    }
+    throw new IllegalArgumentException(Tag.stringValue(tag));
+  }
+
+  private final int tag;
+  private final String name;
+  private final String iso3List;
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/LayoutCommonTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/LayoutCommonTable.java
new file mode 100644
index 0000000..901d53b
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/LayoutCommonTable.java
@@ -0,0 +1,137 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.SubTable;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+abstract class LayoutCommonTable<T extends LookupTable> extends SubTable {
+  private static int VERSION_OFFSET = 0;
+  private static int SCRIPT_LIST_OFFSET = 4;
+  private static int FEATURE_LIST_OFFSET = 6;
+  private static int LOOKUP_LIST_OFFSET = 8;
+  private static int HEADER_SIZE = 10;
+
+  private static int VERSION_ID = 0x00010000;
+
+  private final boolean dataIsCanonical;
+
+  /**
+   * @param data
+   *          the GSUB or GPOS data
+   */
+  protected LayoutCommonTable(ReadableFontData data, boolean dataIsCanonical) {
+    super(data);
+    this.dataIsCanonical = dataIsCanonical;
+  }
+
+  private static int readScriptListOffset(ReadableFontData data) {
+    return data.readUShort(SCRIPT_LIST_OFFSET);
+  }
+
+  private static ReadableFontData scriptListData(ReadableFontData commonData,
+      boolean dataIsCanonical) {
+    int start = readScriptListOffset(commonData);
+    if (dataIsCanonical) {
+      int limit = readFeatureListOffset(commonData);
+      return commonData.slice(start, limit - start);
+    }
+    return commonData.slice(start);
+  }
+
+  ScriptListTable createScriptList() {
+    return new ScriptListTable(scriptListData(data, dataIsCanonical), dataIsCanonical);
+  }
+
+  private static int readFeatureListOffset(ReadableFontData data) {
+    return data.readUShort(FEATURE_LIST_OFFSET);
+  }
+
+  private static ReadableFontData featureListData(ReadableFontData commonData,
+      boolean dataIsCanonical) {
+    int start = readFeatureListOffset(commonData);
+    if (dataIsCanonical) {
+      int limit = readLookupListOffset(commonData);
+      return commonData.slice(start, limit - start);
+    }
+    return commonData.slice(start);
+  }
+
+  FeatureListTable createFeatureList() {
+    return new FeatureListTable(featureListData(data, dataIsCanonical), dataIsCanonical);
+  }
+
+  private static int readLookupListOffset(ReadableFontData data) {
+    return data.readUShort(LOOKUP_LIST_OFFSET);
+  }
+
+  private static ReadableFontData lookupListData(ReadableFontData commonData,
+      boolean dataIsCanonical) {
+    int start = readLookupListOffset(commonData);
+    if (dataIsCanonical) {
+      int limit = commonData.length();
+      return commonData.slice(start, limit - start);
+    }
+    return commonData.slice(start);
+  }
+
+  protected LookupListTable createLookupList() {
+    return handleCreateLookupList(lookupListData(data, dataIsCanonical), dataIsCanonical);
+  }
+
+  protected abstract LookupListTable handleCreateLookupList(
+      ReadableFontData data, boolean dataIsCanonical);
+
+  static abstract class Builder<T extends LookupTable>
+  extends SubTable.Builder<LayoutCommonTable<T>> {
+    private int serializedLength;
+    private ScriptListTable.Builder serializedScriptListBuilder;
+    private FeatureListTable.Builder serializedFeatureListBuilder;
+    private LookupListTable.Builder serializedLookupListBuilder;
+
+    /**
+     * @param data
+     *          the GSUB or GPOS data
+     */
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data);
+    }
+
+    protected Builder() {
+      super(null);
+    }
+
+    protected abstract LookupListTable handleCreateLookupList(
+        ReadableFontData data, boolean dataIsCanonical);
+
+    protected abstract LookupListTable.Builder createLookupListBuilder();
+
+    @Override
+    protected int subSerialize(WritableFontData newData) {
+      if (serializedLength == 0) {
+        return 0;
+      }
+      newData.writeULong(VERSION_OFFSET, VERSION_ID);
+      int pos = HEADER_SIZE;
+      newData.writeUShort(SCRIPT_LIST_OFFSET, pos);
+      pos += serializedScriptListBuilder.subSerialize(newData.slice(pos));
+      newData.writeUShort(FEATURE_LIST_OFFSET, pos);
+      pos += serializedFeatureListBuilder.subSerialize(newData.slice(pos));
+      newData.writeUShort(LOOKUP_LIST_OFFSET, pos);
+      pos += serializedLookupListBuilder.subSerialize(newData.slice(pos));
+      return serializedLength;
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    @Override
+    protected abstract LayoutCommonTable<T> subBuildTable(ReadableFontData data);
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/LigatureSubst.java b/java/src/com/google/typography/font/sfntly/table/opentype/LigatureSubst.java
new file mode 100644
index 0000000..867682d
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/LigatureSubst.java
@@ -0,0 +1,108 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.opentype.ligaturesubst.InnerArrayFmt1;
+import com.google.typography.font.sfntly.table.opentype.ligaturesubst.LigatureSet;
+
+import java.util.Iterator;
+
+public class LigatureSubst extends SubstSubtable implements Iterable<LigatureSet> {
+  private final InnerArrayFmt1 array;
+
+  // //////////////
+  // Constructors
+
+  LigatureSubst(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    if (format != 1) {
+      throw new IllegalStateException("Subt format value is " + format + " (should be 1).");
+    }
+    array = new InnerArrayFmt1(data, headerSize(), dataIsCanonical);
+  }
+
+  // //////////////////////////////////
+  // Methods redirected to the array
+
+  public int subTableCount() {
+    return array.recordList.count();
+  }
+
+  public LigatureSet subTableAt(int index) {
+    return array.subTableAt(index);
+  }
+
+  @Override
+  public Iterator<LigatureSet> iterator() {
+    return array.iterator();
+  }
+
+  // //////////////////////////////////
+  // Methods specific to this class
+
+  public CoverageTable coverage() {
+    return array.coverage;
+  }
+
+  // //////////////////////////////////
+  // Builder
+
+  static class Builder extends SubstSubtable.Builder<SubstSubtable> {
+
+    private final InnerArrayFmt1.Builder arrayBuilder;
+
+    // //////////////
+    // Constructors
+
+    Builder() {
+      super();
+      arrayBuilder = new InnerArrayFmt1.Builder();
+    }
+
+    Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+      arrayBuilder = new InnerArrayFmt1.Builder(data, dataIsCanonical);
+    }
+
+    Builder(SubstSubtable subTable) {
+      LigatureSubst ligSubst = (LigatureSubst) subTable;
+      arrayBuilder = new InnerArrayFmt1.Builder(ligSubst.array);
+    }
+
+    // /////////////////////////////
+    // private methods for builders
+
+
+
+    // ///////////////////////////////
+    // private methods to serialize
+
+    @Override
+    public int subDataSizeToSerialize() {
+      return arrayBuilder.subDataSizeToSerialize();
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      return arrayBuilder.subSerialize(newData);
+    }
+
+    // /////////////////////////////////
+    // must implement abstract methods
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    @Override
+    public void subDataSet() {
+      arrayBuilder.subDataSet();
+    }
+
+    @Override
+    public LigatureSubst subBuildTable(ReadableFontData data) {
+      return new LigatureSubst(data, 0, true);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/LookupList.java b/java/src/com/google/typography/font/sfntly/table/opentype/LookupList.java
new file mode 100644
index 0000000..ccc587c
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/LookupList.java
@@ -0,0 +1,174 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.SubTable;
+import com.google.typography.font.sfntly.table.opentype.component.LookupType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+abstract class LookupList extends SubTable {
+  private LookupList(ReadableFontData data, boolean dataIsCanonical) {
+    super(data);
+  }
+
+  private static final int LOOKUP_COUNT_OFFSET = 0;
+  private static final int LOOKUP_OFFSET_BASE = 2;
+  private static final int LOOKUP_OFFSET_SIZE = 2;
+
+  private static int readLookupCount(ReadableFontData data) {
+    if (data == null) {
+      return 0;
+    }
+    return data.readUShort(LOOKUP_COUNT_OFFSET);
+  }
+
+  private static int readLookupOffsetAt(ReadableFontData data, int index) {
+    if (data == null) {
+      return -1;
+    }
+    return data.readUShort(LOOKUP_OFFSET_BASE + index * LOOKUP_OFFSET_SIZE);
+  }
+
+  private static ReadableFontData readLookupData(ReadableFontData data, boolean dataIsCanonical,
+      int index) {
+    ReadableFontData newData;
+    int offset = readLookupOffsetAt(data, index);
+    if (dataIsCanonical) {
+      int nextOffset;
+      if (index < readLookupCount(data) - 1) {
+        nextOffset = readLookupOffsetAt(data, index + 1);
+      } else {
+        nextOffset = data.length();
+      }
+      newData = data.slice(offset, nextOffset - offset);
+    } else {
+      newData = data.slice(offset);
+    }
+    return newData;
+  }
+
+  protected abstract LookupType lookupTypeAt(int index);
+
+  protected abstract LookupTable createLookup(ReadableFontData data);
+
+  static abstract class Builder extends SubTable.Builder<LookupList> {
+    private List<LookupTable.Builder> builders;
+    private boolean dataIsCanonical;
+    private int serializedCount;
+    private int serializedLength;
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data);
+      this.dataIsCanonical = dataIsCanonical;
+    }
+
+    protected Builder() {
+      this(null, false);
+    }
+
+    protected abstract LookupTable.Builder createLookupBuilder(
+        ReadableFontData lookupData);
+
+    private void initFromData(ReadableFontData data) {
+      int count = readLookupCount(data);
+      builders = new ArrayList<LookupTable.Builder>(count);
+      for (int i = 0; i < count; ++i) {
+        ReadableFontData lookupData = readLookupData(data, dataIsCanonical, i);
+        LookupTable.Builder lookup = createLookupBuilder(lookupData);
+        if (lookup != null) {
+          builders.add(lookup);
+        }
+      }
+    }
+
+    private void prepareToEdit() {
+      if (builders == null) {
+        initFromData(internalReadData());
+      }
+    }
+
+    private int serializeFromBuilders(WritableFontData newData) {
+      if (serializedCount == 0) {
+        return 0;
+      }
+      newData.writeUShort(LOOKUP_COUNT_OFFSET, serializedCount);
+      int rpos = LOOKUP_OFFSET_BASE;
+      int spos = rpos + serializedCount * LOOKUP_OFFSET_SIZE;
+      for (int i = 0; i < builders.size(); ++i) {
+        LookupTable.Builder builder = builders.get(i);
+        int s = builder.subDataSizeToSerialize();
+        if (s > 0) {
+          newData.writeUShort(rpos, spos);
+          rpos += LOOKUP_OFFSET_SIZE;
+
+          WritableFontData targetData = newData.slice(spos);
+          builder.subSerialize(targetData);
+          spos += s;
+        }
+      }
+      return serializedLength;
+    }
+
+    @Override
+    protected int subSerialize(WritableFontData newData) {
+      if (builders == null) {
+        // Only the case if data is canonical
+        ReadableFontData data = internalReadData();
+        data.copyTo(newData);
+        return data.length();
+      }
+      return serializeFromBuilders(newData);
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    private int computeSerializedSizeFromBuilders() {
+      int size = 0;
+      int count = 0;
+      for (int i = 0; i < builders.size(); ++i) {
+        int s = builders.get(i).subDataSizeToSerialize();
+        if (s > 0) {
+          ++count;
+          size += s;
+        }
+      }
+      if (count > 0) {
+        size += LOOKUP_OFFSET_BASE + count * LOOKUP_OFFSET_SIZE;
+      }
+
+      serializedCount = count;
+      serializedLength = size;
+
+      return serializedLength;
+    }
+
+    @Override
+    protected int subDataSizeToSerialize() {
+      if (builders == null) {
+        if (dataIsCanonical) {
+          return internalReadData().length();
+        }
+        prepareToEdit();
+      }
+      return computeSerializedSizeFromBuilders();
+    }
+
+    @Override
+    protected void subDataSet() {
+      builders = null;
+    }
+
+    @Override
+    protected abstract LookupList subBuildTable(ReadableFontData data);
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/LookupListTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/LookupListTable.java
new file mode 100644
index 0000000..20ff03e
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/LookupListTable.java
@@ -0,0 +1,57 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.OffsetRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class LookupListTable extends OffsetRecordTable<LookupTable> {
+  private static final int FIELD_COUNT = 0;
+
+  LookupListTable(ReadableFontData data, boolean dataIsCanonical) {
+    super(data, dataIsCanonical);
+  }
+
+  @Override
+  protected LookupTable readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new LookupTable(data, base, dataIsCanonical);
+  }
+
+  static class Builder extends OffsetRecordTable.Builder<LookupListTable, LookupTable> {
+
+    @Override
+    protected LookupListTable readTable(
+        ReadableFontData data, int baseUnused, boolean dataIsCanonical) {
+      return new LookupListTable(data, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<LookupTable> createSubTableBuilder() {
+      return new LookupTable.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<LookupTable> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new LookupTable.Builder(data, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<LookupTable> createSubTableBuilder(LookupTable subTable) {
+      return new LookupTable.Builder(subTable);
+    }
+
+    @Override
+    protected void initFields() {
+    }
+
+    @Override
+    public int fieldCount() {
+      return FIELD_COUNT;
+    }
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/LookupSubTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/LookupSubTable.java
new file mode 100644
index 0000000..3515898
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/LookupSubTable.java
@@ -0,0 +1,31 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.LookupType;
+
+abstract class LookupSubTable extends OTSubTable {
+
+  protected LookupSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    super(data, dataIsCanonical);
+  }
+
+  // @Override
+  // private abstract Builder<? extends LookupSubTable> builder();
+
+  protected abstract LookupType lookupType();
+
+  abstract static class Builder<T extends LookupSubTable> extends OTSubTable.Builder<T> {
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    protected Builder(T table) {
+      super(table);
+    }
+
+    protected abstract LookupType lookupType();
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/LookupTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/LookupTable.java
new file mode 100644
index 0000000..d77b71e
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/LookupTable.java
@@ -0,0 +1,142 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.GsubLookupType;
+import com.google.typography.font.sfntly.table.opentype.component.OffsetRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class LookupTable extends OffsetRecordTable<SubstSubtable> {
+  private static final int FIELD_COUNT = 2;
+
+  static final int LOOKUP_TYPE_INDEX = 0;
+  private static final int LOOKUP_TYPE_DEFAULT = 0;
+
+  private static final int LOOKUP_FLAG_INDEX = 1;
+
+  private enum LookupFlagBit {
+    RIGHT_TO_LEFT(0x0001),
+    IGNORE_BASE_GLYPHS(0x0002),
+    IGNORE_LIGATURES(0x0004),
+    IGNORE_MARKS(0x0008),
+    USE_MARK_FILTERING_SET(0x0010),
+    RESERVED(0x00E0),
+    MARK_ATTACHMENT_TYPE(0xFF00);
+
+    private int bit;
+
+    private LookupFlagBit(int bit) {
+      this.bit = bit;
+    }
+
+    private int getValue(int value) {
+      return bit & value;
+    }
+  }
+
+  protected LookupTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    int lookupFlag = getField(LOOKUP_FLAG_INDEX);
+    if (LookupFlagBit.USE_MARK_FILTERING_SET.getValue(lookupFlag) != 0) {
+      throw new IllegalArgumentException(
+          "Lookup Flag has Use Mark Filtering Set which is unimplemented.");
+    }
+    if (LookupFlagBit.RESERVED.getValue(lookupFlag) != 0) {
+      throw new IllegalArgumentException("Reserved bits of Lookup Flag are not 0");
+    }
+  }
+
+  public GsubLookupType lookupType() {
+    return GsubLookupType.forTypeNum(getField(LOOKUP_TYPE_INDEX));
+  }
+
+  public GsubLookupType lookupFlag() {
+    return GsubLookupType.forTypeNum(getField(LOOKUP_FLAG_INDEX));
+  }
+
+  @Override
+  protected SubstSubtable readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    int lookupType = getField(LOOKUP_TYPE_INDEX);
+    GsubLookupType gsubLookupType = GsubLookupType.forTypeNum(lookupType);
+    switch (gsubLookupType) {
+    case GSUB_LIGATURE:
+      return new LigatureSubst(data, base, dataIsCanonical);
+    case GSUB_SINGLE:
+      return new SingleSubst(data, base, dataIsCanonical);
+    case GSUB_MULTIPLE:
+      return new MultipleSubst(data, base, dataIsCanonical);
+    case GSUB_ALTERNATE:
+      return new AlternateSubst(data, base, dataIsCanonical);
+    case GSUB_CONTEXTUAL:
+      return new ContextSubst(data, base, dataIsCanonical);
+    case GSUB_CHAINING_CONTEXTUAL:
+      return new ChainContextSubst(data, base, dataIsCanonical);
+    case GSUB_EXTENSION:
+      return new ExtensionSubst(data, base, dataIsCanonical);
+    case GSUB_REVERSE_CHAINING_CONTEXTUAL_SINGLE:
+      return new ReverseChainSingleSubst(data, base, dataIsCanonical);
+    default:
+      System.err.println("Unimplemented LookupType: " + gsubLookupType);
+      return new NullTable(data, base, dataIsCanonical);
+      // throw new IllegalArgumentException("LookupType is " + lookupType);
+    }
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+
+  Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder extends OffsetRecordTable.Builder<LookupTable, SubstSubtable> {
+    Builder() {
+      super();
+    }
+
+    Builder(ReadableFontData data, boolean dataIsCanonical) {
+      this(data, 0, dataIsCanonical);
+    }
+
+    private Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data, base, dataIsCanonical);
+    }
+
+    Builder(LookupTable table) {
+      super(table);
+    }
+
+    @Override
+    protected LookupTable readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new LookupTable(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubstSubtable> createSubTableBuilder() {
+      return new LigatureSubst.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubstSubtable> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new LigatureSubst.Builder(data, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubstSubtable> createSubTableBuilder(SubstSubtable subTable) {
+      return new LigatureSubst.Builder(subTable);
+    }
+
+    @Override
+    public int fieldCount() {
+      return FIELD_COUNT;
+    }
+
+    @Override
+    public void initFields() {
+      setField(LOOKUP_TYPE_INDEX, LOOKUP_TYPE_DEFAULT);
+      setField(LOOKUP_FLAG_INDEX, LOOKUP_FLAG_INDEX);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/MultipleSubst.java b/java/src/com/google/typography/font/sfntly/table/opentype/MultipleSubst.java
new file mode 100644
index 0000000..12142b0
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/MultipleSubst.java
@@ -0,0 +1,10 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.OneToManySubst;
+
+public class MultipleSubst extends OneToManySubst {
+  MultipleSubst(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/NullTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/NullTable.java
new file mode 100644
index 0000000..3d2a7ec
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/NullTable.java
@@ -0,0 +1,56 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public final class NullTable extends SubstSubtable {
+  private static final int RECORD_SIZE = 0;
+
+  NullTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  private NullTable(ReadableFontData data) {
+    super(data, 0, false);
+  }
+
+  private NullTable() {
+    super(null, 0, false);
+  }
+
+  public final static class Builder extends VisibleSubTable.Builder<NullTable> {
+    private Builder() {
+    }
+
+    private Builder(ReadableFontData data, boolean dataIsCanonical) {
+    }
+
+    private Builder(NullTable table) {
+    }
+
+    @Override
+    public int subDataSizeToSerialize() {
+      return NullTable.RECORD_SIZE;
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      return NullTable.RECORD_SIZE;
+    }
+
+    @Override
+    public NullTable subBuildTable(ReadableFontData data) {
+      return new NullTable(data);
+    }
+
+    @Override
+    public void subDataSet() {
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/OTSubTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/OTSubTable.java
new file mode 100644
index 0000000..4d6d701
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/OTSubTable.java
@@ -0,0 +1,129 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.SubTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+/**
+ * Consolidates dataIsCanonical handling and building logic used by the OpenType tables.
+ *
+ * @author [email protected] (Doug Felt)
+ */
+abstract class OTSubTable extends SubTable {
+  final boolean dataIsCanonical;
+
+  protected OTSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    super(data);
+    this.dataIsCanonical = dataIsCanonical;
+  }
+
+  protected abstract Builder<? extends OTSubTable> builder();
+
+  abstract static class Builder<T extends OTSubTable> extends VisibleSubTable.Builder<T> {
+    private final boolean dataIsCanonical;
+    private int serializedLength;
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data);
+      this.dataIsCanonical = data == null ? false : dataIsCanonical;
+      if (this.dataIsCanonical) {
+        serializedLength = data.length();
+      } else if (data == null || data.length() == 0) {
+        serializedLength = 0;
+      } else {
+        serializedLength = -1;
+        setModelChanged();
+      }
+    }
+
+    protected Builder(T table) {
+      this(table.readFontData(), table.dataIsCanonical);
+    }
+
+    /**
+     * Returns true if the data has not been edited, and thus there is no model
+     * to serialize.
+     */
+    protected abstract boolean unedited();
+
+    /**
+     * Create a model for editing from the data.
+     * This causes <code>unedited()</code> to return false;
+     * @param data
+     */
+    protected abstract void readModel(ReadableFontData data, boolean dataIsCanonical);
+
+    /**
+     * Computes the serialized length of the data.  This computes its own serialized
+     * length and calls subDataSizeToSerialize on any subTables to get their length.
+     */
+    protected abstract int computeSerializedLength();
+
+    /**
+     * Writes the model, which is exactly as long as computeSerializedLength.
+     */
+    protected abstract void writeModel(WritableFontData data);
+
+    /**
+     * The first time this is called, it calls initFromData to build the model and
+     * calls setModelChanged to indicate that the model will need to be written out.
+     * It also resets the serializedLength so it will be recomputed.
+     */
+    protected void prepareToEdit() {
+      if (unedited()) {
+        readModel(internalReadData(), dataIsCanonical);
+        serializedLength = -1;
+        setModelChanged();
+      }
+    }
+
+    /**
+     * This is called first by FontDataTable when the tables is to be built.  It calls
+     * subDataSizeToSerialize and returns true if the result > 0.  This ensures that
+     * no object is built if there is no data.
+     */
+    @Override
+    protected final boolean subReadyToSerialize() {
+      return subDataSizeToSerialize() > 0;
+    }
+
+    /**
+     * This is called twice, once by subReadyToSerialize, and then again by FontDataTable.
+     * The first call will compute the serializedLength, and the second just returns the
+     * cached value.  The actual work is done in computeSerializedLength, which might
+     * call this recursively on its sub-tables.
+     */
+    @Override
+    public final int subDataSizeToSerialize() {
+      if (serializedLength == -1) {
+        if (unedited()) {
+            prepareToEdit();
+        }
+        serializedLength = computeSerializedLength();
+      }
+      return serializedLength;
+    }
+
+    /**
+     * This is called after subDataSizeToSerialize, newData has a length equal to
+     * that returned by that call. If the data has not been edited, it is because
+     * the data is canonical and can be copied straight to newData.  Otherwise,
+     * serializeEditState is called to do the actual serialization.  When this
+     * finishes, resets serializedLength.
+     */
+    @Override
+    public final int subSerialize(WritableFontData newData) {
+      if (unedited()) {
+        internalReadData().copyTo(newData);
+      } else {
+        writeModel(newData);
+      }
+      int length = serializedLength;
+      serializedLength = -1;
+      return length;
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/ReverseChainSingleSubst.java b/java/src/com/google/typography/font/sfntly/table/opentype/ReverseChainSingleSubst.java
new file mode 100644
index 0000000..47a2193
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/ReverseChainSingleSubst.java
@@ -0,0 +1,162 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.CoverageArray;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.InnerArraysFmt3;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class ReverseChainSingleSubst extends SubstSubtable {
+  private static final int FIELD_COUNT = 1;
+  private static final int COVERAGE_INDEX = SubstSubtable.FIELD_SIZE;
+  public final CoverageTable coverage;
+  public final CoverageArray backtrackGlyphs;
+  public final CoverageArray lookAheadGlyphs;
+  public final NumRecordTable substitutes;
+
+  // //////////////
+  // Constructors
+
+  ReverseChainSingleSubst(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    if (format != 1) {
+      throw new IllegalStateException("Subt format value is " + format + " (should be 1).");
+    }
+    int coverageOffset = getField(COVERAGE_INDEX);
+    coverage = new CoverageTable(data.slice(coverageOffset), 0, dataIsCanonical);
+
+    NumRecordList records = new NumRecordList(data, 0, headerSize());
+    backtrackGlyphs = new CoverageArray(records);
+
+    records = new NumRecordList(data, 0, records.limit());
+    lookAheadGlyphs = new CoverageArray(records);
+
+    records = new NumRecordList(data, 0, records.limit());
+    substitutes = new NumRecordTable(records);
+  }
+
+  @Override
+  public int fieldCount() {
+    return super.fieldCount() + FIELD_COUNT;
+  }
+
+  public static class Builder extends VisibleSubTable.Builder<ReverseChainSingleSubst> {
+    private CoverageTable.Builder coverageBuilder;
+    private CoverageArray.Builder backtrackGlyphsBuilder;
+    private CoverageArray.Builder lookAheadGlyphsBuilder;
+
+    protected Builder() {
+      super();
+    }
+
+    protected Builder(InnerArraysFmt3 table) {
+      this(table.readFontData(), 0, false);
+    }
+
+    protected Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data);
+      if (!dataIsCanonical) {
+        prepareToEdit();
+      }
+    }
+
+    protected Builder(Builder other) {
+      super();
+      coverageBuilder = other.coverageBuilder;
+      backtrackGlyphsBuilder = other.backtrackGlyphsBuilder;
+      lookAheadGlyphsBuilder = other.lookAheadGlyphsBuilder;
+    }
+
+    @Override
+    public int subDataSizeToSerialize() {
+      if (lookAheadGlyphsBuilder != null) {
+        serializedLength = lookAheadGlyphsBuilder.limit();
+      } else {
+        computeSizeFromData(internalReadData());
+      }
+      return serializedLength;
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      if (serializedLength == 0) {
+        return 0;
+      }
+
+      if (coverageBuilder == null
+          || backtrackGlyphsBuilder == null || lookAheadGlyphsBuilder == null) {
+        return serializeFromData(newData);
+      }
+
+      int tableOnlySize = 0;
+      tableOnlySize += coverageBuilder.headerSize();
+      tableOnlySize += backtrackGlyphsBuilder.tableSizeToSerialize();
+      tableOnlySize += lookAheadGlyphsBuilder.tableSizeToSerialize();
+      int subTableWriteOffset = tableOnlySize;
+
+      coverageBuilder.subSerialize(newData);
+
+      backtrackGlyphsBuilder.subSerialize(newData, subTableWriteOffset);
+      subTableWriteOffset += backtrackGlyphsBuilder.subTableSizeToSerialize();
+      int tableWriteOffset = backtrackGlyphsBuilder.tableSizeToSerialize();
+
+      lookAheadGlyphsBuilder.subSerialize(newData.slice(tableWriteOffset), subTableWriteOffset);
+      subTableWriteOffset += lookAheadGlyphsBuilder.subTableSizeToSerialize();
+
+      return subTableWriteOffset;
+    }
+
+    @Override
+    public ReverseChainSingleSubst subBuildTable(ReadableFontData data) {
+      return new ReverseChainSingleSubst(data, 0, true);
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    @Override
+    public void subDataSet() {
+      backtrackGlyphsBuilder = null;
+      lookAheadGlyphsBuilder = null;
+    }
+
+    // ////////////////////////////////////
+    // private methods
+
+    private void prepareToEdit() {
+      initFromData(internalReadData());
+      setModelChanged();
+    }
+
+    private void initFromData(ReadableFontData data) {
+      if (backtrackGlyphsBuilder == null
+          || lookAheadGlyphsBuilder == null) {
+        NumRecordList records = new NumRecordList(data);
+        backtrackGlyphsBuilder = new CoverageArray.Builder(records);
+
+        records = new NumRecordList(data, 0, records.limit());
+        lookAheadGlyphsBuilder = new CoverageArray.Builder(records);
+      }
+    }
+
+    private void computeSizeFromData(ReadableFontData data) {
+      // This assumes canonical data.
+      int len = 0;
+      if (data != null) {
+        len = data.length();
+      }
+      serializedLength = len;
+    }
+
+    private int serializeFromData(WritableFontData newData) {
+      // The source data must be canonical.
+      ReadableFontData data = internalReadData();
+      data.copyTo(newData);
+      return data.length();
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/ScriptListTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/ScriptListTable.java
new file mode 100644
index 0000000..79f6cbf
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/ScriptListTable.java
@@ -0,0 +1,72 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.TagOffsetsTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ScriptListTable extends TagOffsetsTable<ScriptTable> {
+
+  ScriptListTable(ReadableFontData data, boolean dataIsCanonical) {
+    super(data, dataIsCanonical);
+  }
+
+  @Override
+  protected ScriptTable readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new ScriptTable(data, 0, dataIsCanonical);
+  }
+
+  public ScriptTag scriptAt(int index) {
+    return ScriptTag.fromTag(this.tagAt(index));
+  }
+
+  public Map<ScriptTag, ScriptTable> map() {
+    Map<ScriptTag, ScriptTable> map = new HashMap<ScriptTag, ScriptTable>();
+    for (int i = 0; i < count(); i++) {
+      ScriptTag script;
+      try {
+        script = scriptAt(i);
+      } catch (IllegalArgumentException e) {
+        System.err.println("Invalid Script tag found: " + e.getMessage());
+        continue;
+      }
+      map.put(script, subTableAt(i));
+    }
+    return map;
+  }
+
+  static class Builder extends TagOffsetsTable.Builder<ScriptListTable, ScriptTable> {
+
+    @Override
+    protected VisibleSubTable.Builder<ScriptTable> createSubTableBuilder(
+        ReadableFontData data, int tag, boolean dataIsCanonical) {
+      return new ScriptTable.Builder(data, 0, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ScriptTable> createSubTableBuilder() {
+      return new ScriptTable.Builder();
+    }
+
+    @Override
+    protected ScriptListTable readTable(
+        ReadableFontData data, int baseUnused, boolean dataIsCanonical) {
+      return new ScriptListTable(data, dataIsCanonical);
+    }
+
+    @Override
+    protected void initFields() {
+    }
+
+    @Override
+    public int fieldCount() {
+      return 0;
+    }
+  }
+
+  @Override
+  public int fieldCount() {
+    return 0;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/ScriptTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/ScriptTable.java
new file mode 100644
index 0000000..ad32685
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/ScriptTable.java
@@ -0,0 +1,129 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.TagOffsetsTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ScriptTable extends TagOffsetsTable<LangSysTable> {
+  private static final int FIELD_COUNT = 1;
+
+  private static final int DEFAULT_LANG_SYS_INDEX = 0;
+  private static final int NO_DEFAULT_LANG_SYS = 0;
+
+  ScriptTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  public LangSysTable defaultLangSysTable() {
+    int defaultLangSysOffset = getField(DEFAULT_LANG_SYS_INDEX);
+    if (defaultLangSysOffset == NO_DEFAULT_LANG_SYS) {
+      return null;
+    }
+
+    ReadableFontData newData = data.slice(defaultLangSysOffset);
+    LangSysTable langSysTable = new LangSysTable(newData, dataIsCanonical);
+    return langSysTable;
+  }
+
+  private LanguageTag langSysAt(int index) {
+    return LanguageTag.fromTag(this.tagAt(index));
+  }
+
+  public Map<LanguageTag, LangSysTable> map() {
+    Map<LanguageTag, LangSysTable> map = new HashMap<LanguageTag, LangSysTable>();
+    LangSysTable defaultLangSys = defaultLangSysTable();
+    if (defaultLangSys != null) {
+      map.put(LanguageTag.DFLT, defaultLangSys);
+    }
+    for (int i = 0; i < count(); i++) {
+      LanguageTag lang;
+      try {
+        lang = langSysAt(i);
+      } catch (IllegalArgumentException e) {
+        System.err.println("Invalid LangSys tag found: " + e.getMessage());
+        continue;
+      }
+      map.put(lang, subTableAt(i));
+    }
+    return map;
+  }
+
+  @Override
+  protected LangSysTable readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new LangSysTable(data, dataIsCanonical);
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+
+  static class Builder extends TagOffsetsTable.Builder<ScriptTable, LangSysTable> {
+    private VisibleSubTable.Builder<LangSysTable> defLangSysBuilder;
+
+    Builder() {
+      super();
+    }
+
+    Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data, base, dataIsCanonical);
+      int defLangSys = getField(DEFAULT_LANG_SYS_INDEX);
+      if (defLangSys != NO_DEFAULT_LANG_SYS) {
+        defLangSysBuilder = new LangSysTable.Builder(data.slice(defLangSys), dataIsCanonical);
+      }
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<LangSysTable> createSubTableBuilder(
+        ReadableFontData data, int tag, boolean dataIsCanonical) {
+      return new LangSysTable.Builder(data, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<LangSysTable> createSubTableBuilder() {
+      return new LangSysTable.Builder();
+    }
+
+    @Override
+    protected ScriptTable readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new ScriptTable(data, base, dataIsCanonical);
+    }
+
+    @Override
+    public int subDataSizeToSerialize() {
+      int size = super.subDataSizeToSerialize();
+      if (defLangSysBuilder != null) {
+        size += defLangSysBuilder.subDataSizeToSerialize();
+      }
+      return size;
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      int byteCount = super.subSerialize(newData);
+      if (defLangSysBuilder != null) {
+        byteCount += defLangSysBuilder.subSerialize(newData.slice(byteCount));
+      }
+      return byteCount;
+    }
+
+    @Override
+    public void subDataSet() {
+      super.subDataSet();
+      defLangSysBuilder = null;
+    }
+
+    @Override
+    public int fieldCount() {
+      return FIELD_COUNT;
+    }
+
+    @Override
+    protected void initFields() {
+      setField(DEFAULT_LANG_SYS_INDEX, NO_DEFAULT_LANG_SYS);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/ScriptTag.java b/java/src/com/google/typography/font/sfntly/table/opentype/ScriptTag.java
new file mode 100644
index 0000000..4fd5eee
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/ScriptTag.java
@@ -0,0 +1,155 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.Tag;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+public enum ScriptTag {
+  arab("Arabic"),
+  armn("Armenian"),
+  avst("Avestan"),
+  bali("Balinese"),
+  bamu("Bamum"),
+  batk("Batak"),
+  beng("Bengali"),
+  bng2("Bengali v.2"),
+  bopo("Bopomofo"),
+  brai("Braille"),
+  brah("Brahmi"),
+  bugi("Buginese"),
+  buhd("Buhid"),
+  byzm("Byzantine Music"),
+  cans("Canadian Syllabics"),
+  cari("Carian"),
+  cakm("Chakma"),
+  cham("Cham"),
+  cher("Cherokee"),
+  hani("CJK Ideographic"),
+  copt("Coptic"),
+  cprt("Cypriot Syllabary"),
+  cyrl("Cyrillic"),
+  DFLT("Default"),
+  dsrt("Deseret"),
+  deva("Devanagari"),
+  dev2("Devanagari v.2"),
+  egyp("Egyptian heiroglyphs"),
+  ethi("Ethiopic"),
+  geor("Georgian"),
+  glag("Glagolitic"),
+  goth("Gothic"),
+  grek("Greek"),
+  gujr("Gujarati"),
+  gjr2("Gujarati v.2"),
+  guru("Gurmukhi"),
+  gur2("Gurmukhi v.2"),
+  hang("Hangul"),
+  jamo("Hangul Jamo"),
+  hano("Hanunoo"),
+  hebr("Hebrew"),
+  kana("Hiragana or Katakana"),
+  armi("Imperial Aramaic"),
+  phli("Inscriptional Pahlavi"),
+  prti("Inscriptional Parthian"),
+  java("Javanese"),
+  kthi("Kaithi"),
+  knda("Kannada"),
+  knd2("Kannada v.2"),
+  kali("Kayah Li"),
+  khar("Kharosthi"),
+  khmr("Khmer"),
+  lao("Lao"),
+  latn("Latin"),
+  lepc("Lepcha"),
+  limb("Limbu"),
+  linb("Linear B"),
+  lisu("Lisu (Fraser)"),
+  lyci("Lycian"),
+  lydi("Lydian"),
+  mlym("Malayalam"),
+  mlm2("Malayalam v.2"),
+  mly2("Malayalam v.2 alt"),
+  mand("Mandaic, Mandaean"),
+  math("Mathematical Alphanumeric Symbols"),
+  mtei("Meitei Mayek (Meithei, Meetei)"),
+  merc("Meroitic Cursive"),
+  mero("Meroitic Hieroglyphs"),
+  mong("Mongolian"),
+  musc("Musical Symbols"),
+  musi("Musical Symbols Alt"),
+  mymr("Myanmar"),
+  mym2("Myanmar v.2"),
+  talu("New Tai Lue"),
+  nko("N'Ko"),
+  ogam("Ogham"),
+  olck("Ol Chiki"),
+  ital("Old Italic"),
+  xpeo("Old Persian Cuneiform"),
+  sarb("Old South Arabian"),
+  orkh("Old Turkic, Orkhon Runic"),
+  orya("Odia (formerly Oriya)"),
+  ory2("Odia v.2 (formerly Oriya v.2)"),
+  osma("Osmanya"),
+  phag("Phags-pa"),
+  phnx("Phoenician"),
+  rjng("Rejang"),
+  runr("Runic"),
+  samr("Samaritan"),
+  saur("Saurashtra"),
+  shrd("Sharada"),
+  shaw("Shavian"),
+  sinh("Sinhala"),
+  sora("Sora Sompeng"),
+  xsux("Sumero-Akkadian Cuneiform"),
+  sund("Sundanese"),
+  sylo("Syloti Nagri"),
+  syrc("Syriac"),
+  tglg("Tagalog"),
+  tagb("Tagbanwa"),
+  tale("Tai Le"),
+  lana("Tai Tham (Lanna)"),
+  tavt("Tai Viet"),
+  takr("Takri"),
+  taml("Tamil"),
+  tml2("Tamil v.2"),
+  telu("Telugu"),
+  tel2("Telugu v.2"),
+  thaa("Thaana"),
+  thai("Thai"),
+  tibt("Tibetan"),
+  tfng("Tifinagh"),
+  ugar("Ugaritic Cuneiform"),
+  vai("Vai"),
+  yi("Yi");
+
+  private ScriptTag(String description) {
+    String tag = name();
+    while (tag.length() < 4) {
+      tag = tag + ' ';
+    }
+    this.tag = Tag.intValue(tag);
+    this.description = description;
+  }
+
+  public int tag() {
+    return tag;
+  }
+
+  public String description() {
+    return description;
+  }
+
+  private final int tag;
+  private final String description;
+
+  static ScriptTag fromTag(int tag) {
+    for (ScriptTag script : ScriptTag.values()) {
+      if (script.tag == tag) {
+        return script;
+      }
+    }
+    throw new IllegalArgumentException(Tag.stringValue(tag));
+  }
+}
\ No newline at end of file
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/SingleSubst.java b/java/src/com/google/typography/font/sfntly/table/opentype/SingleSubst.java
new file mode 100644
index 0000000..7d9f764
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/SingleSubst.java
@@ -0,0 +1,113 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.opentype.singlesubst.HeaderFmt1;
+import com.google.typography.font.sfntly.table.opentype.singlesubst.InnerArrayFmt2;
+
+public class SingleSubst extends SubstSubtable {
+  private final HeaderFmt1 fmt1;
+  private final InnerArrayFmt2 fmt2;
+
+  // //////////////
+  // Constructors
+
+  SingleSubst(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    switch (format) {
+    case 1:
+      fmt1 = new HeaderFmt1(data, headerSize(), dataIsCanonical);
+      fmt2 = null;
+      break;
+    case 2:
+      fmt1 = null;
+      fmt2 = new InnerArrayFmt2(data, headerSize(), dataIsCanonical);
+      break;
+    default:
+      throw new IllegalStateException("Subt format value is " + format + " (should be 1 or 2).");
+    }
+  }
+
+  // //////////////////////////////////
+  // Methods specific to this class
+
+  public CoverageTable coverage() {
+    switch (format) {
+    case 1:
+      return fmt1.coverage;
+    case 2:
+      return fmt2.coverage;
+    default:
+      throw new IllegalArgumentException("unexpected format table requested: " + format);
+    }
+  }
+
+  public HeaderFmt1 fmt1Table() {
+    if (format == 1) {
+      return fmt1;
+    }
+    throw new IllegalArgumentException("unexpected format table requested: " + format);
+  }
+
+  public InnerArrayFmt2 fmt2Table() {
+    if (format == 2) {
+      return fmt2;
+    }
+    throw new IllegalArgumentException("unexpected format table requested: " + format);
+  }
+
+  public static class Builder extends SubstSubtable.Builder<SubstSubtable> {
+
+    private final HeaderFmt1.Builder fmt1Builder;
+    private final InnerArrayFmt2.Builder fmt2Builder;
+
+    protected Builder() {
+      super();
+      fmt1Builder = new HeaderFmt1.Builder();
+      fmt2Builder = new InnerArrayFmt2.Builder();
+    }
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+      fmt1Builder = new HeaderFmt1.Builder(data, dataIsCanonical);
+      fmt2Builder = new InnerArrayFmt2.Builder(data, dataIsCanonical);
+    }
+
+    protected Builder(SubstSubtable subTable) {
+      SingleSubst ligSubst = (SingleSubst) subTable;
+      fmt1Builder = new HeaderFmt1.Builder(ligSubst.fmt1);
+      fmt2Builder = new InnerArrayFmt2.Builder(ligSubst.fmt2);
+    }
+
+    @Override
+    public int subDataSizeToSerialize() {
+      return fmt1Builder.subDataSizeToSerialize() + fmt2Builder.subDataSizeToSerialize();
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      int byteCount = fmt1Builder.subSerialize(newData);
+      byteCount += fmt2Builder.subSerialize(newData.slice(byteCount));
+      return byteCount;
+    }
+
+    // /////////////////////////////////
+    // must implement abstract methods
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    @Override
+    public void subDataSet() {
+      fmt1Builder.subDataSet();
+      fmt2Builder.subDataSet();
+    }
+
+    @Override
+    public SingleSubst subBuildTable(ReadableFontData data) {
+      return new SingleSubst(data, 0, true);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/SubstSubtable.java b/java/src/com/google/typography/font/sfntly/table/opentype/SubstSubtable.java
new file mode 100644
index 0000000..e7adb60
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/SubstSubtable.java
@@ -0,0 +1,44 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.HeaderTable;
+
+public abstract class SubstSubtable extends HeaderTable {
+  private static final int FIELD_COUNT = 1;
+  private static final int FORMAT_INDEX = 0;
+  private static final int FORMAT_DEFAULT = 0;
+  public final int format;
+
+  protected SubstSubtable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    format = getField(FORMAT_INDEX);
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+
+  public abstract static class Builder<T extends SubstSubtable> extends HeaderTable.Builder<T> {
+    protected int format;
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data);
+      format = getField(FORMAT_INDEX);
+    }
+
+    protected Builder() {
+      super();
+    }
+
+    @Override
+    protected void initFields() {
+      setField(FORMAT_INDEX, FORMAT_DEFAULT);
+    }
+
+    @Override
+    public int fieldCount() {
+      return FIELD_COUNT;
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/TaggedData.java b/java/src/com/google/typography/font/sfntly/table/opentype/TaggedData.java
new file mode 100644
index 0000000..d7d505c
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/TaggedData.java
@@ -0,0 +1,62 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+public interface TaggedData {
+  /**
+   * @param string
+   *          label
+   * @param start
+   *          start of range to tag
+   * @param length
+   *          length of range to tag
+   * @param depth
+   *          nesting depth of range
+   */
+  void tagRange(String string, int start, int length, int depth);
+
+  /**
+   * @param position
+   *          the position of the field
+   * @param width
+   *          number of bytes for the field at position
+   * @param value
+   *          the value in those bytes
+   * @param alt
+   *          an alternate presentation of the value (in decimal, a tag)
+   * @param label
+   *          the label of this field
+   */
+  void tagField(int position, int width, int value, String alt, String label);
+
+  /**
+   * @param position
+   *          the position of the reference to target
+   * @param value
+   *          the raw value of the field
+   * @param targetPosition
+   *          the target position;
+   * @param label
+   *          name for this reference, or null
+   */
+  void tagTarget(int position, int value, int targetPosition, String label);
+
+  void pushRange(String string, ReadableFontData data);
+
+  void pushRangeAtOffset(String label, int base);
+
+  int tagRangeField(FieldType ft, String label);
+
+  void setRangePosition(int rangePosition);
+
+  void popRange();
+
+  static enum FieldType {
+    TAG, SHORT, SHORT_IGNORED, SHORT_IGNORED_FFFF, OFFSET, OFFSET_NONZERO, OFFSET32, GLYPH;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubClassRule.java b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubClassRule.java
new file mode 100644
index 0000000..68b2a07
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubClassRule.java
@@ -0,0 +1,29 @@
+package com.google.typography.font.sfntly.table.opentype.chaincontextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+
+public class ChainSubClassRule extends ChainSubGenericRule {
+  ChainSubClassRule(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  static class Builder extends ChainSubGenericRule.Builder<ChainSubClassRule> {
+    Builder() {
+      super();
+    }
+
+    Builder(ChainSubClassRule table) {
+      super(table);
+    }
+
+    Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data, base, dataIsCanonical);
+    }
+
+    @Override
+    public ChainSubClassRule subBuildTable(ReadableFontData data) {
+      return new ChainSubClassRule(data, 0, true);
+    }
+
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubClassSet.java b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubClassSet.java
new file mode 100644
index 0000000..d414866
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubClassSet.java
@@ -0,0 +1,53 @@
+package com.google.typography.font.sfntly.table.opentype.chaincontextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class ChainSubClassSet extends ChainSubGenericRuleSet<ChainSubClassRule> {
+  ChainSubClassSet(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  @Override
+  protected ChainSubClassRule readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new ChainSubClassRule(data, base, dataIsCanonical);
+  }
+
+  static class Builder
+      extends ChainSubGenericRuleSet.Builder<ChainSubClassSet, ChainSubClassRule> {
+    Builder() {
+      super();
+    }
+
+    Builder(ChainSubClassSet table) {
+      super(table);
+    }
+
+    Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    @Override
+    protected ChainSubClassSet readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new ChainSubClassSet(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ChainSubClassRule> createSubTableBuilder() {
+      return new ChainSubClassRule.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ChainSubClassRule> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new ChainSubClassRule.Builder(data, 0, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ChainSubClassRule> createSubTableBuilder(
+        ChainSubClassRule subTable) {
+      return new ChainSubClassRule.Builder(subTable);
+    }
+
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubClassSetArray.java b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubClassSetArray.java
new file mode 100644
index 0000000..929c473
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubClassSetArray.java
@@ -0,0 +1,98 @@
+package com.google.typography.font.sfntly.table.opentype.chaincontextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.ClassDefTable;
+import com.google.typography.font.sfntly.table.opentype.CoverageTable;
+import com.google.typography.font.sfntly.table.opentype.component.OffsetRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class ChainSubClassSetArray extends OffsetRecordTable<ChainSubClassSet> {
+  private static final int FIELD_COUNT = 4;
+
+  private static final int COVERAGE_INDEX = 0;
+  private static final int COVERAGE_DEFAULT = 0;
+  private static final int BACKTRACK_CLASS_DEF_INDEX = 1;
+  private static final int BACKTRACK_CLASS_DEF_DEFAULT = 0;
+  private static final int INPUT_CLASS_DEF_INDEX = 2;
+  private static final int INPUT_CLASS_DEF_DEFAULT = 0;
+  private static final int LOOK_AHEAD_CLASS_DEF_INDEX = 3;
+  private static final int LOOK_AHEAD_CLASS_DEF_DEFAULT = 0;
+
+  public final CoverageTable coverage;
+  public final ClassDefTable backtrackClassDef;
+  public final ClassDefTable inputClassDef;
+  public final ClassDefTable lookAheadClassDef;
+
+  public ChainSubClassSetArray(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    int coverageOffset = getField(COVERAGE_INDEX);
+    coverage = new CoverageTable(data.slice(coverageOffset), 0, dataIsCanonical);
+    int classDefOffset = getField(BACKTRACK_CLASS_DEF_INDEX);
+    backtrackClassDef = new ClassDefTable(data.slice(classDefOffset), 0, dataIsCanonical);
+    classDefOffset = getField(INPUT_CLASS_DEF_INDEX);
+    inputClassDef = new ClassDefTable(data.slice(classDefOffset), 0, dataIsCanonical);
+    classDefOffset = getField(LOOK_AHEAD_CLASS_DEF_INDEX);
+    lookAheadClassDef = new ClassDefTable(data.slice(classDefOffset), 0, dataIsCanonical);
+  }
+
+  @Override
+  public ChainSubClassSet readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new ChainSubClassSet(data, 0, dataIsCanonical);
+  }
+
+  public static class Builder
+      extends OffsetRecordTable.Builder<ChainSubClassSetArray, ChainSubClassSet> {
+
+    protected Builder() {
+      super();
+    }
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    protected Builder(ChainSubClassSetArray table) {
+      super(table);
+    }
+
+    @Override
+    protected ChainSubClassSetArray readTable(
+        ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new ChainSubClassSetArray(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ChainSubClassSet> createSubTableBuilder() {
+      return new ChainSubClassSet.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ChainSubClassSet> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new ChainSubClassSet.Builder(data, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ChainSubClassSet> createSubTableBuilder(ChainSubClassSet subTable) {
+      return new ChainSubClassSet.Builder(subTable);
+    }
+
+    @Override
+    protected void initFields() {
+      setField(COVERAGE_INDEX, COVERAGE_DEFAULT);
+      setField(BACKTRACK_CLASS_DEF_INDEX, BACKTRACK_CLASS_DEF_DEFAULT);
+      setField(INPUT_CLASS_DEF_INDEX, INPUT_CLASS_DEF_DEFAULT);
+      setField(LOOK_AHEAD_CLASS_DEF_INDEX, LOOK_AHEAD_CLASS_DEF_DEFAULT);
+    }
+
+    @Override
+    public int fieldCount() {
+      return FIELD_COUNT;
+    }
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubGenericRule.java b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubGenericRule.java
new file mode 100644
index 0000000..a2f948b
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubGenericRule.java
@@ -0,0 +1,122 @@
+package com.google.typography.font.sfntly.table.opentype.chaincontextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.SubTable;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.SubstLookupRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class ChainSubGenericRule extends SubTable {
+  public final NumRecordList backtrackGlyphs;
+  public final NumRecordList inputClasses;
+  public final NumRecordList lookAheadGlyphs;
+  public final SubstLookupRecordList lookupRecords;
+
+  // //////////////
+  // Constructors
+
+  protected ChainSubGenericRule(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data);
+    backtrackGlyphs = new NumRecordList(data);
+    inputClasses = new NumRecordList(data, 1, backtrackGlyphs.limit());
+    lookAheadGlyphs = new NumRecordList(data, 0, inputClasses.limit());
+    lookupRecords = new SubstLookupRecordList(
+        data, lookAheadGlyphs.limit(), lookAheadGlyphs.limit() + 2);
+  }
+
+  abstract static class Builder<T extends ChainSubGenericRule> extends
+      VisibleSubTable.Builder<T> {
+    private NumRecordList backtrackGlyphsBuilder;
+    private NumRecordList inputGlyphsBuilder;
+    private NumRecordList lookAheadGlyphsBuilder;
+    private SubstLookupRecordList lookupRecordsBuilder;
+
+    protected Builder() {
+      super();
+    }
+
+    protected Builder(ChainSubGenericRule table) {
+      this(table.readFontData(), 0, false);
+    }
+
+    protected Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data);
+      if (!dataIsCanonical) {
+        prepareToEdit();
+      }
+    }
+
+    @Override
+    public int subDataSizeToSerialize() {
+      if (lookupRecordsBuilder != null) {
+        serializedLength = lookupRecordsBuilder.limit();
+      } else {
+        computeSizeFromData(internalReadData());
+      }
+      return serializedLength;
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      if (serializedLength == 0) {
+        return 0;
+      }
+
+      if (backtrackGlyphsBuilder == null || inputGlyphsBuilder == null
+          || lookAheadGlyphsBuilder == null || lookupRecordsBuilder == null) {
+        return serializeFromData(newData);
+      }
+
+      return backtrackGlyphsBuilder.writeTo(newData) + inputGlyphsBuilder.writeTo(newData)
+          + lookAheadGlyphsBuilder.writeTo(newData) + lookupRecordsBuilder.writeTo(newData);
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    @Override
+    public void subDataSet() {
+      backtrackGlyphsBuilder = null;
+      inputGlyphsBuilder = null;
+      lookupRecordsBuilder = null;
+      lookAheadGlyphsBuilder = null;
+    }
+
+    // ////////////////////////////////////
+    // private methods
+
+    private void prepareToEdit() {
+      initFromData(internalReadData());
+      setModelChanged();
+    }
+
+    private void initFromData(ReadableFontData data) {
+      if (backtrackGlyphsBuilder == null || inputGlyphsBuilder == null
+          || lookAheadGlyphsBuilder == null || lookupRecordsBuilder == null) {
+        backtrackGlyphsBuilder = new NumRecordList(data);
+        inputGlyphsBuilder = new NumRecordList(data, 0, backtrackGlyphsBuilder.limit());
+        lookAheadGlyphsBuilder = new NumRecordList(data, 0, inputGlyphsBuilder.limit());
+        lookupRecordsBuilder = new SubstLookupRecordList(data, lookAheadGlyphsBuilder.limit());
+      }
+    }
+
+    private void computeSizeFromData(ReadableFontData data) {
+      // This assumes canonical data.
+      int len = 0;
+      if (data != null) {
+        len = data.length();
+      }
+      serializedLength = len;
+    }
+
+    private int serializeFromData(WritableFontData newData) {
+      // The source data must be canonical.
+      ReadableFontData data = internalReadData();
+      data.copyTo(newData);
+      return data.length();
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubGenericRuleSet.java b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubGenericRuleSet.java
new file mode 100644
index 0000000..887ed76
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubGenericRuleSet.java
@@ -0,0 +1,42 @@
+package com.google.typography.font.sfntly.table.opentype.chaincontextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.OffsetRecordTable;
+
+public abstract class ChainSubGenericRuleSet<T extends ChainSubGenericRule>
+    extends OffsetRecordTable<T> {
+  protected ChainSubGenericRuleSet(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  @Override
+  public int fieldCount() {
+    return 0;
+  }
+
+  static abstract class Builder<
+      T extends ChainSubGenericRuleSet<S>, S extends ChainSubGenericRule>
+      extends OffsetRecordTable.Builder<T, S> {
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    protected Builder() {
+      super();
+    }
+
+    protected Builder(T table) {
+      super(table);
+    }
+
+    @Override
+    protected void initFields() {
+    }
+
+    @Override
+    public int fieldCount() {
+      return 0;
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubRule.java b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubRule.java
new file mode 100644
index 0000000..2de2a22
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubRule.java
@@ -0,0 +1,28 @@
+package com.google.typography.font.sfntly.table.opentype.chaincontextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+
+public class ChainSubRule extends ChainSubGenericRule {
+  ChainSubRule(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  static class Builder extends ChainSubGenericRule.Builder<ChainSubRule> {
+    Builder() {
+      super();
+    }
+
+    Builder(ChainSubRule table) {
+      super(table);
+    }
+
+    Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data, base, dataIsCanonical);
+    }
+
+    @Override
+    public ChainSubRule subBuildTable(ReadableFontData data) {
+      return new ChainSubRule(data, 0, true);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubRuleSet.java b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubRuleSet.java
new file mode 100644
index 0000000..f97742c
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubRuleSet.java
@@ -0,0 +1,52 @@
+package com.google.typography.font.sfntly.table.opentype.chaincontextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class ChainSubRuleSet extends ChainSubGenericRuleSet<ChainSubRule> {
+  ChainSubRuleSet(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  @Override
+  protected ChainSubRule readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new ChainSubRule(data, base, dataIsCanonical);
+  }
+
+  static class Builder
+      extends ChainSubGenericRuleSet.Builder<ChainSubRuleSet, ChainSubRule> {
+
+    Builder() {
+      super();
+    }
+
+    Builder(ChainSubRuleSet table) {
+      super(table);
+    }
+
+    Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    @Override
+    protected ChainSubRuleSet readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new ChainSubRuleSet(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ChainSubRule> createSubTableBuilder() {
+      return new ChainSubRule.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ChainSubRule> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new ChainSubRule.Builder(data, 0, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ChainSubRule> createSubTableBuilder(ChainSubRule subTable) {
+      return new ChainSubRule.Builder(subTable);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubRuleSetArray.java b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubRuleSetArray.java
new file mode 100644
index 0000000..24948ee
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/ChainSubRuleSetArray.java
@@ -0,0 +1,80 @@
+package com.google.typography.font.sfntly.table.opentype.chaincontextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.CoverageTable;
+import com.google.typography.font.sfntly.table.opentype.component.OffsetRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class ChainSubRuleSetArray extends OffsetRecordTable<ChainSubRuleSet> {
+  private static final int FIELD_COUNT = 1;
+
+  private static final int COVERAGE_INDEX = 0;
+  private static final int COVERAGE_DEFAULT = 0;
+
+  public final CoverageTable coverage;
+
+  public ChainSubRuleSetArray(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    int coverageOffset = getField(COVERAGE_INDEX);
+    coverage = new CoverageTable(data.slice(coverageOffset), 0, dataIsCanonical);
+  }
+
+  @Override
+  public ChainSubRuleSet readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new ChainSubRuleSet(data, 0, dataIsCanonical);
+  }
+
+  public static class Builder
+      extends OffsetRecordTable.Builder<ChainSubRuleSetArray, ChainSubRuleSet> {
+
+    public Builder() {
+      super();
+    }
+
+    public Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    public Builder(ChainSubRuleSetArray table) {
+      super(table);
+    }
+
+    @Override
+    protected ChainSubRuleSetArray readTable(
+        ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new ChainSubRuleSetArray(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ChainSubRuleSet> createSubTableBuilder() {
+      return new ChainSubRuleSet.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ChainSubRuleSet> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new ChainSubRuleSet.Builder(data, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<ChainSubRuleSet> createSubTableBuilder(
+        ChainSubRuleSet subTable) {
+      return new ChainSubRuleSet.Builder(subTable);
+    }
+
+    @Override
+    protected void initFields() {
+      setField(COVERAGE_INDEX, COVERAGE_DEFAULT);
+    }
+
+    @Override
+    public int fieldCount() {
+      return FIELD_COUNT;
+    }
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/CoverageArray.java b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/CoverageArray.java
new file mode 100644
index 0000000..8877fb3
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/CoverageArray.java
@@ -0,0 +1,66 @@
+package com.google.typography.font.sfntly.table.opentype.chaincontextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.CoverageTable;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.OffsetRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class CoverageArray extends OffsetRecordTable<CoverageTable> {
+  private static final int FIELD_COUNT = 0;
+
+  private CoverageArray(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  public CoverageArray(NumRecordList records) {
+    super(records);
+  }
+
+  @Override
+  protected CoverageTable readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new CoverageTable(data, 0, dataIsCanonical);
+  }
+
+  public static class Builder extends OffsetRecordTable.Builder<CoverageArray, CoverageTable> {
+
+    public Builder(NumRecordList records) {
+      super(records);
+    }
+
+    @Override
+    protected CoverageArray readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new CoverageArray(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<CoverageTable> createSubTableBuilder() {
+      return new CoverageTable.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<CoverageTable> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new CoverageTable.Builder(data, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<CoverageTable> createSubTableBuilder(CoverageTable subTable) {
+      return new CoverageTable.Builder(subTable);
+    }
+
+    @Override
+    protected void initFields() {
+    }
+
+    @Override
+    public int fieldCount() {
+      return FIELD_COUNT;
+    }
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/InnerArraysFmt3.java b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/InnerArraysFmt3.java
new file mode 100644
index 0000000..89ba0e9
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/chaincontextsubst/InnerArraysFmt3.java
@@ -0,0 +1,162 @@
+package com.google.typography.font.sfntly.table.opentype.chaincontextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.SubTable;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.SubstLookupRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class InnerArraysFmt3 extends SubTable {
+  public final CoverageArray backtrackGlyphs;
+  public final CoverageArray inputGlyphs;
+  public final CoverageArray lookAheadGlyphs;
+  public final SubstLookupRecordList lookupRecords;
+
+  // //////////////
+  // Constructors
+
+  public InnerArraysFmt3(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data);
+    NumRecordList records = new NumRecordList(data, 0, base);
+    backtrackGlyphs = new CoverageArray(records);
+
+    records = new NumRecordList(data, 0, records.limit());
+    inputGlyphs = new CoverageArray(records);
+
+    records = new NumRecordList(data, 0, records.limit());
+    lookAheadGlyphs = new CoverageArray(records);
+
+    lookupRecords = new SubstLookupRecordList(data, records.limit());
+  }
+
+  public static class Builder extends VisibleSubTable.Builder<InnerArraysFmt3> {
+    private CoverageArray.Builder backtrackGlyphsBuilder;
+    private CoverageArray.Builder inputGlyphsBuilder;
+    private CoverageArray.Builder lookAheadGlyphsBuilder;
+    private SubstLookupRecordList lookupRecordsBuilder;
+
+    protected Builder() {
+      super();
+    }
+
+    protected Builder(InnerArraysFmt3 table) {
+      this(table.readFontData(), 0, false);
+    }
+
+    protected Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data);
+      if (!dataIsCanonical) {
+        prepareToEdit();
+      }
+    }
+
+    protected Builder(Builder other) {
+      super();
+      backtrackGlyphsBuilder = other.backtrackGlyphsBuilder;
+      inputGlyphsBuilder = other.inputGlyphsBuilder;
+      lookAheadGlyphsBuilder = other.lookAheadGlyphsBuilder;
+      lookupRecordsBuilder = other.lookupRecordsBuilder;
+    }
+
+    @Override
+    public int subDataSizeToSerialize() {
+      if (lookupRecordsBuilder != null) {
+        serializedLength = lookupRecordsBuilder.limit();
+      } else {
+        computeSizeFromData(internalReadData());
+      }
+      return serializedLength;
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      if (serializedLength == 0) {
+        return 0;
+      }
+
+      if (backtrackGlyphsBuilder == null || inputGlyphsBuilder == null
+          || lookAheadGlyphsBuilder == null || lookupRecordsBuilder == null) {
+        return serializeFromData(newData);
+      }
+
+      int tableOnlySize = 0;
+      tableOnlySize += backtrackGlyphsBuilder.tableSizeToSerialize();
+      tableOnlySize += inputGlyphsBuilder.tableSizeToSerialize();
+      tableOnlySize += lookAheadGlyphsBuilder.tableSizeToSerialize();
+      int subTableWriteOffset = tableOnlySize
+          + lookupRecordsBuilder.writeTo(newData.slice(tableOnlySize));
+
+      backtrackGlyphsBuilder.subSerialize(newData, subTableWriteOffset);
+      subTableWriteOffset += backtrackGlyphsBuilder.subTableSizeToSerialize();
+      int tableWriteOffset = backtrackGlyphsBuilder.tableSizeToSerialize();
+
+      inputGlyphsBuilder.subSerialize(newData.slice(tableWriteOffset), subTableWriteOffset);
+      subTableWriteOffset += inputGlyphsBuilder.subTableSizeToSerialize();
+      tableWriteOffset += inputGlyphsBuilder.tableSizeToSerialize();
+
+      lookAheadGlyphsBuilder.subSerialize(newData.slice(tableWriteOffset), subTableWriteOffset);
+      subTableWriteOffset += lookAheadGlyphsBuilder.subTableSizeToSerialize();
+
+      return subTableWriteOffset;
+    }
+
+    @Override
+    public InnerArraysFmt3 subBuildTable(ReadableFontData data) {
+      return new InnerArraysFmt3(data, 0, true);
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    @Override
+    public void subDataSet() {
+      backtrackGlyphsBuilder = null;
+      inputGlyphsBuilder = null;
+      lookupRecordsBuilder = null;
+      lookAheadGlyphsBuilder = null;
+    }
+
+    // ////////////////////////////////////
+    // private methods
+
+    private void prepareToEdit() {
+      initFromData(internalReadData());
+      setModelChanged();
+    }
+
+    private void initFromData(ReadableFontData data) {
+      if (backtrackGlyphsBuilder == null || inputGlyphsBuilder == null
+          || lookAheadGlyphsBuilder == null || lookupRecordsBuilder == null) {
+        NumRecordList records = new NumRecordList(data);
+        backtrackGlyphsBuilder = new CoverageArray.Builder(records);
+
+        records = new NumRecordList(data, 0, records.limit());
+        inputGlyphsBuilder = new CoverageArray.Builder(records);
+
+        records = new NumRecordList(data, 0, records.limit());
+        lookAheadGlyphsBuilder = new CoverageArray.Builder(records);
+
+        lookupRecordsBuilder = new SubstLookupRecordList(data, records.limit());
+      }
+    }
+
+    private void computeSizeFromData(ReadableFontData data) {
+      // This assumes canonical data.
+      int len = 0;
+      if (data != null) {
+        len = data.length();
+      }
+      serializedLength = len;
+    }
+
+    private int serializeFromData(WritableFontData newData) {
+      // The source data must be canonical.
+      ReadableFontData data = internalReadData();
+      data.copyTo(newData);
+      return data.length();
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/classdef/InnerArrayFmt1.java b/java/src/com/google/typography/font/sfntly/table/opentype/classdef/InnerArrayFmt1.java
new file mode 100644
index 0000000..c5b609c
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/classdef/InnerArrayFmt1.java
@@ -0,0 +1,57 @@
+package com.google.typography.font.sfntly.table.opentype.classdef;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecord;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.RecordList;
+import com.google.typography.font.sfntly.table.opentype.component.RecordsTable;
+
+public class InnerArrayFmt1 extends RecordsTable<NumRecord> {
+  private static final int FIELD_COUNT = 1;
+
+  public static final int START_GLYPH_INDEX = 0;
+  private static final int START_GLYPH_CONST = 0;
+
+  public InnerArrayFmt1(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  @Override
+  protected RecordList<NumRecord> createRecordList(ReadableFontData data) {
+    return new NumRecordList(data);
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+
+  public static class Builder extends RecordsTable.Builder<InnerArrayFmt1, NumRecord> {
+    public Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected void initFields() {
+      setField(START_GLYPH_INDEX, START_GLYPH_CONST);
+    }
+
+    @Override
+    protected InnerArrayFmt1 readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new InnerArrayFmt1(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected RecordList<NumRecord> readRecordList(ReadableFontData data, int base) {
+      if (base != 0) {
+        throw new UnsupportedOperationException();
+      }
+      return new NumRecordList(data);
+    }
+
+    @Override
+    public int fieldCount() {
+      return FIELD_COUNT;
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/GlyphClassList.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/GlyphClassList.java
new file mode 100644
index 0000000..3f86b0e
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/GlyphClassList.java
@@ -0,0 +1,31 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+
+public class GlyphClassList extends NumRecordList {
+  private GlyphClassList(WritableFontData data) {
+    super(data);
+  }
+
+  private GlyphClassList(ReadableFontData data) {
+    super(data);
+  }
+
+  private GlyphClassList(ReadableFontData data, int countDecrement) {
+    super(data, countDecrement);
+  }
+
+  private GlyphClassList(
+      ReadableFontData data, int countDecrement, int countOffset, int valuesOffset) {
+    super(data, countDecrement, countOffset, valuesOffset);
+  }
+
+  public GlyphClassList(NumRecordList other) {
+    super(other);
+  }
+
+  public static int sizeOfListOfCount(int count) {
+    return RecordList.DATA_OFFSET + count * NumRecord.RECORD_SIZE;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/GlyphGroup.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/GlyphGroup.java
new file mode 100644
index 0000000..892df1f
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/GlyphGroup.java
@@ -0,0 +1,141 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.table.core.PostScriptTable;
+
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+public class GlyphGroup extends BitSet implements Iterable<Integer> {
+  private static final long serialVersionUID = 1L;
+
+  private boolean inverse = false;
+
+  public GlyphGroup() {
+    super();
+  }
+
+  GlyphGroup(int glyph) {
+    super.set(glyph);
+  }
+
+  GlyphGroup(Collection<Integer> glyphs) {
+    for (int glyph : glyphs) {
+      super.set(glyph);
+    }
+  }
+
+  static GlyphGroup inverseGlyphGroup(Collection<GlyphGroup> glyphGroups) {
+    GlyphGroup result = new GlyphGroup();
+    for(GlyphGroup glyphGroup : glyphGroups) {
+      result.or(glyphGroup);
+    }
+    result.inverse = true;
+    return result;
+  }
+
+  public void add(int glyph) {
+    this.set(glyph);
+  }
+
+  void addAll(Collection<Integer> glyphs) {
+    for (int glyph : glyphs) {
+      super.set(glyph);
+    }
+  }
+
+  void addAll(GlyphGroup other) {
+    this.or(other);
+  }
+
+  void copyTo(Collection<Integer> target) {
+    List<Integer> list = new LinkedList<Integer>();
+    for ( int i = this.nextSetBit( 0 ); i >= 0; i = this.nextSetBit( i + 1 ) ) {
+      target.add(i);
+    }
+  }
+
+  GlyphGroup intersection(GlyphGroup other) {
+    GlyphGroup intersection = new GlyphGroup();
+    if (this.inverse && !other.inverse) {
+      intersection.or(other);
+      intersection.andNot(this);
+    } else if (other.inverse && !this.inverse) {
+      intersection.or(this);
+      intersection.andNot(other);
+    } else if (other.inverse && this.inverse) {
+      intersection.inverse = true;
+      intersection.or(this);
+      intersection.or(other);
+    } else {
+      intersection.or(this);
+      intersection.and(other);
+    }
+    return intersection;
+  }
+
+  boolean contains(int glyph) {
+    return get(glyph) ^ inverse;
+  }
+
+  @Override
+  public int size() {
+    return cardinality();
+  }
+
+  @Override
+  public Iterator<Integer> iterator() {
+    return new Iterator<Integer>() {
+      int i = 0;
+      @Override
+      public boolean hasNext() {
+        return nextSetBit(i) >= 0 ;
+      }
+
+      @Override
+      public Integer next() {
+        i = nextSetBit(i);
+        return i++;
+      }
+
+      @Override
+      public void remove() {
+        throw new UnsupportedOperationException();
+      }
+    };
+  }
+
+  @Override
+  public String toString() {
+    return toString(null);
+  }
+
+  public String toString(PostScriptTable post) {
+    StringBuilder sb = new StringBuilder();
+    if (this.inverse) {
+      sb.append("not-");
+    }
+    int glyphCount = size();
+    if (glyphCount > 1) {
+      sb.append("[ ");
+    }
+    for (int glyphId : this) {
+      sb.append(glyphId);
+
+      if (post != null) {
+        String glyphName = post.glyphName(glyphId);
+        if (glyphName != null) {
+          sb.append("-");
+          sb.append(glyphName);
+        }
+      }
+      sb.append(" ");
+    }
+    if (glyphCount > 1) {
+      sb.append("] ");
+    }
+    return sb.toString();
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/GlyphList.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/GlyphList.java
new file mode 100644
index 0000000..67a6a6b
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/GlyphList.java
@@ -0,0 +1,11 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import java.util.ArrayList;
+
+class GlyphList extends ArrayList<Integer> {
+  private static final long serialVersionUID = 4699092062720505377L;
+
+  GlyphList() {
+    super();
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/GsubLookupType.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/GsubLookupType.java
new file mode 100644
index 0000000..e7bf7da
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/GsubLookupType.java
@@ -0,0 +1,32 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+public enum GsubLookupType implements LookupType {
+  GSUB_SINGLE,
+  GSUB_MULTIPLE,
+  GSUB_ALTERNATE,
+  GSUB_LIGATURE,
+  GSUB_CONTEXTUAL,
+  GSUB_CHAINING_CONTEXTUAL,
+  GSUB_EXTENSION,
+  GSUB_REVERSE_CHAINING_CONTEXTUAL_SINGLE;
+
+  @Override
+  public int typeNum() {
+    return ordinal() + 1;
+  }
+
+  @Override
+  public String toString() {
+    return super.toString().toLowerCase();
+  }
+
+  public static GsubLookupType forTypeNum(int typeNum) {
+    if (typeNum <= 0 || typeNum > values.length) {
+      System.err.format("unknown gsub lookup typeNum: %d\n", typeNum);
+      return null;
+    }
+    return values[typeNum - 1];
+  }
+
+  private static final GsubLookupType[] values = values();
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/HeaderTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/HeaderTable.java
new file mode 100644
index 0000000..a90571e
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/HeaderTable.java
@@ -0,0 +1,104 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.SubTable;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+public abstract class HeaderTable extends SubTable {
+  protected static final int FIELD_SIZE = 2;
+  protected boolean dataIsCanonical = false;
+  protected int base = 0;
+
+  protected HeaderTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data);
+    this.base = base;
+    this.dataIsCanonical = dataIsCanonical;
+  }
+
+  public int getField(int index) {
+    return data.readUShort(base + index * FIELD_SIZE);
+  }
+
+  public int headerSize() {
+    return FIELD_SIZE * fieldCount();
+  }
+
+  public abstract int fieldCount();
+
+  public abstract static class Builder<T extends HeaderTable> extends VisibleSubTable.Builder<T> {
+    private Map<Integer, Integer> map = new HashMap<Integer, Integer>();
+    protected boolean dataIsCanonical = false;
+
+    protected Builder() {
+      super();
+      initFields();
+    }
+
+    protected Builder(ReadableFontData data) {
+      super(data);
+      initFields();
+    }
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data);
+      this.dataIsCanonical = dataIsCanonical;
+      initFields();
+    }
+
+    protected Builder(T table) {
+      super();
+      initFields();
+      for (int i = 0; i < table.fieldCount(); i++) {
+        map.put(i, table.getField(i));
+      }
+    }
+
+    protected int setField(int index, int value) {
+      return map.put(index, value);
+    }
+
+    protected int getField(int index) {
+      return map.get(index);
+    }
+
+    protected abstract void initFields();
+
+    protected abstract int fieldCount();
+
+    public int headerSize() {
+      return FIELD_SIZE * fieldCount();
+    }
+
+    /**
+     * Even though public, not to be used by the end users. Made public only
+     * make it available to packages under
+     * {@code com.google.typography.font.sfntly.table.opentype}.
+     */
+    @Override
+    public int subDataSizeToSerialize() {
+      return headerSize();
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      for (Entry<Integer, Integer> entry : map.entrySet()) {
+        newData.writeUShort(entry.getKey() * FIELD_SIZE, entry.getValue());
+      }
+      return headerSize();
+    }
+
+    @Override
+    public void subDataSet() {
+      map = new HashMap<Integer, Integer>();
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/LookupFlag.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/LookupFlag.java
new file mode 100644
index 0000000..556e2f3
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/LookupFlag.java
@@ -0,0 +1,38 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+enum LookupFlag {
+  RIGHT_TO_LEFT(1),
+  IGNORE_BASE_GLYPHS(2),
+  IGNORE_LIGATURES(4),
+  IGNORE_MARKS(8);
+
+  boolean isSet(int flags) {
+    return isFlagSet(flags, mask);
+  }
+
+  int set(int flags) {
+    return setFlag(flags, mask);
+  }
+
+  int clear(int flags) {
+    return clearFlag(flags, mask);
+  }
+
+  private final int mask;
+  private LookupFlag(int mask) {
+    this.mask = mask;
+  }
+  
+  static boolean isFlagSet(int flags, int mask) {
+    return (flags & mask) != 0;
+  }
+
+  static int setFlag(int flags, int mask) {
+    return flags | mask;
+  }
+
+  static int clearFlag(int flags, int mask) {
+    return flags & ~mask;
+  }
+}
+
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/LookupType.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/LookupType.java
new file mode 100644
index 0000000..adbc9f5
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/LookupType.java
@@ -0,0 +1,5 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+public interface LookupType {
+  int typeNum();
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/NumRecord.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/NumRecord.java
new file mode 100644
index 0000000..512e754
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/NumRecord.java
@@ -0,0 +1,24 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+
+public final class NumRecord implements Record {
+  static final int RECORD_SIZE = 2;
+  private static final int TAG_POS = 0;
+  final int value;
+
+  NumRecord(ReadableFontData data, int base) {
+    this.value = data.readUShort(base + TAG_POS);
+  }
+
+  public NumRecord(int num) {
+    this.value = num;
+  }
+
+  @Override
+  public int writeTo(WritableFontData newData, int base) {
+    newData.writeUShort(base + TAG_POS, value);
+    return RECORD_SIZE;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/NumRecordList.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/NumRecordList.java
new file mode 100644
index 0000000..9816020
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/NumRecordList.java
@@ -0,0 +1,58 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+
+import java.util.Iterator;
+
+public class NumRecordList extends RecordList<NumRecord> {
+  public NumRecordList(WritableFontData data) {
+    super(data);
+  }
+
+  public NumRecordList(ReadableFontData data) {
+    super(data);
+  }
+
+  public NumRecordList(ReadableFontData data, int countDecrement) {
+    super(data, countDecrement);
+  }
+
+  public NumRecordList(ReadableFontData data, int countDecrement, int countOffset) {
+    super(data, countDecrement, countOffset);
+  }
+
+  public NumRecordList(
+      ReadableFontData data, int countDecrement, int countOffset, int valuesOffset) {
+    super(data, countDecrement, countOffset, valuesOffset);
+  }
+
+  public NumRecordList(NumRecordList other) {
+    super(other);
+  }
+
+  public static int sizeOfListOfCount(int count) {
+    return RecordList.DATA_OFFSET + count * NumRecord.RECORD_SIZE;
+  }
+
+  public boolean contains(int value) {
+    Iterator<NumRecord> iterator = iterator();
+    while (iterator.hasNext()) {
+      NumRecord record = iterator.next();
+      if (record.value == value) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  protected NumRecord getRecordAt(ReadableFontData data, int offset) {
+    return new NumRecord(data, offset);
+  }
+
+  @Override
+  protected int recordSize() {
+    return NumRecord.RECORD_SIZE;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/NumRecordTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/NumRecordTable.java
new file mode 100644
index 0000000..1aa3a5e
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/NumRecordTable.java
@@ -0,0 +1,63 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+
+public class NumRecordTable extends RecordsTable<NumRecord> {
+
+  public NumRecordTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  public NumRecordTable(NumRecordList records) {
+    super(records);
+  }
+
+  @Override
+  protected RecordList<NumRecord> createRecordList(ReadableFontData data) {
+    return new NumRecordList(data);
+  }
+
+  @Override
+  public int fieldCount() {
+    return 0;
+  }
+
+  public static class Builder extends RecordsTable.Builder<NumRecordTable, NumRecord> {
+    public Builder() {
+      super();
+    }
+
+    public Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data, base, dataIsCanonical);
+    }
+
+    public Builder(NumRecordTable table) {
+      super(table);
+    }
+
+    @Override
+    protected NumRecordTable readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      if (base != 0) {
+        throw new UnsupportedOperationException();
+      }
+      return new NumRecordTable(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected RecordList<NumRecord> readRecordList(ReadableFontData data, int base) {
+      if (base != 0) {
+        throw new UnsupportedOperationException();
+      }
+      return new NumRecordList(data);
+    }
+
+    @Override
+    protected int fieldCount() {
+      return 0;
+    }
+
+    @Override
+    protected void initFields() {
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/OffsetRecordTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/OffsetRecordTable.java
new file mode 100644
index 0000000..feb0bc7
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/OffsetRecordTable.java
@@ -0,0 +1,349 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.SubTable;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+public abstract class OffsetRecordTable<S extends SubTable> extends HeaderTable
+implements Iterable<S> {
+  public final NumRecordList recordList;
+
+  // ///////////////
+  // constructors
+
+  protected OffsetRecordTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    recordList = new NumRecordList(data.slice(base + headerSize()));
+  }
+
+  protected OffsetRecordTable(ReadableFontData data, boolean dataIsCanonical) {
+    this(data, 0, dataIsCanonical);
+  }
+
+  protected OffsetRecordTable(NumRecordList records) {
+    super(records.readData, records.base, false);
+    recordList = records;
+  }
+
+  // ////////////////
+  // public methods
+
+  public int subTableCount() {
+    return recordList.count();
+  }
+
+  public S subTableAt(int index) {
+    NumRecord record = recordList.get(index);
+    return subTableForRecord(record);
+  }
+
+  @Override
+  public Iterator<S> iterator() {
+    return new Iterator<S>() {
+      Iterator<NumRecord> recordIterator = recordList.iterator();
+
+      @Override
+      public boolean hasNext() {
+        return recordIterator.hasNext();
+      }
+
+      @Override
+      public S next() {
+        if (!hasNext()) {
+          throw new NoSuchElementException();
+        }
+        NumRecord record = recordIterator.next();
+        return subTableForRecord(record);
+      }
+
+      @Override
+      public void remove() {
+        throw new UnsupportedOperationException();
+      }
+    };
+  }
+
+  // ////////////////////////////////////
+  // implementations pushed to subclasses
+
+  protected abstract S readSubTable(ReadableFontData data, boolean dataIsCanonical);
+
+  // ////////////////////////////////////
+  // private methods
+
+  private S subTableForRecord(NumRecord record) {
+    if (record.value == 0) {
+      // No reference to itself is allowed.
+      return null;
+    }
+    ReadableFontData newBase = data.slice(record.value);
+    return readSubTable(newBase, dataIsCanonical);
+  }
+
+  public abstract static class Builder<
+  T extends OffsetRecordTable<? extends SubTable>, S extends SubTable>
+  extends HeaderTable.Builder<T> {
+
+    private List<VisibleSubTable.Builder<S>> builders;
+    private boolean dataIsCanonical;
+    private int serializedLength;
+    private int serializedCount;
+    private final int base;
+    private int serializedSubtablePartLength;
+    private int serializedTablePartLength;
+
+    protected Builder() {
+      super();
+      base = 0;
+    }
+
+    protected Builder(T table) {
+      this(table.readFontData(), table.base, table.dataIsCanonical);
+    }
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      this(data, 0, dataIsCanonical);
+    }
+
+    protected Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data);
+      this.base = base;
+      this.dataIsCanonical = dataIsCanonical;
+      if (!dataIsCanonical) {
+        prepareToEdit();
+      }
+    }
+
+    protected Builder(NumRecordList records) {
+      super();
+      base = records.base;
+      if (builders == null) {
+        initFromData(records);
+        setModelChanged();
+      }
+    }
+
+    // ////////////////
+    // public methods
+
+    public int subTableCount() {
+      if (builders == null) {
+        return new NumRecordList(internalReadData().slice(base + headerSize())).count();
+      }
+      return builders.size();
+    }
+
+    public SubTable.Builder<? extends SubTable> builderForTag(int tag) {
+      prepareToEdit();
+      return builders.get(tag);
+    }
+
+    public VisibleSubTable.Builder<S> addBuilder() {
+      prepareToEdit();
+      VisibleSubTable.Builder<S> builder = createSubTableBuilder();
+      builders.add(builder);
+      return builder;
+    }
+
+    public VisibleSubTable.Builder<S> addBuilder(S subTable) {
+      prepareToEdit();
+      VisibleSubTable.Builder<S> builder = createSubTableBuilder(subTable);
+      builders.add(builder);
+      return builder;
+    }
+
+    public void removeBuilderForTag(int tag) {
+      prepareToEdit();
+      builders.remove(tag);
+    }
+
+    public int limit() {
+      return base + serializedLength;
+    }
+
+    // ////////////////////////////////////
+    // overriden methods
+
+    @Override
+    public int subDataSizeToSerialize() {
+      if (builders != null) {
+        computeSizeFromBuilders();
+      } else {
+        computeSizeFromData(internalReadData().slice(base + headerSize()));
+      }
+      return serializedLength;
+    }
+
+    public int tableSizeToSerialize() {
+      computeSizeFromBuilders();
+      return serializedTablePartLength;
+    }
+
+    public int subTableSizeToSerialize() {
+      computeSizeFromBuilders();
+      return serializedSubtablePartLength;
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    public int subSerialize(WritableFontData newData, int subTableWriteOffset) {
+      if (serializedLength == 0) {
+        return 0;
+      }
+
+      if (builders != null) {
+        return serializeFromBuilders(newData, subTableWriteOffset);
+      }
+      return serializeFromData(newData);
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      return subSerialize(newData, 0);
+    }
+
+    @Override
+    public void subDataSet() {
+      builders = null;
+    }
+
+    @Override
+    public T subBuildTable(ReadableFontData data) {
+      return readTable(data, 0, true);
+    }
+
+    // ////////////////////////////////////
+    // implementations pushed to subclasses
+
+    protected abstract T readTable(ReadableFontData data, int base, boolean dataIsCanonical);
+
+    protected abstract VisibleSubTable.Builder<S> createSubTableBuilder();
+
+    protected abstract VisibleSubTable.Builder<S> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical);
+
+    protected abstract VisibleSubTable.Builder<S> createSubTableBuilder(S subTable);
+
+    // ////////////////////////////////////
+    // private methods
+
+    private void prepareToEdit() {
+      if (builders == null) {
+        initFromData(internalReadData(), base);
+        setModelChanged();
+      }
+    }
+
+    private void initFromData(ReadableFontData data, int base) {
+      NumRecordList recordList = new NumRecordList(data, 0, base + headerSize());
+      initFromData(recordList);
+    }
+
+    private void initFromData(NumRecordList recordList) {
+      ReadableFontData data = recordList.readData;
+      builders = new ArrayList<VisibleSubTable.Builder<S>>();
+      if (data == null) {
+        return;
+      }
+
+      if (recordList.count() == 0) {
+        return;
+      }
+
+      int subTableLimit = recordList.limit();
+      Iterator<NumRecord> recordIterator = recordList.iterator();
+      do {
+        NumRecord record = recordIterator.next();
+        int offset = record.value;
+        VisibleSubTable.Builder<S> builder = createSubTableBuilder(data, offset);
+        builders.add(builder);
+      } while (recordIterator.hasNext());
+    }
+
+    private void computeSizeFromBuilders() {
+      // This does not merge LangSysTables that reference the same
+      // features.
+
+      // If there is no data in the default LangSysTable or any
+      // of the other LangSysTables, the size is zero, and this table
+      // will not be written.
+
+      int len = 0;
+      int count = 0;
+      for (VisibleSubTable.Builder<S> builder : builders) {
+        int sublen = builder.subDataSizeToSerialize();
+        if (sublen > 0) {
+          ++count;
+          len += sublen;
+        }
+      }
+      serializedSubtablePartLength = len;
+      if (len > 0) {
+        serializedTablePartLength = NumRecordList.sizeOfListOfCount(count);
+      }
+      serializedLength = serializedTablePartLength + serializedSubtablePartLength;
+      serializedCount = count;
+    }
+
+    private void computeSizeFromData(ReadableFontData data) {
+      // This assumes canonical data.
+      int len = 0;
+      int count = 0;
+      if (data != null) {
+        len = data.length();
+        count = new NumRecordList(data).count();
+      }
+      serializedLength = len;
+      serializedCount = count;
+    }
+
+    private int serializeFromBuilders(WritableFontData newData, int subTableWriteOffset) {
+      // The canonical form of the data consists of the header,
+      // the index, then the
+      // scriptTables from the index in index order. All
+      // scriptTables are distinct; there's no sharing of tables.
+
+      // Find size for table
+      int tableSize = NumRecordList.sizeOfListOfCount(serializedCount);
+
+      // Fill header in table and serialize its builder.
+      int subTableFillPos = tableSize;
+      if (subTableWriteOffset > 0) {
+        subTableFillPos = subTableWriteOffset;
+      }
+
+      NumRecordList recordList = new NumRecordList(newData);
+      for (VisibleSubTable.Builder<S> builder : builders) {
+        if (builder.serializedLength > 0) {
+          NumRecord record = new NumRecord(subTableFillPos);
+          recordList.add(record);
+          subTableFillPos += builder.subSerialize(newData.slice(subTableFillPos));
+        }
+      }
+      recordList.writeTo(newData);
+      return subTableFillPos;
+    }
+
+    private int serializeFromData(WritableFontData newData) {
+      // The source data must be canonical.
+      ReadableFontData data = internalReadData().slice(base);
+      data.copyTo(newData);
+      return data.length();
+    }
+
+    private VisibleSubTable.Builder<S> createSubTableBuilder(ReadableFontData data, int offset) {
+      ReadableFontData newData = data.slice(offset);
+      return createSubTableBuilder(newData, dataIsCanonical);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/OneToManySubst.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/OneToManySubst.java
new file mode 100644
index 0000000..1886206
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/OneToManySubst.java
@@ -0,0 +1,94 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.opentype.CoverageTable;
+import com.google.typography.font.sfntly.table.opentype.SubstSubtable;
+import com.google.typography.font.sfntly.table.opentype.multiplesubst.GlyphIds;
+
+import java.util.Iterator;
+
+public class OneToManySubst extends SubstSubtable implements Iterable<NumRecordTable> {
+  private final GlyphIds array;
+
+  // //////////////
+  // Constructors
+
+  protected OneToManySubst(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    if (format != 1) {
+      throw new IllegalStateException("Subt format value is " + format + " (should be 1).");
+    }
+    array = new GlyphIds(data, headerSize(), dataIsCanonical);
+  }
+
+  // //////////////////////////////////
+  // Methods redirected to the array
+
+  public NumRecordList recordList() {
+    return array.recordList;
+  }
+
+  public NumRecordTable subTableAt(int index) {
+    return array.subTableAt(index);
+  }
+
+  @Override
+  public Iterator<NumRecordTable> iterator() {
+    return array.iterator();
+  }
+
+  // //////////////////////////////////
+  // Methods specific to this class
+
+  public CoverageTable coverage() {
+    return array.coverage;
+  }
+
+  public static class Builder extends SubstSubtable.Builder<SubstSubtable> {
+    private final GlyphIds.Builder arrayBuilder;
+
+    protected Builder() {
+      super();
+      arrayBuilder = new GlyphIds.Builder();
+    }
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+      arrayBuilder = new GlyphIds.Builder(data, dataIsCanonical);
+    }
+
+    protected Builder(SubstSubtable subTable) {
+      OneToManySubst multiSubst = (OneToManySubst) subTable;
+      arrayBuilder = new GlyphIds.Builder(multiSubst.array);
+    }
+
+    @Override
+    public int subDataSizeToSerialize() {
+      return arrayBuilder.subDataSizeToSerialize();
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      return arrayBuilder.subSerialize(newData);
+    }
+
+    // /////////////////////////////////
+    // must implement abstract methods
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    @Override
+    public void subDataSet() {
+      arrayBuilder.subDataSet();
+    }
+
+    @Override
+    public OneToManySubst subBuildTable(ReadableFontData data) {
+      return new OneToManySubst(data, 0, true);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/RangeRecord.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/RangeRecord.java
new file mode 100644
index 0000000..1a592c0
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/RangeRecord.java
@@ -0,0 +1,27 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+
+final class RangeRecord implements Record {
+  static final int RECORD_SIZE = 6;
+  private static final int START_OFFSET = 0;
+  private static final int END_OFFSET = 2;
+  private static final int PROPERTY_OFFSET = 4;
+  final int start;
+  final int end;
+  final int property;
+
+  RangeRecord(ReadableFontData data, int base) {
+    this.start = data.readUShort(base + START_OFFSET);
+    this.end = data.readUShort(base + END_OFFSET);
+    this.property = data.readUShort(base + PROPERTY_OFFSET);
+  }
+
+  @Override
+  public int writeTo(WritableFontData newData, int base) {
+    newData.writeUShort(base + START_OFFSET, start);
+    newData.writeUShort(base + END_OFFSET, end);
+    return RECORD_SIZE;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/RangeRecordList.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/RangeRecordList.java
new file mode 100644
index 0000000..e51be3e
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/RangeRecordList.java
@@ -0,0 +1,28 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+
+public final class RangeRecordList extends RecordList<RangeRecord> {
+  public RangeRecordList(WritableFontData data) {
+    super(data);
+  }
+
+  public RangeRecordList(ReadableFontData data) {
+    super(data);
+  }
+
+  public static int sizeOfListOfCount(int count) {
+    return RecordList.DATA_OFFSET + count * RangeRecord.RECORD_SIZE;
+  }
+
+  @Override
+  protected RangeRecord getRecordAt(ReadableFontData data, int offset) {
+    return new RangeRecord(data, offset);
+  }
+
+  @Override
+  protected int recordSize() {
+    return RangeRecord.RECORD_SIZE;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/RangeRecordTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/RangeRecordTable.java
new file mode 100644
index 0000000..19cec05
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/RangeRecordTable.java
@@ -0,0 +1,50 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+
+public class RangeRecordTable extends RecordsTable<RangeRecord> {
+  public RangeRecordTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  @Override
+  protected RecordList<RangeRecord> createRecordList(ReadableFontData data) {
+    return new RangeRecordList(data);
+  }
+
+  @Override
+  public int fieldCount() {
+    return 0;
+  }
+
+  public static class Builder extends RecordsTable.Builder<RangeRecordTable, RangeRecord> {
+    public Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected RangeRecordTable readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      if (base != 0) {
+        throw new UnsupportedOperationException();
+      }
+      return new RangeRecordTable(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected RecordList<RangeRecord> readRecordList(ReadableFontData data, int base) {
+      if (base != 0) {
+        throw new UnsupportedOperationException();
+      }
+      return new RangeRecordList(data);
+    }
+
+    @Override
+    protected int fieldCount() {
+      return 0;
+    }
+
+    @Override
+    protected void initFields() {
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/Record.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/Record.java
new file mode 100644
index 0000000..c100787
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/Record.java
@@ -0,0 +1,7 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.WritableFontData;
+
+interface Record {
+  int writeTo(WritableFontData newData, int base);
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/RecordList.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/RecordList.java
new file mode 100644
index 0000000..58b0a6d
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/RecordList.java
@@ -0,0 +1,160 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+public abstract class RecordList<T extends Record> implements Iterable<T> {
+  private static final int COUNT_OFFSET = 0;
+  static final int DATA_OFFSET = 2;
+  final int base;
+  private final int recordBase;
+
+  final ReadableFontData readData;
+  private final WritableFontData writeData;
+  private int count;
+  private List<T> recordsToWrite;
+
+  /*
+   *private RecordList(WritableFontData data) { this.readData = null;
+   * this.writeData = data; this.count = 0; this.base = 0; this.recordBase =
+   * RECORD_BASE_DEFAULT; if (writeData != null) {
+   * writeData.writeUShort(COUNT_OFFSET, 0); } }
+   */
+  protected RecordList(ReadableFontData data, int countDecrement, int countOffset,
+      int valuesOffset) {
+    this.readData = data;
+    this.writeData = null;
+    this.base = countOffset;
+    this.recordBase = valuesOffset; // base + RECORD_BASE_DEFAULT +
+                                    // recordBaseOffset;
+    if (readData != null) {
+      this.count = data.readUShort(countOffset + COUNT_OFFSET) - countDecrement;
+    }
+  }
+
+  protected RecordList(RecordList<T> other) {
+    this.readData = other.readData;
+    this.writeData = other.writeData;
+    this.base = other.base;
+    this.recordBase = other.recordBase;
+    this.count = other.count;
+    this.recordsToWrite = other.recordsToWrite;
+  }
+
+  protected RecordList(ReadableFontData data) {
+    this(data, 0);
+  }
+
+  protected RecordList(ReadableFontData data, int countDecrement) {
+    this(data, countDecrement, 0, DATA_OFFSET);
+  }
+
+  protected RecordList(ReadableFontData data, int countDecrement, int countOffset) {
+    this(data, countDecrement, countOffset, countOffset + DATA_OFFSET);
+  }
+
+  public int count() {
+    if (recordsToWrite != null) {
+      return recordsToWrite.size();
+    }
+    return count;
+  }
+
+  public int limit() {
+    return sizeOfList(count());
+  }
+
+  private int sizeOfList(int count) {
+    return baseAt(recordBase, count);
+  }
+
+  private int baseAt(int base, int index) {
+    return base + index * recordSize();
+  }
+
+  T get(int index) {
+    if (recordsToWrite != null) {
+      return recordsToWrite.get(index);
+    }
+    return getRecordAt(readData, sizeOfList(index));
+  }
+
+  public boolean contains(T record) {
+    if (recordsToWrite != null) {
+      return recordsToWrite.contains(record);
+    }
+
+    Iterator<T> iterator = iterator();
+    while (iterator.hasNext()) {
+      if (record.equals(iterator.next())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public Iterator<T> iterator() {
+    if (recordsToWrite != null) {
+      return recordsToWrite.iterator();
+    }
+
+    return new Iterator<T>() {
+      private int current = 0;
+
+      @Override
+      public boolean hasNext() {
+        return current < count;
+      }
+
+      @Override
+      public T next() {
+        if (!hasNext()) {
+          throw new NoSuchElementException();
+        }
+        return getRecordAt(readData, sizeOfList(current++));
+      }
+
+      @Override
+      public void remove() {
+        throw new UnsupportedOperationException();
+      }
+    };
+  }
+
+  public RecordList<T> add(T record) {
+    copyFromRead();
+    recordsToWrite.add(record);
+    return this;
+  }
+
+  public int writeTo(WritableFontData writeData) {
+    copyFromRead();
+
+    writeData.writeUShort(base + COUNT_OFFSET, count);
+    int nextWritePos = recordBase;
+    for (T record : recordsToWrite) {
+      nextWritePos += record.writeTo(writeData, nextWritePos);
+    }
+    return nextWritePos - recordBase + DATA_OFFSET; // bytes wrote
+  }
+
+  private void copyFromRead() {
+    if (recordsToWrite == null) {
+      recordsToWrite = new ArrayList<T>(count);
+      Iterator<T> iterator = iterator();
+      while (iterator.hasNext()) {
+        recordsToWrite.add(iterator.next());
+      }
+    }
+  }
+
+  protected abstract T getRecordAt(ReadableFontData data, int pos);
+
+  protected abstract int recordSize();
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/RecordsTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/RecordsTable.java
new file mode 100644
index 0000000..45f325a
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/RecordsTable.java
@@ -0,0 +1,164 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+
+import java.util.Iterator;
+
+public abstract class RecordsTable<R extends Record> extends HeaderTable implements Iterable<R> {
+  public final RecordList<R> recordList;
+
+  // ///////////////
+  // constructors
+
+  protected RecordsTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    recordList = createRecordList(data.slice(base + headerSize()));
+  }
+
+  protected RecordsTable(ReadableFontData data, boolean dataIsCanonical) {
+    this(data, 0, dataIsCanonical);
+  }
+
+  protected RecordsTable(RecordList<R> records) {
+    super(records.readData, records.base, false);
+    recordList = records;
+  }
+
+  @Override
+  public Iterator<R> iterator() {
+    return recordList.iterator();
+  }
+
+  // ////////////////////////////////////
+  // implementations pushed to subclasses
+
+  protected abstract RecordList<R> createRecordList(ReadableFontData data);
+
+  public abstract static class Builder<T extends HeaderTable, R extends Record>
+  extends HeaderTable.Builder<T> {
+
+    protected RecordList<R> records;
+    private int serializedLength;
+    private final int base;
+
+    protected Builder() {
+      super();
+      base = 0;
+    }
+
+    protected Builder(RecordsTable<R> table) {
+      this(table.readFontData(), table.base, table.dataIsCanonical);
+    }
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      this(data, 0, dataIsCanonical);
+    }
+
+    protected Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data);
+      this.base = base;
+      if (!dataIsCanonical) {
+        prepareToEdit();
+      }
+    }
+
+    protected Builder(RecordsTable.Builder<T, R> other) {
+      super();
+      base = other.base;
+      records = other.records;
+    }
+
+    // ////////////////
+    // private methods
+
+    public RecordList<R> records() {
+      return records;
+    }
+
+    public int count() {
+      initFromData(internalReadData(), base);
+      return records.count();
+    }
+
+    // ////////////////////////////////////
+    // overriden methods
+
+    @Override
+    public int subDataSizeToSerialize() {
+      if (records != null) {
+        serializedLength = records.limit();
+      } else {
+        computeSizeFromData(internalReadData().slice(base + headerSize()));
+      }
+      return serializedLength;
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      if (serializedLength == 0) {
+        return 0;
+      }
+
+      if (records == null) {
+        return serializeFromData(newData);
+      }
+
+      return records.writeTo(newData);
+    }
+
+    @Override
+    public T subBuildTable(ReadableFontData data) {
+      return readTable(data, 0, true);
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    @Override
+    public void subDataSet() {
+      records = null;
+    }
+
+    // ////////////////////////////////////
+    // implementations pushed to subclasses
+
+    protected abstract T readTable(ReadableFontData data, int base, boolean dataIsCanonical);
+
+    protected abstract RecordList<R> readRecordList(ReadableFontData data, int base);
+
+    // ////////////////////////////////////
+    // private methods
+
+    private void prepareToEdit() {
+      initFromData(internalReadData(), base + headerSize());
+      setModelChanged();
+    }
+
+    private void initFromData(ReadableFontData data, int base) {
+      if (records == null) {
+        records = readRecordList(data, base);
+      }
+    }
+
+    private void computeSizeFromData(ReadableFontData data) {
+      // This assumes canonical data.
+      int len = 0;
+      if (data != null) {
+        len = data.length();
+      }
+      serializedLength = len;
+    }
+
+    private int serializeFromData(WritableFontData newData) {
+      // The source data must be canonical.
+      ReadableFontData data = internalReadData().slice(base + headerSize());
+      data.copyTo(newData);
+      return data.length();
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/Rule.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/Rule.java
new file mode 100644
index 0000000..f231683
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/Rule.java
@@ -0,0 +1,518 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.Font;
+import com.google.typography.font.sfntly.Tag;
+import com.google.typography.font.sfntly.table.core.CMap;
+import com.google.typography.font.sfntly.table.core.CMapTable;
+import com.google.typography.font.sfntly.table.core.PostScriptTable;
+import com.google.typography.font.sfntly.table.opentype.FeatureListTable;
+import com.google.typography.font.sfntly.table.opentype.GSubTable;
+import com.google.typography.font.sfntly.table.opentype.LangSysTable;
+import com.google.typography.font.sfntly.table.opentype.LookupListTable;
+import com.google.typography.font.sfntly.table.opentype.ScriptListTable;
+import com.google.typography.font.sfntly.table.opentype.ScriptTable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class Rule {
+  private final RuleSegment backtrack;
+  private final RuleSegment input;
+  private final RuleSegment lookAhead;
+  final RuleSegment subst;
+  private final int hashCode;
+
+  Rule(RuleSegment backtrack, RuleSegment input, RuleSegment lookAhead, RuleSegment subst) {
+    this.backtrack = backtrack;
+    this.input = input;
+    this.lookAhead = lookAhead;
+    this.subst = subst;
+    this.hashCode = getHashCode();
+  }
+
+  // Closure related
+  public static GlyphGroup charGlyphClosure(Font font, String txt) {
+    CMapTable cmapTable = font.getTable(Tag.cmap);
+    GlyphGroup glyphGroup = glyphGroupForText(txt, cmapTable);
+
+    Set<Rule> featuredRules = featuredRules(font);
+    Map<Integer, Set<Rule>> glyphRuleMap = createGlyphRuleMap(featuredRules);
+    GlyphGroup ruleClosure = closure(glyphRuleMap, glyphGroup);
+    return ruleClosure;
+  }
+
+  public static GlyphGroup closure(Map<Integer, Set<Rule>> glyphRuleMap, GlyphGroup glyphs) {
+    int prevSize = 0;
+    while (glyphs.size() > prevSize) {
+      prevSize = glyphs.size();
+      for (Rule rule : rulesForGlyph(glyphRuleMap, glyphs)) {
+        rule.addMatchingTargetGlyphs(glyphs);
+      }
+    }
+    return glyphs;
+  }
+
+  private void addMatchingTargetGlyphs(GlyphGroup glyphs) {
+    for (RuleSegment seg : new RuleSegment[] { input, backtrack, lookAhead }) {
+      if (seg == null) {
+        continue;
+      }
+      for (GlyphGroup g : seg) {
+        if (!g.intersects(glyphs)) {
+          return;
+        }
+      }
+    }
+
+    for (GlyphGroup glyphGroup : subst) {
+      glyphs.addAll(glyphGroup);
+    }
+  }
+
+  public static Map<Integer, Set<Rule>> glyphRulesMap(Font font) {
+    Set<Rule> featuredRules = Rule.featuredRules(font);
+    if (featuredRules == null) {
+      return null;
+    }
+    return createGlyphRuleMap(featuredRules);
+  }
+
+  private static Map<Integer, Set<Rule>> createGlyphRuleMap(Set<Rule> lookupRules) {
+    Map<Integer, Set<Rule>> map = new HashMap<Integer, Set<Rule>>();
+
+    for (Rule rule : lookupRules) {
+      for (int glyph : rule.input.get(0)) {
+        if (!map.containsKey(glyph)) {
+          map.put(glyph, new HashSet<Rule>());
+        }
+        map.get(glyph).add(rule);
+      }
+    }
+    return map;
+  }
+
+  private static Set<Rule> rulesForGlyph(Map<Integer, Set<Rule>> glyphRuleMap, GlyphGroup glyphs) {
+    Set<Rule> set = new HashSet<Rule>();
+    for(int glyph : glyphs) {
+      if (glyphRuleMap.containsKey(glyph)) {
+        set.addAll(glyphRuleMap.get(glyph));
+      }
+    }
+    return set;
+  }
+
+  private static Set<Rule> featuredRules(
+      Set<Integer> lookupIds, Map<Integer, Set<Rule>> ruleMap) {
+    Set<Rule> rules = new LinkedHashSet<Rule>();
+    for (int lookupId : lookupIds) {
+      Set<Rule> ruleForLookup = ruleMap.get(lookupId);
+      if (ruleForLookup == null) {
+        System.err.printf("Lookup ID %d is used in features but not defined.\n", lookupId);
+        continue;
+      }
+      rules.addAll(ruleForLookup);
+    }
+    return rules;
+  }
+
+  private static Set<Integer> featuredLookups(Font font) {
+    GSubTable gsub = font.getTable(Tag.GSUB);
+    if (gsub == null) {
+      return null;
+    }
+
+    ScriptListTable scripts = gsub.scriptList();
+    FeatureListTable featureList = gsub.featureList();
+    LookupListTable lookupList = gsub.lookupList();
+    Map<Integer, Set<Rule>> ruleMap = RuleExtractor.extract(lookupList);
+
+    Set<Integer> features = new HashSet<Integer>();
+    Set<Integer> lookupIds = new HashSet<Integer>();
+
+    for (ScriptTable script : scripts.map().values()) {
+      for (LangSysTable langSys : script.map().values()) {
+        // We are assuming if required feature exists, it will be in the list
+        // of features as well.
+        for (NumRecord feature : langSys) {
+          if (!features.contains(feature.value)) {
+            features.add(feature.value);
+            for (NumRecord lookup : featureList.subTableAt(feature.value)) {
+              lookupIds.add(lookup.value);
+            }
+          }
+        }
+      }
+    }
+    return lookupIds;
+  }
+
+  private static Set<Rule> featuredRules(Font font) {
+    GSubTable gsub = font.getTable(Tag.GSUB);
+    if (gsub == null) {
+      return null;
+    }
+
+    LookupListTable lookupList = gsub.lookupList();
+    Map<Integer, Set<Rule>> ruleMap = RuleExtractor.extract(lookupList);
+    Set<Integer> lookupIds = featuredLookups(font);
+    Set<Rule> featuredRules = Rule.featuredRules(lookupIds, ruleMap);
+    return featuredRules;
+  }
+
+  // Utility method for glyphs for text
+
+  public static GlyphGroup glyphGroupForText(String str, CMapTable cmapTable) {
+    GlyphGroup glyphGroup = new GlyphGroup();
+    Set<Integer> codes = codepointsFromStr(str);
+    for (int code : codes) {
+      for (CMap cmap : cmapTable) {
+        if (cmap.platformId() == 3 && cmap.encodingId() == 1 || // Unicode BMP
+            cmap.platformId() == 3 && cmap.encodingId() == 10 || // UCS2
+            cmap.platformId() == 0 && cmap.encodingId() == 5) { // Variation
+          int glyph = cmap.glyphId(code);
+          if (glyph != CMapTable.NOTDEF) {
+            glyphGroup.add(glyph);
+          }
+          // System.out.println("code: " + code + " glyph: " + glyph + " platform: " + cmap.platformId() + " encodingId: " + cmap.encodingId() + " format: " + cmap.format());
+
+        }
+      }
+    }
+    return glyphGroup;
+  }
+
+  // Rule operation
+
+  private void applyRuleOnRuleWithSubst(Rule targetRule, int at, LinkedList<Rule> accumulateTo) {
+    RuleSegment matchSegment = targetRule.match(this, at);
+    if (matchSegment == null) {
+      return;
+    }
+
+    if (at < 0) {
+      throw new IllegalStateException();
+    }
+
+    int backtrackSize = targetRule.backtrack != null ? targetRule.backtrack.size() : 0;
+    RuleSegment newBacktrack = new RuleSegment();
+    newBacktrack.addAll(matchSegment.subList(0, backtrackSize));
+
+    if (at <= targetRule.subst.size()) {
+      RuleSegment newInput = new RuleSegment();
+      newInput.addAll(targetRule.input);
+      newInput.addAll(matchSegment.subList(backtrackSize + targetRule.subst.size(), backtrackSize + at + input.size()));
+
+      RuleSegment newLookAhead = new RuleSegment();
+      newLookAhead.addAll(matchSegment.subList(backtrackSize + at + input.size(), matchSegment.size()));
+
+      RuleSegment newSubst = new RuleSegment();
+      newSubst.addAll(targetRule.subst.subList(0, at));
+      newSubst.addAll(subst);
+      if (at + input.size() < targetRule.subst.size()) {
+        newSubst.addAll(targetRule.subst.subList(at + input.size(), targetRule.subst.size()));
+      }
+
+      Rule newTargetRule = new Rule(newBacktrack, newInput, newLookAhead, newSubst);
+      accumulateTo.add(newTargetRule);
+      return;
+    }
+
+    if (at >= targetRule.subst.size()) {
+      List<GlyphGroup> skippedLookAheadPart = matchSegment.subList(backtrackSize + targetRule.subst.size(), at);
+      Set<RuleSegment> intermediateSegments = permuteToSegments(skippedLookAheadPart);
+
+      RuleSegment newLookAhead = new RuleSegment();
+      List<GlyphGroup> remainingLookAhead = matchSegment.subList(backtrackSize + at + input.size(), matchSegment.size());
+      newLookAhead.addAll(remainingLookAhead);
+
+      for (RuleSegment interRuleSegment : intermediateSegments) {
+
+        RuleSegment newInput = new RuleSegment();
+        newInput.addAll(targetRule.input);
+        newInput.addAll(interRuleSegment);
+        newInput.addAll(input);
+
+        RuleSegment newSubst = new RuleSegment();
+        newSubst.addAll(targetRule.subst);
+        newInput.addAll(interRuleSegment);
+        newSubst.addAll(subst);
+
+        Rule newTargetRule = new Rule(newBacktrack, newInput, newLookAhead, newSubst);
+        accumulateTo.add(newTargetRule);
+      }
+    }
+  }
+
+  private static Set<RuleSegment> permuteToSegments(List<GlyphGroup> glyphGroups) {
+    Set<RuleSegment> result = new LinkedHashSet<RuleSegment>();
+    result.add(new RuleSegment());
+
+    for (GlyphGroup glyphGroup : glyphGroups) {
+      Set<RuleSegment> newResult = new LinkedHashSet<RuleSegment>();
+      for (Integer glyphId : glyphGroup) {
+        for (RuleSegment segment : result) {
+          RuleSegment newSegment = new RuleSegment();
+          newSegment.addAll(segment);
+          newSegment.add(new GlyphGroup(glyphId));
+          newResult.add(newSegment);
+        }
+      }
+      result = newResult;
+    }
+    return result;
+  }
+
+  private static Rule applyRuleOnRuleWithoutSubst(Rule ruleToApply, Rule targetRule, int at) {
+
+    RuleSegment matchSegment = targetRule.match(ruleToApply, at);
+    if (matchSegment == null) {
+      return null;
+    }
+
+    int backtrackSize = targetRule.backtrack != null ? targetRule.backtrack.size() : 0;
+
+    RuleSegment newBacktrack =  new RuleSegment();
+    newBacktrack.addAll(matchSegment.subList(0, backtrackSize + at));
+
+    RuleSegment newLookAhead = new RuleSegment();
+    newLookAhead.addAll(matchSegment.subList(backtrackSize + at + ruleToApply.input.size(), matchSegment.size()));
+
+    return new Rule(newBacktrack, ruleToApply.input, newLookAhead, ruleToApply.subst);
+  }
+
+  private static void applyRulesOnRuleWithSubst(Set<Rule> rulesToApply, Rule targetRule, int at,
+      LinkedList<Rule> accumulateTo) {
+    for (Rule ruleToApply : rulesToApply) {
+      ruleToApply.applyRuleOnRuleWithSubst(targetRule, at, accumulateTo);
+    }
+  }
+
+  private static void applyRulesOnRuleWithoutSubst(Set<Rule> rulesToApply, Rule targetRule, int at,
+      LinkedList<Rule> accumulateTo) {
+    for (Rule ruleToApply : rulesToApply) {
+      Rule newRule = applyRuleOnRuleWithoutSubst(ruleToApply, targetRule, at);
+      if (newRule != null) {
+        accumulateTo.add(newRule);
+      }
+    }
+  }
+
+  static LinkedList<Rule> applyRulesOnRules(Set<Rule> rulesToApply, List<Rule> targetRules,
+      int at) {
+    LinkedList<Rule> result = new LinkedList<Rule>();
+    for (Rule targetRule : targetRules) {
+      if (targetRule.subst != null) {
+        applyRulesOnRuleWithSubst(rulesToApply, targetRule, at, result);
+      } else {
+        applyRulesOnRuleWithoutSubst(rulesToApply, targetRule, at, result);
+      }
+    }
+    return result;
+  }
+
+  private RuleSegment match(Rule other, int at) {
+    if (at < 0) {
+      throw new IllegalStateException();
+    }
+
+    RuleSegment thisAllSegments = new RuleSegment();
+    if (backtrack != null) {
+      thisAllSegments.addAll(backtrack);
+    }
+    if (subst != null) {
+      thisAllSegments.addAll(subst);
+    } else {
+      thisAllSegments.addAll(input);
+    }
+    if (lookAhead != null) {
+      thisAllSegments.addAll(lookAhead);
+    }
+
+    RuleSegment otherAllSegments = new RuleSegment();
+    if (other.backtrack != null) {
+      otherAllSegments.addAll(other.backtrack);
+    }
+    otherAllSegments.addAll(other.input);
+    if (other.lookAhead != null) {
+      otherAllSegments.addAll(other.lookAhead);
+    }
+
+    int backtrackSize = backtrack != null ? backtrack.size() : 0;
+    int otherBacktrackSize = other.backtrack != null ? other.backtrack.size() : 0;
+    int initialPos = backtrackSize + at - otherBacktrackSize;
+
+    if (initialPos < 0) {
+      return null;
+    }
+
+    if (thisAllSegments.size() - initialPos < otherAllSegments.size()) {
+      return null;
+    }
+
+    for(int i = 0; i < otherAllSegments.size(); i++) {
+      GlyphGroup thisGlyphs = thisAllSegments.get(i + initialPos);
+      GlyphGroup otherGlyphs = otherAllSegments.get(i);
+
+      GlyphGroup intersection = thisGlyphs.intersection(otherGlyphs);
+      if (intersection.isEmpty()) {
+        return null;
+      }
+      thisAllSegments.set(i+initialPos, intersection);
+    }
+
+    return thisAllSegments;
+  }
+
+  private static Rule prependToInput(int prefix, Rule other) {
+    RuleSegment input = new RuleSegment(prefix);
+    input.addAll(other.input);
+
+    return new Rule(other.backtrack, input, other.lookAhead, other.subst);
+  }
+
+  static List<Rule> prependToInput(int prefix, List<Rule> rules) {
+    List<Rule> result = new ArrayList<Rule>();
+    for (Rule rule : rules) {
+      result.add(prependToInput(prefix, rule));
+    }
+    return result;
+  }
+
+  static Set<Rule> deltaRules(List<Integer> glyphIds, int delta) {
+    Set<Rule> result = new LinkedHashSet<Rule>();
+    for (int glyphId : glyphIds) {
+      RuleSegment input = new RuleSegment(glyphId);
+      RuleSegment subst = new RuleSegment(glyphId + delta);
+      result.add(new Rule(null, input, null, subst));
+    }
+    return result;
+  }
+
+  static Set<Rule> oneToOneRules(RuleSegment backtrack, List<Integer> inputs,
+      RuleSegment lookAhead, List<Integer> substs) {
+    if (inputs.size() != substs.size()) {
+      throw new IllegalArgumentException("input - subst should have same count");
+    }
+
+    Set<Rule> result = new LinkedHashSet<Rule>();
+    for (int i = 0; i < inputs.size(); i++) {
+      RuleSegment input = new RuleSegment(inputs.get(i));
+      RuleSegment subst = new RuleSegment(substs.get(i));
+      result.add(new Rule(backtrack, input, lookAhead, subst));
+    }
+    return result;
+  }
+
+  static Set<Rule> oneToOneRules(List<Integer> inputs, List<Integer> substs) {
+    return oneToOneRules(null, inputs, null, substs);
+  }
+
+  // Dump routines
+  private static Set<Integer> codepointsFromStr(String s) {
+    Set<Integer> list = new HashSet<Integer>();
+    for (int cp, i = 0; i < s.length(); i += Character.charCount(cp)) {
+      cp = s.codePointAt(i);
+      list.add(cp);
+    }
+    return list;
+  }
+
+  private static void dumpRuleMap(Map<Integer, Set<Rule>> rulesList, PostScriptTable post) {
+    for (int index : rulesList.keySet()) {
+      Set<Rule> rules = rulesList.get(index);
+      System.out.println(
+          "------------------------------ " + index + " --------------------------------");
+      for (Rule rule : rules) {
+        System.out.println(rule.toString(post));
+      }
+    }
+  }
+
+  public static void dumpLookups(Font font) {
+    GSubTable gsub = font.getTable(Tag.GSUB);
+    Map<Integer, Set<Rule>> ruleMap = RuleExtractor.extract(gsub.lookupList());
+    PostScriptTable post = font.getTable(Tag.post);
+    dumpRuleMap(ruleMap, post);
+    System.out.println("\nFeatured Lookup IDs: " + Rule.featuredLookups(font));
+  }
+
+  private String toString(PostScriptTable post) {
+    StringBuilder sb = new StringBuilder();
+    if (backtrack != null && backtrack.size() > 0) {
+      sb.append(backtrack.toString(post));
+      sb.append("} ");
+    }
+    sb.append(input.toString(post));
+    if (lookAhead != null && lookAhead.size() > 0) {
+      sb.append("{ ");
+      sb.append(lookAhead.toString(post));
+    }
+    sb.append("=> ");
+    sb.append(subst.toString(post));
+    return sb.toString();
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    if (backtrack != null && backtrack.size() > 0) {
+      sb.append(backtrack.toString());
+      sb.append("} ");
+    }
+    sb.append(input.toString());
+    if (lookAhead != null && lookAhead.size() > 0) {
+      sb.append("{ ");
+      sb.append(lookAhead.toString());
+    }
+    sb.append("=> ");
+    sb.append(subst.toString());
+    return sb.toString();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) {
+      return true;
+    }
+    if (!(o instanceof Rule)) {
+      return false;
+    }
+    Rule other = (Rule) o;
+    if (hashCode != other.hashCode) {
+      return false;
+    }
+    RuleSegment[] these = new RuleSegment[] {input, subst, backtrack, lookAhead};
+    RuleSegment[] others = new RuleSegment[] {other.input, other.subst, other.backtrack, other.lookAhead};
+    for (int i = 0; i < these.length; i++) {
+      RuleSegment thisSeg = these[i];
+      RuleSegment otherSeg = others[i];
+      if (thisSeg != null) {
+        if (!thisSeg.equals(otherSeg)) {
+          return false;
+        }
+      } else if (otherSeg != null){
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    return hashCode;
+  }
+
+  private int getHashCode() {
+    int hashCode = 1;
+    for (RuleSegment e : new RuleSegment[] {input, subst, backtrack, lookAhead}) {
+      hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
+    }
+    return hashCode;
+  }
+}
\ No newline at end of file
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/RuleExtractor.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/RuleExtractor.java
new file mode 100644
index 0000000..0a181e2
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/RuleExtractor.java
@@ -0,0 +1,580 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.table.opentype.AlternateSubst;
+import com.google.typography.font.sfntly.table.opentype.ChainContextSubst;
+import com.google.typography.font.sfntly.table.opentype.ClassDefTable;
+import com.google.typography.font.sfntly.table.opentype.ContextSubst;
+import com.google.typography.font.sfntly.table.opentype.CoverageTable;
+import com.google.typography.font.sfntly.table.opentype.ExtensionSubst;
+import com.google.typography.font.sfntly.table.opentype.LigatureSubst;
+import com.google.typography.font.sfntly.table.opentype.LookupListTable;
+import com.google.typography.font.sfntly.table.opentype.LookupTable;
+import com.google.typography.font.sfntly.table.opentype.MultipleSubst;
+import com.google.typography.font.sfntly.table.opentype.ReverseChainSingleSubst;
+import com.google.typography.font.sfntly.table.opentype.SingleSubst;
+import com.google.typography.font.sfntly.table.opentype.SubstSubtable;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubClassRule;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubClassSet;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubClassSetArray;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubRule;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubRuleSet;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.ChainSubRuleSetArray;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.CoverageArray;
+import com.google.typography.font.sfntly.table.opentype.chaincontextsubst.InnerArraysFmt3;
+import com.google.typography.font.sfntly.table.opentype.classdef.InnerArrayFmt1;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubClassRule;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubClassSet;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubClassSetArray;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubRule;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubRuleSet;
+import com.google.typography.font.sfntly.table.opentype.contextsubst.SubRuleSetArray;
+import com.google.typography.font.sfntly.table.opentype.ligaturesubst.Ligature;
+import com.google.typography.font.sfntly.table.opentype.ligaturesubst.LigatureSet;
+import com.google.typography.font.sfntly.table.opentype.singlesubst.HeaderFmt1;
+import com.google.typography.font.sfntly.table.opentype.singlesubst.InnerArrayFmt2;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+class RuleExtractor {
+  private static Set<Rule> extract(LigatureSubst table) {
+    Set<Rule> allRules = new LinkedHashSet<Rule>();
+    List<Integer> prefixChars = extract(table.coverage());
+
+    for (int i = 0; i < table.subTableCount(); i++) {
+      List<Rule> subRules = extract(table.subTableAt(i));
+      subRules = Rule.prependToInput(prefixChars.get(i), subRules);
+      allRules.addAll(subRules);
+    }
+    return allRules;
+  }
+
+  private static GlyphList extract(CoverageTable table) {
+    switch (table.format) {
+    case 1:
+      return extract(table.fmt1Table());
+    case 2:
+      RangeRecordTable array = table.fmt2Table();
+      Map<Integer, GlyphGroup> map = extract(array);
+      Collection<GlyphGroup> groups = map.values();
+      GlyphList result = new GlyphList();
+      for (GlyphGroup glyphIds : groups) {
+        glyphIds.copyTo(result);
+      }
+      return result;
+    default:
+      throw new IllegalArgumentException("unimplemented format " + table.format);
+    }
+  }
+
+  private static GlyphList extract(RecordsTable<NumRecord> table) {
+    GlyphList result = new GlyphList();
+    for (NumRecord record : table.recordList) {
+      result.add(record.value);
+    }
+    return result;
+  }
+
+  private static Map<Integer, GlyphGroup> extract(RangeRecordTable table) {
+    // Order is important.
+    Map<Integer, GlyphGroup> result = new LinkedHashMap<Integer, GlyphGroup>();
+    for (RangeRecord record : table.recordList) {
+      if (!result.containsKey(record.property)) {
+        result.put(record.property, new GlyphGroup());
+      }
+      GlyphGroup existingGlyphs = result.get(record.property);
+      existingGlyphs.addAll(extract(record));
+    }
+    return result;
+  }
+
+  private static GlyphGroup extract(RangeRecord record) {
+    int len = record.end - record.start + 1;
+    GlyphGroup result = new GlyphGroup();
+    for (int i = record.start; i <= record.end; i++) {
+      result.add(i);
+    }
+    return result;
+  }
+
+  private static List<Rule> extract(LigatureSet table) {
+    List<Rule> allRules = new ArrayList<Rule>();
+
+    for (int i = 0; i < table.subTableCount(); i++) {
+      Rule subRule = extract(table.subTableAt(i));
+      allRules.add(subRule);
+    }
+    return allRules;
+  }
+
+  private static Rule extract(Ligature table) {
+
+    int glyphId = table.getField(Ligature.LIG_GLYPH_INDEX);
+    RuleSegment subst = new RuleSegment(glyphId);
+    RuleSegment input = new RuleSegment();
+    for (NumRecord record : table.recordList) {
+      input.add(record.value);
+    }
+    return new Rule(null, input, null, subst);
+  }
+
+  private static Set<Rule> extract(SingleSubst table) {
+    switch (table.format) {
+    case 1:
+      return extract(table.fmt1Table());
+    case 2:
+      return extract(table.fmt2Table());
+    default:
+      throw new IllegalArgumentException("unimplemented format " + table.format);
+    }
+  }
+
+  private static Set<Rule> extract(HeaderFmt1 fmt1Table) {
+    List<Integer> coverage = extract(fmt1Table.coverage);
+    int delta = fmt1Table.getDelta();
+    return Rule.deltaRules(coverage, delta);
+  }
+
+  private static Set<Rule> extract(InnerArrayFmt2 fmt2Table) {
+    List<Integer> coverage = extract(fmt2Table.coverage);
+    List<Integer> substs = extract((RecordsTable<NumRecord>) fmt2Table);
+    return Rule.oneToOneRules(coverage, substs);
+  }
+
+  private static Set<Rule> extract(MultipleSubst table) {
+    Set<Rule> result = new LinkedHashSet<Rule>();
+
+    GlyphList coverage = extract(table.coverage());
+    int i = 0;
+    for (NumRecordTable glyphIds : table) {
+      RuleSegment input = new RuleSegment(coverage.get(i));
+
+      GlyphList glyphList = extract(glyphIds);
+      RuleSegment subst = new RuleSegment(glyphList);
+
+      Rule rule = new Rule(null, input, null, subst);
+      result.add(rule);
+      i++;
+    }
+    return result;
+  }
+
+  private static Set<Rule> extract(AlternateSubst table) {
+    Set<Rule> result = new LinkedHashSet<Rule>();
+
+    GlyphList coverage = extract(table.coverage());
+    int i = 0;
+    for (NumRecordTable glyphIds : table) {
+      RuleSegment input = new RuleSegment(coverage.get(i));
+
+      GlyphList glyphList = extract(glyphIds);
+      GlyphGroup glyphGroup = new GlyphGroup(glyphList);
+      RuleSegment subst = new RuleSegment(glyphGroup);
+
+      Rule rule = new Rule(null, input, null, subst);
+      result.add(rule);
+      i++;
+    }
+    return result;
+  }
+
+  private static Set<Rule> extract(ContextSubst table, LookupListTable lookupListTable,
+      Map<Integer, Set<Rule>> allLookupRules) {
+    switch (table.format) {
+    case 1:
+      return extract(table.fmt1Table(), lookupListTable, allLookupRules);
+    case 2:
+      return extract(table.fmt2Table(), lookupListTable, allLookupRules);
+    default:
+      throw new IllegalArgumentException("unimplemented format " + table.format);
+    }
+  }
+
+  private static Set<Rule> extract(SubRuleSetArray table, LookupListTable lookupListTable,
+      Map<Integer, Set<Rule>> allLookupRules) {
+    GlyphList coverage = extract(table.coverage);
+
+    Set<Rule> result = new LinkedHashSet<Rule>();
+    int i = 0;
+    for (SubRuleSet subRuleSet : table) {
+      Set<Rule> subRules = extract(coverage.get(i), subRuleSet, lookupListTable, allLookupRules);
+      result.addAll(subRules);
+      i++;
+    }
+    return result;
+  }
+
+  private static Set<Rule> extract(
+      Integer firstGlyph, SubRuleSet table, LookupListTable lookupListTable, Map<Integer, Set<Rule>> allLookupRules) {
+    Set<Rule> result = new LinkedHashSet<Rule>();
+    for (SubRule subRule : table) {
+      Set<Rule> subrules = extract(firstGlyph, subRule, lookupListTable, allLookupRules);
+      if (subrules == null) {
+        return null;
+      }
+      result.addAll(subrules);
+    }
+    return result;
+  }
+
+  private static Set<Rule> extract(
+      Integer firstGlyph, SubRule table, LookupListTable lookupListTable, Map<Integer, Set<Rule>> allLookupRules) {
+    RuleSegment inputRow = new RuleSegment(firstGlyph);
+    for (NumRecord record : table.inputGlyphs) {
+      inputRow.add(record.value);
+    }
+
+    Rule ruleSansSubst = new Rule(null, inputRow, null, null);
+    return applyChainingLookup(ruleSansSubst, table.lookupRecords, lookupListTable, allLookupRules);
+  }
+
+  private static Set<Rule> extract(
+      SubClassSetArray table, LookupListTable lookupListTable, Map<Integer, Set<Rule>> allLookupRules) {
+    GlyphList coverage = extract(table.coverage);
+    Map<Integer, GlyphGroup> classDef = extract(table.classDef);
+
+    Set<Rule> result = new LinkedHashSet<Rule>();
+    int i = 0;
+    for (SubClassSet subClassRuleSet : table) {
+      if (subClassRuleSet != null) {
+        Set<Rule> subRules = extract(subClassRuleSet, i, classDef, lookupListTable, allLookupRules);
+        result.addAll(subRules);
+      }
+      i++;
+    }
+    return result;
+  }
+
+  private static Set<Rule> extract(SubClassSet table, int firstInputClass,
+      Map<Integer, GlyphGroup> inputClassDef, LookupListTable lookupListTable, Map<Integer, Set<Rule>> allLookupRules) {
+    Set<Rule> result = new LinkedHashSet<Rule>();
+    for (SubClassRule subRule : table) {
+      Set<Rule> subRules = extract(subRule, firstInputClass, inputClassDef, lookupListTable, allLookupRules);
+      result.addAll(subRules);
+    }
+    return result;
+  }
+
+  private static Set<Rule> extract(SubClassRule table, int firstInputClass,
+      Map<Integer, GlyphGroup> inputClassDef, LookupListTable lookupListTable, Map<Integer, Set<Rule>> allLookupRules) {
+    RuleSegment input = extract(firstInputClass, table.inputClasses(), inputClassDef);
+
+    Rule ruleSansSubst = new Rule(null, input, null, null);
+    return applyChainingLookup(ruleSansSubst, table.lookupRecords, lookupListTable, allLookupRules);
+  }
+
+  private static Set<Rule> extract(
+      ChainContextSubst table, LookupListTable lookupListTable, Map<Integer, Set<Rule>> allLookupRules) {
+    switch (table.format) {
+    case 1:
+      return extract(table.fmt1Table(), lookupListTable, allLookupRules);
+    case 2:
+      return extract(table.fmt2Table(), lookupListTable, allLookupRules);
+    case 3:
+      return extract(table.fmt3Table(), lookupListTable, allLookupRules);
+    default:
+      throw new IllegalArgumentException("unimplemented format " + table.format);
+    }
+  }
+
+  private static Set<Rule> extract(
+      ChainSubRuleSetArray table, LookupListTable lookupListTable, Map<Integer, Set<Rule>> allLookupRules) {
+    GlyphList coverage = extract(table.coverage);
+
+    Set<Rule> result = new LinkedHashSet<Rule>();
+    int i = 0;
+    for (ChainSubRuleSet subRuleSet : table) {
+      Set<Rule> subRules = extract(coverage.get(i), subRuleSet, lookupListTable, allLookupRules);
+      result.addAll(subRules);
+      i++;
+    }
+    return result;
+  }
+
+  private static Set<Rule> extract(
+      Integer firstGlyph, ChainSubRuleSet table, LookupListTable lookupListTable, Map<Integer, Set<Rule>> allLookupRules) {
+    Set<Rule> result = new LinkedHashSet<Rule>();
+    for (ChainSubRule subRule : table) {
+      result.addAll(extract(firstGlyph, subRule, lookupListTable, allLookupRules));
+    }
+    return result;
+  }
+
+  private static Set<Rule> extract(
+      Integer firstGlyph, ChainSubRule table, LookupListTable lookupListTable, Map<Integer, Set<Rule>> allLookupRules) {
+    RuleSegment inputRow = new RuleSegment(firstGlyph);
+    for (NumRecord record : table.inputClasses) {
+      inputRow.add(record.value);
+    }
+
+    RuleSegment backtrack = ruleSegmentFromGlyphs(table.backtrackGlyphs);
+    RuleSegment lookAhead = ruleSegmentFromGlyphs(table.lookAheadGlyphs);
+
+    Rule ruleSansSubst = new Rule(backtrack, inputRow, lookAhead, null);
+    return applyChainingLookup(ruleSansSubst, table.lookupRecords, lookupListTable, allLookupRules);
+  }
+
+  private static RuleSegment ruleSegmentFromGlyphs(NumRecordList records) {
+    RuleSegment segment = new RuleSegment();
+    for (NumRecord record : records) {
+      segment.add(new GlyphGroup(record.value));
+    }
+    return segment;
+  }
+
+  private static Set<Rule> extract(
+      ChainSubClassSetArray table, LookupListTable lookupListTable, Map<Integer, Set<Rule>> allLookupRules) {
+    Map<Integer, GlyphGroup> backtrackClassDef = extract(table.backtrackClassDef);
+    Map<Integer, GlyphGroup> inputClassDef = extract(table.inputClassDef);
+    Map<Integer, GlyphGroup> lookAheadClassDef = extract(table.lookAheadClassDef);
+
+    Set<Rule> result = new LinkedHashSet<Rule>();
+    int i = 0;
+    for (ChainSubClassSet chainSubRuleSet : table) {
+      if (chainSubRuleSet != null) {
+        result.addAll(extract(chainSubRuleSet,
+            backtrackClassDef,
+            i,
+            inputClassDef,
+            lookAheadClassDef,
+            lookupListTable,
+            allLookupRules));
+      }
+      i++;
+    }
+    return result;
+  }
+
+  private static Map<Integer, GlyphGroup> extract(ClassDefTable table) {
+    switch (table.format) {
+    case 1:
+      return extract(table.fmt1Table());
+    case 2:
+      return extract(table.fmt2Table());
+    default:
+      throw new IllegalArgumentException("unimplemented format " + table.format);
+    }
+  }
+
+  private static Map<Integer, GlyphGroup> extract(InnerArrayFmt1 table) {
+    Map<Integer, GlyphGroup> result = new HashMap<Integer, GlyphGroup>();
+    int glyphId = table.getField(InnerArrayFmt1.START_GLYPH_INDEX);
+    for (NumRecord record : table) {
+      int classId = record.value;
+      if (!result.containsKey(classId)) {
+        result.put(classId, new GlyphGroup());
+      }
+
+      result.get(classId).add(glyphId);
+      glyphId++;
+    }
+    return result;
+  }
+
+  private static List<Rule> extract(ChainSubClassSet table,
+      Map<Integer, GlyphGroup> backtrackClassDef,
+      int firstInputClass,
+      Map<Integer, GlyphGroup> inputClassDef,
+      Map<Integer, GlyphGroup> lookAheadClassDef,
+      LookupListTable lookupListTable,
+      Map<Integer, Set<Rule>> allLookupRules) {
+    List<Rule> result = new ArrayList<Rule>();
+    for (ChainSubClassRule chainSubRule : table) {
+      result.addAll(extract(chainSubRule,
+          backtrackClassDef,
+          firstInputClass,
+          inputClassDef,
+          lookAheadClassDef,
+          lookupListTable,
+          allLookupRules));
+    }
+    return result;
+  }
+
+  private static Set<Rule> extract(ChainSubClassRule table,
+      Map<Integer, GlyphGroup> backtrackClassDef,
+      int firstInputClass,
+      Map<Integer, GlyphGroup> inputClassDef,
+      Map<Integer, GlyphGroup> lookAheadClassDef,
+      LookupListTable lookupListTable,
+      Map<Integer, Set<Rule>> allLookupRules) {
+    RuleSegment backtrack = ruleSegmentFromClasses(table.backtrackGlyphs, backtrackClassDef);
+    RuleSegment inputRow = extract(firstInputClass, table.inputClasses, inputClassDef);
+    RuleSegment lookAhead = ruleSegmentFromClasses(table.lookAheadGlyphs, lookAheadClassDef);
+
+    Rule ruleSansSubst = new Rule(backtrack, inputRow, lookAhead, null);
+    return applyChainingLookup(ruleSansSubst, table.lookupRecords, lookupListTable, allLookupRules);
+  }
+
+  private static RuleSegment extract(
+      int firstInputClass, NumRecordList inputClasses, Map<Integer, GlyphGroup> classDef) {
+    RuleSegment input = new RuleSegment(classDef.get(firstInputClass));
+    for (NumRecord inputClass : inputClasses) {
+      int classId = inputClass.value;
+      GlyphGroup glyphs = classDef.get(classId);
+      if (glyphs == null && classId == 0) {
+        // Any glyph not mentioned in the classes
+        glyphs = GlyphGroup.inverseGlyphGroup(classDef.values());
+      }
+      input.add(glyphs);
+    }
+    return input;
+  }
+
+  private static RuleSegment ruleSegmentFromClasses(
+      NumRecordList classes, Map<Integer, GlyphGroup> classDef) {
+    RuleSegment segment = new RuleSegment();
+    for (NumRecord classRecord : classes) {
+      int classId = classRecord.value;
+      GlyphGroup glyphs = classDef.get(classId);
+      if (glyphs == null && classId == 0) {
+        // Any glyph not mentioned in the classes
+        glyphs = GlyphGroup.inverseGlyphGroup(classDef.values());
+      }
+      segment.add(glyphs);
+    }
+    return segment;
+  }
+
+  private static Set<Rule> extract(InnerArraysFmt3 table, LookupListTable lookupListTable,
+      Map<Integer, Set<Rule>> allLookupRules) {
+    RuleSegment backtrackContext = extract(table.backtrackGlyphs);
+    RuleSegment input = extract(table.inputGlyphs);
+    RuleSegment lookAheadContext = extract(table.lookAheadGlyphs);
+
+    Rule ruleSansSubst = new Rule(backtrackContext, input, lookAheadContext, null);
+    Set<Rule> result = applyChainingLookup(
+        ruleSansSubst, table.lookupRecords, lookupListTable, allLookupRules);
+    return result;
+  }
+
+  private static Set<Rule> extract(ReverseChainSingleSubst table) {
+    List<Integer> coverage = extract(table.coverage);
+
+    RuleSegment backtrackContext = new RuleSegment();
+    backtrackContext.addAll(extract(table.backtrackGlyphs));
+
+    RuleSegment lookAheadContext = new RuleSegment();
+    lookAheadContext.addAll(extract(table.lookAheadGlyphs));
+
+    List<Integer> substs = extract(table.substitutes);
+
+    return Rule.oneToOneRules(backtrackContext, coverage, lookAheadContext, substs);
+  }
+
+  private static Set<Rule> applyChainingLookup(Rule ruleSansSubst,
+      SubstLookupRecordList lookups, LookupListTable lookupListTable, Map<Integer, Set<Rule>> allLookupRules) {
+
+    LinkedList<Rule> targetRules = new LinkedList<Rule>();
+    targetRules.add(ruleSansSubst);
+    for (SubstLookupRecord lookup : lookups) {
+      int at = lookup.sequenceIndex;
+      int lookupIndex = lookup.lookupListIndex;
+      Set<Rule> rulesToApply = extract(lookupListTable, allLookupRules, lookupIndex);
+      if (rulesToApply == null) {
+        throw new IllegalArgumentException(
+            "Out of bound lookup index for chaining lookup: " + lookupIndex);
+      }
+      LinkedList<Rule> newRules = Rule.applyRulesOnRules(rulesToApply, targetRules, at);
+
+      LinkedList<Rule> result = new LinkedList<Rule>();
+      result.addAll(newRules);
+      result.addAll(targetRules);
+      targetRules = result;
+    }
+
+    Set<Rule> result = new LinkedHashSet<Rule>();
+    for (Rule rule : targetRules) {
+      if (rule.subst == null) {
+        continue;
+      }
+      result.add(rule);
+    }
+    return result;
+  }
+
+  static Map<Integer, Set<Rule>> extract(LookupListTable table) {
+    Map<Integer, Set<Rule>> allRules = new TreeMap<Integer, Set<Rule>>();
+    for (int i = 0; i < table.subTableCount(); i++) {
+      extract(table, allRules, i);
+    }
+    return allRules;
+  }
+
+  private static Set<Rule> extract(LookupListTable lookupListTable,
+      Map<Integer, Set<Rule>> allRules, int i) {
+    if (allRules.containsKey(i)) {
+      return allRules.get(i);
+    }
+
+    Set<Rule> rules = new LinkedHashSet<Rule>();
+
+    LookupTable lookupTable = lookupListTable.subTableAt(i);
+    GsubLookupType lookupType = lookupTable.lookupType();
+    for (SubstSubtable substSubtable : lookupTable) {
+      GsubLookupType subTableLookupType = lookupType;
+
+      if (lookupType == GsubLookupType.GSUB_EXTENSION) {
+        ExtensionSubst extensionSubst = (ExtensionSubst) substSubtable;
+        substSubtable = extensionSubst.subTable();
+        subTableLookupType = extensionSubst.lookupType();
+      }
+
+      Set<Rule> subrules = null;
+
+      switch (subTableLookupType) {
+      case GSUB_LIGATURE:
+        subrules = extract((LigatureSubst) substSubtable);
+        break;
+      case GSUB_SINGLE:
+        subrules = extract((SingleSubst) substSubtable);
+        break;
+      case GSUB_ALTERNATE:
+        subrules = extract((AlternateSubst) substSubtable);
+        break;
+      case GSUB_MULTIPLE:
+        subrules = extract((MultipleSubst) substSubtable);
+        break;
+      case GSUB_REVERSE_CHAINING_CONTEXTUAL_SINGLE:
+        subrules = extract((ReverseChainSingleSubst) substSubtable);
+        break;
+      case GSUB_CHAINING_CONTEXTUAL:
+        subrules = extract((ChainContextSubst) substSubtable, lookupListTable, allRules);
+        break;
+      case GSUB_CONTEXTUAL:
+        subrules = extract((ContextSubst) substSubtable, lookupListTable, allRules);
+        break;
+      default:
+        throw new IllegalStateException();
+      }
+      if (subrules == null) {
+        throw new IllegalStateException();
+      }
+      rules.addAll(subrules);
+    }
+
+    if (rules.size() == 0) {
+      System.err.println("There are no rules in lookup " + i);
+    }
+    allRules.put(i, rules);
+    return rules;
+  }
+
+  private static RuleSegment extract(CoverageArray table) {
+    RuleSegment result = new RuleSegment();
+    for (CoverageTable coverage : table) {
+      GlyphGroup glyphGroup = new GlyphGroup();
+      glyphGroup.addAll(extract(coverage));
+      result.add(glyphGroup);
+    }
+    return result;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/RuleSegment.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/RuleSegment.java
new file mode 100644
index 0000000..72bf042
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/RuleSegment.java
@@ -0,0 +1,71 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.table.core.PostScriptTable;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+class RuleSegment extends LinkedList<GlyphGroup> {
+  private static final long serialVersionUID = 4563803321401665616L;
+
+  RuleSegment() {
+    super();
+  }
+
+  RuleSegment(GlyphGroup glyphGroup) {
+    addInternal(glyphGroup);
+  }
+
+  RuleSegment(int glyph) {
+    GlyphGroup glyphGroup = new GlyphGroup(glyph);
+    addInternal(glyphGroup);
+  }
+
+  RuleSegment(GlyphList glyphs) {
+    for (int glyph : glyphs) {
+      GlyphGroup glyphGroup = new GlyphGroup(glyph);
+      addInternal(glyphGroup);
+    }
+  }
+
+  boolean add(int glyph) {
+    GlyphGroup glyphGroup = new GlyphGroup(glyph);
+    return addInternal(glyphGroup);
+  }
+
+  @Override
+  public boolean addAll(Collection<? extends GlyphGroup> glyphGroups) {
+    for(GlyphGroup glyphGroup : glyphGroups) {
+      if (glyphGroup == null) {
+        throw new IllegalArgumentException("Null GlyphGroup not allowed");
+      }
+    }
+    return super.addAll(glyphGroups);
+  }
+
+  private boolean addInternal(GlyphGroup glyphGroup) {
+    if (glyphGroup == null) {
+      throw new IllegalArgumentException("Null GlyphGroup not allowed");
+    }
+    return super.add(glyphGroup);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    for (GlyphGroup glyphGroup : this) {
+      sb.append(glyphGroup.toString());
+    }
+    return sb.toString();
+  }
+
+  String toString(PostScriptTable post) {
+    StringBuilder sb = new StringBuilder();
+    for (GlyphGroup glyphGroup : this) {
+      sb.append(glyphGroup.toString(post));
+      sb.append(" ");
+    }
+    return sb.toString();
+  }
+
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/SubstLookupRecord.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/SubstLookupRecord.java
new file mode 100644
index 0000000..eda6eed
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/SubstLookupRecord.java
@@ -0,0 +1,24 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+
+final class SubstLookupRecord implements Record {
+  static final int RECORD_SIZE = 4;
+  private static final int SEQUENCE_INDEX_OFFSET = 0;
+  private static final int LOOKUP_LIST_INDEX_OFFSET = 2;
+  final int sequenceIndex;
+  final int lookupListIndex;
+
+  SubstLookupRecord(ReadableFontData data, int base) {
+    this.sequenceIndex = data.readUShort(base + SEQUENCE_INDEX_OFFSET);
+    this.lookupListIndex = data.readUShort(base + LOOKUP_LIST_INDEX_OFFSET);
+  }
+
+  @Override
+  public int writeTo(WritableFontData newData, int base) {
+    newData.writeUShort(base + SEQUENCE_INDEX_OFFSET, sequenceIndex);
+    newData.writeUShort(base + LOOKUP_LIST_INDEX_OFFSET, lookupListIndex);
+    return RECORD_SIZE;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/SubstLookupRecordList.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/SubstLookupRecordList.java
new file mode 100644
index 0000000..d6c8e07
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/SubstLookupRecordList.java
@@ -0,0 +1,28 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+
+public final class SubstLookupRecordList extends RecordList<SubstLookupRecord> {
+  private SubstLookupRecordList(WritableFontData data) {
+    super(data);
+  }
+
+  public SubstLookupRecordList(ReadableFontData data, int base) {
+    super(data, 0, base);
+  }
+
+  public SubstLookupRecordList(ReadableFontData data, int countOffset, int valuesOffset) {
+    super(data, 0, countOffset, valuesOffset);
+  }
+
+  @Override
+  protected SubstLookupRecord getRecordAt(ReadableFontData data, int offset) {
+    return new SubstLookupRecord(data, offset);
+  }
+
+  @Override
+  protected int recordSize() {
+    return SubstLookupRecord.RECORD_SIZE;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/TagOffsetRecord.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/TagOffsetRecord.java
new file mode 100644
index 0000000..3157cf8
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/TagOffsetRecord.java
@@ -0,0 +1,29 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+
+final class TagOffsetRecord implements Record {
+  static final int RECORD_SIZE = 6;
+  private static final int TAG_POS = 0;
+  private static final int OFFSET_POS = 4;
+  final int tag;
+  final int offset;
+
+  TagOffsetRecord(ReadableFontData data, int base) {
+    this.tag = data.readULongAsInt(base + TAG_POS);
+    this.offset = data.readUShort(base + OFFSET_POS);
+  }
+
+  TagOffsetRecord(int tag, int offset) {
+    this.tag = tag;
+    this.offset = offset;
+  }
+
+  @Override
+  public int writeTo(WritableFontData newData, int base) {
+    newData.writeULong(base + TAG_POS, tag);
+    newData.writeUShort(base + OFFSET_POS, offset);
+    return RECORD_SIZE;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/TagOffsetRecordList.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/TagOffsetRecordList.java
new file mode 100644
index 0000000..5f1224d
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/TagOffsetRecordList.java
@@ -0,0 +1,41 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+
+import java.util.Iterator;
+
+final class TagOffsetRecordList extends RecordList<TagOffsetRecord> {
+  TagOffsetRecordList(WritableFontData data) {
+    super(data);
+  }
+
+  TagOffsetRecordList(ReadableFontData data) {
+    super(data);
+  }
+
+  static int sizeOfListOfCount(int count) {
+    return RecordList.DATA_OFFSET + count * TagOffsetRecord.RECORD_SIZE;
+  }
+
+  TagOffsetRecord getRecordForTag(int tag) {
+    Iterator<TagOffsetRecord> iterator = iterator();
+    while (iterator.hasNext()) {
+      TagOffsetRecord record = iterator.next();
+      if (record.tag == tag) {
+        return record;
+      }
+    }
+    return null;
+  }
+
+  @Override
+  protected TagOffsetRecord getRecordAt(ReadableFontData data, int offset) {
+    return new TagOffsetRecord(data, offset);
+  }
+
+  @Override
+  protected int recordSize() {
+    return TagOffsetRecord.RECORD_SIZE;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/TagOffsetsTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/TagOffsetsTable.java
new file mode 100644
index 0000000..6219df6
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/TagOffsetsTable.java
@@ -0,0 +1,288 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.SubTable;
+
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.NoSuchElementException;
+import java.util.TreeMap;
+
+public abstract class TagOffsetsTable<S extends SubTable> extends HeaderTable
+    implements Iterable<S> {
+  private final TagOffsetRecordList recordList;
+
+  // ///////////////
+  // constructors
+
+  protected TagOffsetsTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    recordList = new TagOffsetRecordList(data.slice(headerSize() + base));
+  }
+
+  protected TagOffsetsTable(ReadableFontData data, boolean dataIsCanonical) {
+    this(data, 0, dataIsCanonical);
+  }
+
+  // ////////////////
+  // private methods
+
+  public int count() {
+    return recordList.count();
+  }
+
+  protected int tagAt(int index) {
+    return recordList.get(index).tag;
+  }
+
+  public S subTableAt(int index) {
+    TagOffsetRecord record = recordList.get(index);
+    return subTableForRecord(record);
+  }
+
+  @Override
+  public Iterator<S> iterator() {
+    return new Iterator<S>() {
+      private Iterator<TagOffsetRecord> recordIterator = recordList.iterator();
+
+      @Override
+      public boolean hasNext() {
+        return recordIterator.hasNext();
+      }
+
+      @Override
+      public S next() {
+        if (!hasNext()) {
+          throw new NoSuchElementException();
+        }
+        TagOffsetRecord record = recordIterator.next();
+        return subTableForRecord(record);
+      }
+
+      @Override
+      public void remove() {
+        throw new UnsupportedOperationException();
+      }
+    };
+  }
+
+  // ////////////////////////////////////
+  // implementations pushed to subclasses
+
+  protected abstract S readSubTable(ReadableFontData data, boolean dataIsCanonical);
+
+  // ////////////////////////////////////
+  // private methods
+
+  private S subTableForRecord(TagOffsetRecord record) {
+    ReadableFontData newBase = data.slice(record.offset);
+    return readSubTable(newBase, dataIsCanonical);
+  }
+
+  public abstract static class Builder<T extends HeaderTable, S extends SubTable>
+      extends HeaderTable.Builder<T> {
+
+    private TreeMap<Integer, VisibleSubTable.Builder<S>> builders;
+    private int serializedLength;
+    private int serializedCount;
+    private final int base;
+
+    protected Builder() {
+      super();
+      base = 0;
+    }
+
+    protected Builder(TagOffsetsTable.Builder<T, S> other) {
+      super();
+      builders = other.builders;
+      dataIsCanonical = other.dataIsCanonical;
+      base = other.base;
+    }
+
+    protected Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data);
+      this.base = base;
+      this.dataIsCanonical = dataIsCanonical;
+      if (!dataIsCanonical) {
+        prepareToEdit();
+      }
+    }
+
+    @Override
+    public int subDataSizeToSerialize() {
+      if (builders != null) {
+        computeSizeFromBuilders();
+      } else {
+        computeSizeFromData(internalReadData().slice(headerSize() + base));
+      }
+      serializedLength += super.subDataSizeToSerialize();
+      return serializedLength;
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      if (serializedLength == 0) {
+        return 0;
+      }
+
+      int writtenBytes = super.subSerialize(newData);
+      if (builders != null) {
+        return serializeFromBuilders(newData.slice(writtenBytes));
+      }
+      return serializeFromData(newData.slice(writtenBytes));
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    @Override
+    public void subDataSet() {
+      builders = null;
+    }
+
+    @Override
+    public T subBuildTable(ReadableFontData data) {
+      return readTable(data, 0, true);
+    }
+
+    // ////////////////////////////////////
+    // implementations pushed to subclasses
+
+    protected abstract T readTable(ReadableFontData data, int base, boolean dataIsCanonical);
+
+    protected abstract VisibleSubTable.Builder<S> createSubTableBuilder();
+
+    protected abstract VisibleSubTable.Builder<S> createSubTableBuilder(
+        ReadableFontData data, int tag, boolean dataIsCanonical);
+
+    // ////////////////////////////////////
+    // private methods
+
+    private void prepareToEdit() {
+      if (builders == null) {
+        initFromData(internalReadData(), headerSize() + base);
+        setModelChanged();
+      }
+    }
+
+    private void initFromData(ReadableFontData data, int base) {
+      builders = new TreeMap<Integer, VisibleSubTable.Builder<S>>();
+      if (data == null) {
+        return;
+      }
+
+      data = data.slice(base);
+      // Start of the first subtable in the data, if we're canonical.
+      TagOffsetRecordList recordList = new TagOffsetRecordList(data);
+      if (recordList.count() == 0) {
+        return;
+      }
+
+      int subTableLimit = recordList.limit();
+      Iterator<TagOffsetRecord> recordIterator = recordList.iterator();
+      if (dataIsCanonical) {
+        do {
+          // Each table starts where the previous one ended.
+          int offset = subTableLimit;
+          TagOffsetRecord record = recordIterator.next();
+          int tag = record.tag;
+          // Each table ends at the next start, or at the end of the data.
+          subTableLimit = record.offset;
+          // TODO(cibu): length computation does not seems to be correct.
+          int length = subTableLimit - offset;
+          VisibleSubTable.Builder<S> builder = createSubTableBuilder(data, offset, length, tag);
+          builders.put(tag, builder);
+        } while (recordIterator.hasNext());
+      } else {
+        do {
+          TagOffsetRecord record = recordIterator.next();
+          int offset = record.offset;
+          int tag = record.tag;
+          VisibleSubTable.Builder<S> builder = createSubTableBuilder(data, offset, -1, tag);
+          builders.put(tag, builder);
+        } while (recordIterator.hasNext());
+      }
+    }
+
+    private void computeSizeFromBuilders() {
+      // This does not merge LangSysTables that reference the same
+      // features.
+
+      // If there is no data in the default LangSysTable or any
+      // of the other LangSysTables, the size is zero, and this table
+      // will not be written.
+
+      int len = 0;
+      int count = 0;
+      for (VisibleSubTable.Builder<? extends SubTable> builder : builders.values()) {
+        int sublen = builder.subDataSizeToSerialize();
+        if (sublen > 0) {
+          ++count;
+          len += sublen;
+        }
+      }
+      if (len > 0) {
+        len += TagOffsetRecordList.sizeOfListOfCount(count);
+      }
+      serializedLength = len;
+      serializedCount = count;
+    }
+
+    private void computeSizeFromData(ReadableFontData data) {
+      // This assumes canonical data.
+      int len = 0;
+      int count = 0;
+      if (data != null) {
+        len = data.length();
+        count = new TagOffsetRecordList(data).count();
+      }
+      serializedLength = len;
+      serializedCount = count;
+    }
+
+    private int serializeFromBuilders(WritableFontData newData) {
+      // The canonical form of the data consists of the header,
+      // the index, then the
+      // scriptTables from the index in index order. All
+      // scriptTables are distinct; there's no sharing of tables.
+
+      // Find size for table
+      int tableSize = TagOffsetRecordList.sizeOfListOfCount(serializedCount);
+
+      // Fill header in table and serialize its builder.
+      int subTableFillPos = tableSize;
+
+      TagOffsetRecordList recordList = new TagOffsetRecordList(newData);
+      for (Entry<Integer, VisibleSubTable.Builder<S>> entry : builders.entrySet()) {
+        int tag = entry.getKey();
+        VisibleSubTable.Builder<? extends SubTable> builder = entry.getValue();
+        if (builder.serializedLength > 0) {
+          TagOffsetRecord record = new TagOffsetRecord(tag, subTableFillPos);
+          recordList.add(record);
+          subTableFillPos += builder.subSerialize(newData.slice(subTableFillPos));
+        }
+      }
+      recordList.writeTo(newData);
+      return subTableFillPos;
+    }
+
+    private int serializeFromData(WritableFontData newData) {
+      // The source data must be canonical.
+      ReadableFontData data = internalReadData().slice(base);
+      data.copyTo(newData);
+      return data.length();
+    }
+
+    private VisibleSubTable.Builder<S> createSubTableBuilder(
+        ReadableFontData data, int offset, int length, int tag) {
+      boolean dataIsCanonical = (length >= 0);
+      ReadableFontData newData = dataIsCanonical ? data.slice(offset, length) : data.slice(offset);
+      return createSubTableBuilder(newData, tag, dataIsCanonical);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/VisibleBuilder.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/VisibleBuilder.java
new file mode 100644
index 0000000..fb21034
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/VisibleBuilder.java
@@ -0,0 +1,31 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.SubTable;
+
+public abstract 
+  class VisibleBuilder<T extends SubTable> extends SubTable.Builder<T> {
+
+    protected int serializedLength;
+    
+    protected VisibleBuilder() {
+      super(null);
+    }
+
+    protected VisibleBuilder(ReadableFontData data) {
+      super(data);
+    }
+
+    @Override
+    public abstract int subSerialize(WritableFontData newData);
+
+    @Override
+    public abstract int subDataSizeToSerialize();
+    
+    @Override
+    public abstract void subDataSet();
+
+    @Override
+    public abstract T subBuildTable(ReadableFontData data);
+ }
\ No newline at end of file
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/component/VisibleSubTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/component/VisibleSubTable.java
new file mode 100644
index 0000000..b02a5e0
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/component/VisibleSubTable.java
@@ -0,0 +1,40 @@
+package com.google.typography.font.sfntly.table.opentype.component;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.SubTable;
+
+public abstract class VisibleSubTable extends SubTable {
+  private VisibleSubTable(ReadableFontData data) {
+    super(data);
+  }
+
+  public abstract static class Builder<T extends SubTable> extends SubTable.Builder<T> {
+    protected int serializedLength;
+
+    protected Builder() {
+      super(null);
+    }
+
+    protected Builder(ReadableFontData data) {
+      super(data);
+    }
+
+    @Override
+    public abstract int subSerialize(WritableFontData newData);
+
+    /**
+     * Even though public, not to be used by the end users. Made public only
+     * make it available to packages under
+     * {@code com.google.typography.font.sfntly.table.opentype}.
+     */
+    @Override
+    public abstract int subDataSizeToSerialize();
+
+    @Override
+    protected abstract void subDataSet();
+
+    @Override
+    protected abstract T subBuildTable(ReadableFontData data);
+  }
+}
\ No newline at end of file
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/DoubleRecordTable.java b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/DoubleRecordTable.java
new file mode 100644
index 0000000..af0648b
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/DoubleRecordTable.java
@@ -0,0 +1,121 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly.table.opentype.contextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.data.WritableFontData;
+import com.google.typography.font.sfntly.table.SubTable;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.SubstLookupRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class DoubleRecordTable extends SubTable {
+  public final NumRecordList inputGlyphs;
+  public final SubstLookupRecordList lookupRecords;
+
+  // ///////////////
+  // constructors
+
+  public DoubleRecordTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data);
+    inputGlyphs = new NumRecordList(data, 1, base, base + 4);
+    lookupRecords = new SubstLookupRecordList(data, base + 2, inputGlyphs.limit());
+  }
+
+  public DoubleRecordTable(ReadableFontData data, boolean dataIsCanonical) {
+    this(data, 0, dataIsCanonical);
+  }
+
+  public abstract static class Builder<T extends DoubleRecordTable> extends VisibleSubTable.Builder<T> {
+    protected NumRecordList inputGlyphIdsBuilder;
+    protected SubstLookupRecordList substLookupRecordsBuilder;
+    protected int serializedLength;
+
+    public Builder() {
+      super();
+    }
+
+    public Builder(DoubleRecordTable table) {
+      this(table.readFontData(), 0, false);
+    }
+
+    public Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data);
+      if (!dataIsCanonical) {
+        prepareToEdit();
+      }
+    }
+
+    public Builder(Builder<T> other) {
+      super();
+      inputGlyphIdsBuilder = other.inputGlyphIdsBuilder;
+      substLookupRecordsBuilder = other.substLookupRecordsBuilder;
+    }
+
+    @Override
+    public int subDataSizeToSerialize() {
+      if (substLookupRecordsBuilder != null) {
+        serializedLength = substLookupRecordsBuilder.limit();
+      } else {
+        computeSizeFromData(internalReadData());
+      }
+      return serializedLength;
+    }
+
+    @Override
+    public int subSerialize(WritableFontData newData) {
+      if (serializedLength == 0) {
+        return 0;
+      }
+
+      if (inputGlyphIdsBuilder == null || substLookupRecordsBuilder == null) {
+        return serializeFromData(newData);
+      }
+
+      return inputGlyphIdsBuilder.writeTo(newData) + substLookupRecordsBuilder.writeTo(newData);
+    }
+
+    @Override
+    protected boolean subReadyToSerialize() {
+      return true;
+    }
+
+    @Override
+    public void subDataSet() {
+      inputGlyphIdsBuilder = null;
+      substLookupRecordsBuilder = null;
+    }
+
+    // ////////////////////////////////////
+    // private methods
+
+    private void prepareToEdit() {
+      initFromData(internalReadData());
+      setModelChanged();
+    }
+
+    private void initFromData(ReadableFontData data) {
+      if (inputGlyphIdsBuilder == null || substLookupRecordsBuilder == null) {
+        inputGlyphIdsBuilder = new NumRecordList(data, 1, 0, 4);
+        substLookupRecordsBuilder = new SubstLookupRecordList(
+            data, 2, inputGlyphIdsBuilder.limit());
+      }
+    }
+
+    private void computeSizeFromData(ReadableFontData data) {
+      // This assumes canonical data.
+      int len = 0;
+      if (data != null) {
+        len = data.length();
+      }
+      serializedLength = len;
+    }
+
+    private int serializeFromData(WritableFontData newData) {
+      // The source data must be canonical.
+      ReadableFontData data = internalReadData();
+      data.copyTo(newData);
+      return data.length();
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubClassRule.java b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubClassRule.java
new file mode 100644
index 0000000..f8b5142
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubClassRule.java
@@ -0,0 +1,33 @@
+package com.google.typography.font.sfntly.table.opentype.contextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.GlyphClassList;
+
+public class SubClassRule extends DoubleRecordTable {
+  SubClassRule(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  public GlyphClassList inputClasses() {
+    return new GlyphClassList(inputGlyphs);
+  }
+
+  static class Builder extends DoubleRecordTable.Builder<SubClassRule> {
+    Builder() {
+      super();
+    }
+
+    Builder(SubClassRule table) {
+      super(table);
+    }
+
+    Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected SubClassRule subBuildTable(ReadableFontData data) {
+      return new SubClassRule(data, 0, true);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubClassSet.java b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubClassSet.java
new file mode 100644
index 0000000..d34df68
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubClassSet.java
@@ -0,0 +1,50 @@
+package com.google.typography.font.sfntly.table.opentype.contextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class SubClassSet extends SubGenericRuleSet<SubClassRule> {
+  SubClassSet(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  @Override
+  protected SubClassRule readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new SubClassRule(data, base, dataIsCanonical);
+  }
+
+  static class Builder extends SubGenericRuleSet.Builder<SubClassSet, SubClassRule> {
+    Builder() {
+      super();
+    }
+
+    Builder(SubClassSet table) {
+      super(table);
+    }
+
+    Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    @Override
+    protected SubClassSet readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new SubClassSet(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubClassRule> createSubTableBuilder() {
+      return new SubClassRule.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubClassRule> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new SubClassRule.Builder(data, 0, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubClassRule> createSubTableBuilder(SubClassRule subTable) {
+      return new SubClassRule.Builder(subTable);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubClassSetArray.java b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubClassSetArray.java
new file mode 100644
index 0000000..9f3ddb9
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubClassSetArray.java
@@ -0,0 +1,83 @@
+package com.google.typography.font.sfntly.table.opentype.contextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.ClassDefTable;
+import com.google.typography.font.sfntly.table.opentype.CoverageTable;
+import com.google.typography.font.sfntly.table.opentype.component.OffsetRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class SubClassSetArray extends OffsetRecordTable<SubClassSet> {
+  private static final int FIELD_COUNT = 2;
+
+  private static final int COVERAGE_INDEX = 0;
+  private static final int COVERAGE_DEFAULT = 0;
+  private static final int CLASS_DEF_INDEX = 1;
+  private static final int CLASS_DEF_DEFAULT = 0;
+
+  public final CoverageTable coverage;
+  public final ClassDefTable classDef;
+
+  public SubClassSetArray(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    int coverageOffset = getField(COVERAGE_INDEX);
+    coverage = new CoverageTable(data.slice(coverageOffset), 0, dataIsCanonical);
+    int classDefOffset = getField(CLASS_DEF_INDEX);
+    classDef = new ClassDefTable(data.slice(classDefOffset), 0, dataIsCanonical);
+  }
+
+  @Override
+  public SubClassSet readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new SubClassSet(data, 0, dataIsCanonical);
+  }
+
+  public static class Builder extends OffsetRecordTable.Builder<SubClassSetArray, SubClassSet> {
+    protected Builder() {
+      super();
+    }
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical, boolean isFmt2) {
+      super(data, dataIsCanonical);
+    }
+
+    protected Builder(SubClassSetArray table) {
+      super(table);
+    }
+
+    @Override
+    protected SubClassSetArray readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new SubClassSetArray(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubClassSet> createSubTableBuilder() {
+      return new SubClassSet.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubClassSet> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new SubClassSet.Builder(data, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubClassSet> createSubTableBuilder(SubClassSet subTable) {
+      return new SubClassSet.Builder(subTable);
+    }
+
+    @Override
+    protected void initFields() {
+      setField(COVERAGE_INDEX, COVERAGE_DEFAULT);
+      setField(CLASS_DEF_INDEX, CLASS_DEF_DEFAULT);
+    }
+
+    @Override
+    protected int fieldCount() {
+      return FIELD_COUNT;
+    }
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubGenericRuleSet.java b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubGenericRuleSet.java
new file mode 100644
index 0000000..a8038ee
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubGenericRuleSet.java
@@ -0,0 +1,41 @@
+package com.google.typography.font.sfntly.table.opentype.contextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.OffsetRecordTable;
+
+public abstract class SubGenericRuleSet<T extends DoubleRecordTable> extends OffsetRecordTable<T> {
+  protected SubGenericRuleSet(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  @Override
+  public int fieldCount() {
+    return 0;
+  }
+
+  protected abstract static class Builder<T extends SubGenericRuleSet<S>,
+      S extends DoubleRecordTable>
+      extends OffsetRecordTable.Builder<T, S> {
+
+    protected Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    protected Builder() {
+      super();
+    }
+
+    protected Builder(T table) {
+      super(table);
+    }
+
+    @Override
+    protected void initFields() {
+    }
+
+    @Override
+    protected int fieldCount() {
+      return 0;
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubRule.java b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubRule.java
new file mode 100644
index 0000000..b65a40e
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubRule.java
@@ -0,0 +1,28 @@
+package com.google.typography.font.sfntly.table.opentype.contextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+
+public class SubRule extends DoubleRecordTable {
+  SubRule(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  static class Builder extends DoubleRecordTable.Builder<SubRule> {
+    Builder() {
+      super();
+    }
+
+    Builder(SubRule table) {
+      super(table);
+    }
+
+    Builder(ReadableFontData data, int base, boolean dataIsCanonical) {
+      super(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected SubRule subBuildTable(ReadableFontData data) {
+      return new SubRule(data, 0, true);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubRuleSet.java b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubRuleSet.java
new file mode 100644
index 0000000..30db986
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubRuleSet.java
@@ -0,0 +1,50 @@
+package com.google.typography.font.sfntly.table.opentype.contextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class SubRuleSet extends SubGenericRuleSet<SubRule> {
+  SubRuleSet(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  @Override
+  protected SubRule readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new SubRule(data, base, dataIsCanonical);
+  }
+
+  static class Builder extends SubGenericRuleSet.Builder<SubRuleSet, SubRule> {
+    Builder() {
+      super();
+    }
+
+    Builder(SubRuleSet table) {
+      super(table);
+    }
+
+    Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    @Override
+    protected SubRuleSet readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new SubRuleSet(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubRule> createSubTableBuilder() {
+      return new SubRule.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubRule> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new SubRule.Builder(data, 0, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubRule> createSubTableBuilder(SubRule subTable) {
+      return new SubRule.Builder(subTable);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubRuleSetArray.java b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubRuleSetArray.java
new file mode 100644
index 0000000..f6ca244
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/contextsubst/SubRuleSetArray.java
@@ -0,0 +1,76 @@
+package com.google.typography.font.sfntly.table.opentype.contextsubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.CoverageTable;
+import com.google.typography.font.sfntly.table.opentype.component.OffsetRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class SubRuleSetArray extends OffsetRecordTable<SubRuleSet> {
+  private static final int FIELD_COUNT = 1;
+
+  private static final int COVERAGE_INDEX = 0;
+  private static final int COVERAGE_DEFAULT = 0;
+
+  public final CoverageTable coverage;
+
+  public SubRuleSetArray(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    int coverageOffset = getField(COVERAGE_INDEX);
+    coverage = new CoverageTable(data.slice(coverageOffset), 0, dataIsCanonical);
+  }
+
+  @Override
+  public SubRuleSet readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new SubRuleSet(data, 0, dataIsCanonical);
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+
+  public static class Builder extends OffsetRecordTable.Builder<SubRuleSetArray, SubRuleSet> {
+    public Builder() {
+      super();
+    }
+
+    public Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    public Builder(SubRuleSetArray table) {
+      super(table);
+    }
+
+    @Override
+    protected SubRuleSetArray readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new SubRuleSetArray(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubRuleSet> createSubTableBuilder() {
+      return new SubRuleSet.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubRuleSet> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new SubRuleSet.Builder(data, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<SubRuleSet> createSubTableBuilder(SubRuleSet subTable) {
+      return new SubRuleSet.Builder(subTable);
+    }
+
+    @Override
+    protected void initFields() {
+      setField(COVERAGE_INDEX, COVERAGE_DEFAULT);
+    }
+
+    @Override
+    protected int fieldCount() {
+      return FIELD_COUNT;
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/ligaturesubst/InnerArrayFmt1.java b/java/src/com/google/typography/font/sfntly/table/opentype/ligaturesubst/InnerArrayFmt1.java
new file mode 100644
index 0000000..fa0698f
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/ligaturesubst/InnerArrayFmt1.java
@@ -0,0 +1,75 @@
+package com.google.typography.font.sfntly.table.opentype.ligaturesubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.CoverageTable;
+import com.google.typography.font.sfntly.table.opentype.component.OffsetRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class InnerArrayFmt1 extends OffsetRecordTable<LigatureSet> {
+  private static final int FIELD_COUNT = 1;
+
+  private static final int COVERAGE_INDEX = 0;
+  private static final int COVERAGE_DEFAULT = 0;
+  public final CoverageTable coverage;
+
+  public InnerArrayFmt1(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    int coverageOffset = getField(COVERAGE_INDEX);
+    coverage = new CoverageTable(data.slice(coverageOffset), 0, dataIsCanonical);
+  }
+
+  @Override
+  public LigatureSet readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new LigatureSet(data, 0, dataIsCanonical);
+  }
+
+  public static class Builder extends OffsetRecordTable.Builder<InnerArrayFmt1, LigatureSet> {
+    public Builder() {
+      super();
+    }
+
+    public Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    public Builder(InnerArrayFmt1 table) {
+      super(table);
+    }
+
+    @Override
+    protected InnerArrayFmt1 readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new InnerArrayFmt1(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<LigatureSet> createSubTableBuilder() {
+      return new LigatureSet.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<LigatureSet> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new LigatureSet.Builder(data, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<LigatureSet> createSubTableBuilder(LigatureSet subTable) {
+      return new LigatureSet.Builder(subTable);
+    }
+
+    @Override
+    protected void initFields() {
+      setField(COVERAGE_INDEX, COVERAGE_DEFAULT);
+    }
+
+    @Override
+    protected int fieldCount() {
+      return FIELD_COUNT;
+    }
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/ligaturesubst/Ligature.java b/java/src/com/google/typography/font/sfntly/table/opentype/ligaturesubst/Ligature.java
new file mode 100644
index 0000000..b1670e6
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/ligaturesubst/Ligature.java
@@ -0,0 +1,62 @@
+package com.google.typography.font.sfntly.table.opentype.ligaturesubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecord;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.RecordList;
+import com.google.typography.font.sfntly.table.opentype.component.RecordsTable;
+
+public class Ligature extends RecordsTable<NumRecord> {
+  private static final int FIELD_COUNT = 1;
+
+  public static final int LIG_GLYPH_INDEX = 0;
+  private static final int LIG_GLYPH_DEFAULT = 0;
+
+  Ligature(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  static class Builder extends RecordsTable.Builder<Ligature, NumRecord> {
+    Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    Builder() {
+      super();
+    }
+
+    Builder(Ligature table) {
+      super(table);
+    }
+
+    @Override
+    protected Ligature readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new Ligature(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected void initFields() {
+      setField(LIG_GLYPH_INDEX, LIG_GLYPH_DEFAULT);
+    }
+
+    @Override
+    protected int fieldCount() {
+      return FIELD_COUNT;
+    }
+
+    @Override
+    protected RecordList<NumRecord> readRecordList(ReadableFontData data, int base) {
+      return new NumRecordList(data);
+    }
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+
+  @Override
+  protected RecordList<NumRecord> createRecordList(ReadableFontData data) {
+    return new NumRecordList(data, 1);
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/ligaturesubst/LigatureSet.java b/java/src/com/google/typography/font/sfntly/table/opentype/ligaturesubst/LigatureSet.java
new file mode 100644
index 0000000..1f21f0e
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/ligaturesubst/LigatureSet.java
@@ -0,0 +1,65 @@
+package com.google.typography.font.sfntly.table.opentype.ligaturesubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.component.OffsetRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class LigatureSet extends OffsetRecordTable<Ligature> {
+  LigatureSet(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+  }
+
+  static class Builder extends OffsetRecordTable.Builder<LigatureSet, Ligature> {
+    Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    Builder() {
+      super();
+    }
+
+    Builder(LigatureSet table) {
+      super(table);
+    }
+
+    @Override
+    protected LigatureSet readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new LigatureSet(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<Ligature> createSubTableBuilder() {
+      return new Ligature.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<Ligature> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new Ligature.Builder(data, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<Ligature> createSubTableBuilder(Ligature subTable) {
+      return new Ligature.Builder(subTable);
+    }
+
+    @Override
+    protected void initFields() {
+    }
+
+    @Override
+    protected int fieldCount() {
+      return 0;
+    }
+  }
+
+  @Override
+  protected Ligature readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new Ligature(data, base, dataIsCanonical);
+  }
+
+  @Override
+  public int fieldCount() {
+    return 0;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/multiplesubst/GlyphIds.java b/java/src/com/google/typography/font/sfntly/table/opentype/multiplesubst/GlyphIds.java
new file mode 100644
index 0000000..4896379
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/multiplesubst/GlyphIds.java
@@ -0,0 +1,76 @@
+package com.google.typography.font.sfntly.table.opentype.multiplesubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.CoverageTable;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.OffsetRecordTable;
+import com.google.typography.font.sfntly.table.opentype.component.VisibleSubTable;
+
+public class GlyphIds extends OffsetRecordTable<NumRecordTable> {
+  private static final int FIELD_COUNT = 1;
+
+  private static final int COVERAGE_INDEX = 0;
+  private static final int COVERAGE_DEFAULT = 0;
+  public final CoverageTable coverage;
+
+  public GlyphIds(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    int coverageOffset = getField(COVERAGE_INDEX);
+    coverage = new CoverageTable(data.slice(coverageOffset), 0, dataIsCanonical);
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+
+  @Override
+  public NumRecordTable readSubTable(ReadableFontData data, boolean dataIsCanonical) {
+    return new NumRecordTable(data, 0, dataIsCanonical);
+  }
+
+  public static class Builder extends OffsetRecordTable.Builder<GlyphIds, NumRecordTable> {
+    public Builder() {
+      super();
+    }
+
+    public Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    public Builder(GlyphIds table) {
+      super(table);
+    }
+
+    @Override
+    protected GlyphIds readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new GlyphIds(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected void initFields() {
+      setField(COVERAGE_INDEX, COVERAGE_DEFAULT);
+    }
+
+    @Override
+    protected int fieldCount() {
+      return FIELD_COUNT;
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<NumRecordTable> createSubTableBuilder() {
+      return new NumRecordTable.Builder();
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<NumRecordTable> createSubTableBuilder(
+        ReadableFontData data, boolean dataIsCanonical) {
+      return new NumRecordTable.Builder(data, 0, dataIsCanonical);
+    }
+
+    @Override
+    protected VisibleSubTable.Builder<NumRecordTable> createSubTableBuilder(NumRecordTable subTable) {
+      return new NumRecordTable.Builder(subTable);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/package-info.java b/java/src/com/google/typography/font/sfntly/table/opentype/package-info.java
new file mode 100644
index 0000000..95a46cc
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/package-info.java
@@ -0,0 +1,11 @@
+/**
+ * This package and its sub-packages contain, classes required to do:
+ * <ul>
+ * <li>Parse GSUB table</li>
+ * <li>Compute the closure of a given set of glyph IDs based on GSUB lookups</li>
+ * </ul> 
+ * This is an experimental package. Please treat this API under this package as an alpha code.
+ *  
+ * @author Cibu Johny
+ */
+package com.google.typography.font.sfntly.table.opentype;
\ No newline at end of file
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/singlesubst/HeaderFmt1.java b/java/src/com/google/typography/font/sfntly/table/opentype/singlesubst/HeaderFmt1.java
new file mode 100644
index 0000000..d4a02ab
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/singlesubst/HeaderFmt1.java
@@ -0,0 +1,67 @@
+package com.google.typography.font.sfntly.table.opentype.singlesubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.CoverageTable;
+import com.google.typography.font.sfntly.table.opentype.component.HeaderTable;
+
+public class HeaderFmt1 extends HeaderTable {
+  private static final int FIELD_COUNT = 2;
+
+  private static final int COVERAGE_INDEX = 0;
+  private static final int COVERAGE_DEFAULT = 0;
+
+  private static final int DELTA_GLYPH_ID_INDEX = 1;
+  private static final int DELTA_GLYPH_ID_DEFAULT = 0;
+
+  public final CoverageTable coverage;
+
+  public HeaderFmt1(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    int coverageOffset = getField(COVERAGE_INDEX);
+    coverage = new CoverageTable(data.slice(coverageOffset), 0, dataIsCanonical);
+  }
+
+  public int getDelta() {
+    int delta = getField(DELTA_GLYPH_ID_INDEX);
+    if (delta > 0x7FFF) {
+      // Converting read unsigned int to signed short
+      return (short) delta;
+    }
+    return delta;
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+
+  public static class Builder extends HeaderTable.Builder<HeaderFmt1> {
+    public Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    public Builder(HeaderFmt1 table) {
+      super(table);
+    }
+
+    public Builder() {
+      super();
+    }
+
+    @Override
+    protected void initFields() {
+      setField(COVERAGE_INDEX, COVERAGE_DEFAULT);
+      setField(DELTA_GLYPH_ID_INDEX, DELTA_GLYPH_ID_DEFAULT);
+    }
+
+    @Override
+    protected int fieldCount() {
+      return FIELD_COUNT;
+    }
+
+    @Override
+    protected HeaderFmt1 subBuildTable(ReadableFontData data) {
+      return new HeaderFmt1(data, 0, false);
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/singlesubst/InnerArrayFmt2.java b/java/src/com/google/typography/font/sfntly/table/opentype/singlesubst/InnerArrayFmt2.java
new file mode 100644
index 0000000..036241a
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/singlesubst/InnerArrayFmt2.java
@@ -0,0 +1,69 @@
+package com.google.typography.font.sfntly.table.opentype.singlesubst;
+
+import com.google.typography.font.sfntly.data.ReadableFontData;
+import com.google.typography.font.sfntly.table.opentype.CoverageTable;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecord;
+import com.google.typography.font.sfntly.table.opentype.component.NumRecordList;
+import com.google.typography.font.sfntly.table.opentype.component.RecordList;
+import com.google.typography.font.sfntly.table.opentype.component.RecordsTable;
+
+public class InnerArrayFmt2 extends RecordsTable<NumRecord> {
+  private static final int FIELD_COUNT = 1;
+
+  private static final int COVERAGE_INDEX = 0;
+  private static final int COVERAGE_DEFAULT = 0;
+  public final CoverageTable coverage;
+
+  public InnerArrayFmt2(ReadableFontData data, int base, boolean dataIsCanonical) {
+    super(data, base, dataIsCanonical);
+    int coverageOffset = getField(COVERAGE_INDEX);
+    coverage = new CoverageTable(data.slice(coverageOffset), 0, dataIsCanonical);
+  }
+
+  @Override
+  protected RecordList<NumRecord> createRecordList(ReadableFontData data) {
+    return new NumRecordList(data);
+  }
+
+  public static class Builder extends RecordsTable.Builder<InnerArrayFmt2, NumRecord> {
+    public Builder() {
+      super();
+    }
+
+    public Builder(ReadableFontData data, boolean dataIsCanonical) {
+      super(data, dataIsCanonical);
+    }
+
+    public Builder(InnerArrayFmt2 table) {
+      super(table);
+    }
+
+    @Override
+    protected InnerArrayFmt2 readTable(ReadableFontData data, int base, boolean dataIsCanonical) {
+      return new InnerArrayFmt2(data, base, dataIsCanonical);
+    }
+
+    @Override
+    protected void initFields() {
+      setField(COVERAGE_INDEX, COVERAGE_DEFAULT);
+    }
+
+    @Override
+    protected int fieldCount() {
+      return FIELD_COUNT;
+    }
+
+    @Override
+    protected RecordList<NumRecord> readRecordList(ReadableFontData data, int base) {
+      if (base != 0) {
+        throw new UnsupportedOperationException();
+      }
+      return new NumRecordList(data);
+    }
+  }
+
+  @Override
+  public int fieldCount() {
+    return FIELD_COUNT;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/testing/FontLanguages.java b/java/src/com/google/typography/font/sfntly/table/opentype/testing/FontLanguages.java
new file mode 100644
index 0000000..e7cc211
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/testing/FontLanguages.java
@@ -0,0 +1,681 @@
+package com.google.typography.font.sfntly.table.opentype.testing;
+
+import com.google.typography.font.sfntly.Font;
+import com.google.typography.font.sfntly.Tag;
+import com.google.typography.font.sfntly.table.opentype.GSubTable;
+import com.google.typography.font.sfntly.table.opentype.ScriptListTable;
+import com.google.typography.font.sfntly.table.opentype.ScriptTag;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+class FontLanguages {
+  private static String[][] langScriptData = { { "aa", "Latn" },
+    { "ab", "Cyrl" },
+    { "abq", "Cyrl" },
+    { "ace", "Latn" },
+    { "ach", "Latn" },
+    { "ada", "Latn" },
+    { "ady", "Cyrl" },
+    { "ae", "Avst" },
+    { "af", "Latn" },
+    { "agq", "Latn" },
+    { "aii", "Cyrl" },
+    { "aii", "Syrc" },
+    { "ain", "Kana", "Latn" },
+    { "ak", "Latn" },
+    { "akk", "Xsux" },
+    { "ale", "Latn" },
+    { "alt", "Cyrl" },
+    { "am", "Ethi" },
+    { "amo", "Latn" },
+    { "an", "Latn" },
+    { "anp", "Deva" },
+    { "ar", "Arab" },
+    { "ar", "Syrc" },
+    { "arc", "Armi" },
+    { "arn", "Latn" },
+    { "arp", "Latn" },
+    { "arw", "Latn" },
+    { "as", "Beng" },
+    { "asa", "Latn" },
+    { "ast", "Latn" },
+    { "av", "Cyrl" },
+    { "awa", "Deva" },
+    { "ay", "Latn" },
+    { "az", "Arab", "Cyrl", "Latn" },
+    { "ba", "Cyrl" },
+    { "bal", "Arab", "Latn" },
+    { "ban", "Latn" },
+    { "ban", "Bali" },
+    { "bas", "Latn" },
+    { "bax", "Bamu" },
+    { "bbc", "Latn" },
+    { "bbc", "Batk" },
+    { "be", "Cyrl" },
+    { "bej", "Arab" },
+    { "bem", "Latn" },
+    { "bez", "Latn" },
+    { "bfq", "Taml" },
+    { "bft", "Arab" },
+    { "bft", "Tibt" },
+    { "bfy", "Deva" },
+    { "bg", "Cyrl" },
+    { "bh", "Deva", "Kthi" },
+    { "bhb", "Deva" },
+    { "bho", "Deva" },
+    { "bi", "Latn" },
+    { "bik", "Latn" },
+    { "bin", "Latn" },
+    { "bjj", "Deva" },
+    { "bku", "Latn" },
+    { "bku", "Buhd" },
+    { "bla", "Latn" },
+    { "blt", "Tavt" },
+    { "bm", "Latn" },
+    { "bn", "Beng" },
+    { "bo", "Tibt" },
+    { "bqv", "Latn" },
+    { "br", "Latn" },
+    { "bra", "Deva" },
+    { "brx", "Deva" },
+    { "bs", "Latn" },
+    { "btv", "Deva" },
+    { "bua", "Cyrl" },
+    { "buc", "Latn" },
+    { "bug", "Latn" },
+    { "bug", "Bugi" },
+    { "bya", "Latn" },
+    { "byn", "Ethi" },
+    { "ca", "Latn" },
+    { "cad", "Latn" },
+    { "car", "Latn" },
+    { "cay", "Latn" },
+    { "cch", "Latn" },
+    { "ccp", "Beng" },
+    { "ccp", "Cakm" },
+    { "ce", "Cyrl" },
+    { "ceb", "Latn" },
+    { "cgg", "Latn" },
+    { "ch", "Latn" },
+    { "chk", "Latn" },
+    { "chm", "Cyrl", "Latn" },
+    { "chn", "Latn" },
+    { "cho", "Latn" },
+    { "chp", "Latn" },
+    { "chp", "Cans" },
+    { "chr", "Cher", "Latn" },
+    { "chy", "Latn" },
+    { "cja", "Arab" },
+    { "cja", "Cham" },
+    { "cjm", "Cham" },
+    { "cjm", "Arab" },
+    { "cjs", "Cyrl" },
+    { "ckb", "Arab" },
+    { "ckt", "Cyrl" },
+    { "co", "Latn" },
+    { "cop", "Arab", "Copt", "Grek" },
+    { "cpe", "Latn" },
+    { "cr", "Cans", "Latn" },
+    { "crh", "Cyrl" },
+    { "crk", "Cans" },
+    { "cs", "Latn" },
+    { "csb", "Latn" },
+    { "cu", "Glag" },
+    { "cv", "Cyrl" },
+    { "cy", "Latn" },
+    { "da", "Latn" },
+    { "dak", "Latn" },
+    { "dar", "Cyrl" },
+    { "dav", "Latn" },
+    { "de", "Latn" },
+    { "de", "Runr" },
+    { "del", "Latn" },
+    { "den", "Latn" },
+    { "den", "Cans" },
+    { "dgr", "Latn" },
+    { "din", "Latn" },
+    { "dje", "Latn" },
+    { "dng", "Cyrl" },
+    { "doi", "Arab" },
+    { "dsb", "Latn" },
+    { "dua", "Latn" },
+    { "dv", "Thaa" },
+    { "dyo", "Arab" },
+    { "dyo", "Latn" },
+    { "dyu", "Latn" },
+    { "dz", "Tibt" },
+    { "ebu", "Latn" },
+    { "ee", "Latn" },
+    { "efi", "Latn" },
+    { "egy", "Egyp" },
+    { "eka", "Latn" },
+    { "eky", "Kali" },
+    { "el", "Grek" },
+    { "en", "Latn" },
+    { "en", "Dsrt", "Shaw" },
+    { "eo", "Latn" },
+    { "es", "Latn" },
+    { "et", "Latn" },
+    { "ett", "Ital", "Latn" },
+    { "eu", "Latn" },
+    { "evn", "Cyrl" },
+    { "ewo", "Latn" },
+    { "fa", "Arab" },
+    { "fan", "Latn" },
+    { "ff", "Latn" },
+    { "fi", "Latn" },
+    { "fil", "Latn" },
+    { "fil", "Tglg" },
+    { "fiu", "Latn" },
+    { "fj", "Latn" },
+    { "fo", "Latn" },
+    { "fon", "Latn" },
+    { "fr", "Latn" },
+    { "frr", "Latn" },
+    { "frs", "Latn" },
+    { "fur", "Latn" },
+    { "fy", "Latn" },
+    { "ga", "Latn" },
+    { "gaa", "Latn" },
+    { "gag", "Latn" },
+    { "gag", "Cyrl" },
+    { "gay", "Latn" },
+    { "gba", "Arab" },
+    { "gbm", "Deva" },
+    { "gcr", "Latn" },
+    { "gd", "Latn" },
+    { "gez", "Ethi" },
+    { "gil", "Latn" },
+    { "gl", "Latn" },
+    { "gld", "Cyrl" },
+    { "gn", "Latn" },
+    { "gon", "Deva", "Telu" },
+    { "gor", "Latn" },
+    { "got", "Goth" },
+    { "grb", "Latn" },
+    { "grc", "Cprt", "Grek", "Linb" },
+    { "grt", "Beng" },
+    { "gsw", "Latn" },
+    { "gu", "Gujr" },
+    { "guz", "Latn" },
+    { "gv", "Latn" },
+    { "gwi", "Latn" },
+    { "ha", "Arab", "Latn" },
+    { "hai", "Latn" },
+    { "haw", "Latn" },
+    { "he", "Hebr" },
+    { "hi", "Deva" },
+    { "hil", "Latn" },
+    { "hit", "Xsux" },
+    { "hmn", "Latn" },
+    { "hne", "Deva" },
+    { "hnn", "Latn" },
+    { "hnn", "Hano" },
+    { "ho", "Latn" },
+    { "hoc", "Deva" },
+    { "hoj", "Deva" },
+    { "hop", "Latn" },
+    { "hr", "Latn" },
+    { "hsb", "Latn" },
+    { "ht", "Latn" },
+    { "hu", "Latn" },
+    { "hup", "Latn" },
+    { "hy", "Armn" },
+    { "hz", "Latn" },
+    { "ia", "Latn" },
+    { "iba", "Latn" },
+    { "ibb", "Latn" },
+    { "id", "Latn" },
+    { "id", "Arab" },
+    { "ig", "Latn" },
+    { "ii", "Yiii" },
+    { "ii", "Latn" },
+    { "ik", "Latn" },
+    { "ilo", "Latn" },
+    { "inh", "Cyrl" },
+    { "inh", "Arab", "Latn" },
+    { "is", "Latn" },
+    { "it", "Latn" },
+    { "iu", "Cans" },
+    { "iu", "Latn" },
+    { "ja", "Jpan" },
+    { "jmc", "Latn" },
+    { "jpr", "Hebr" },
+    { "jrb", "Hebr" },
+    { "jv", "Latn" },
+    { "jv", "Java" },
+    { "ka", "Geor" },
+    { "kaa", "Cyrl" },
+    { "kab", "Latn" },
+    { "kac", "Latn" },
+    { "kaj", "Latn" },
+    { "kam", "Latn" },
+    { "kbd", "Cyrl" },
+    { "kca", "Cyrl" },
+    { "kcg", "Latn" },
+    { "kde", "Latn" },
+    { "kdt", "Thai" },
+    { "kea", "Latn" },
+    { "kfo", "Latn" },
+    { "kfr", "Deva" },
+    { "kg", "Latn" },
+    { "kha", "Latn" },
+    { "kha", "Beng" },
+    { "khb", "Talu" },
+    { "khq", "Latn" },
+    { "kht", "Mymr" },
+    { "ki", "Latn" },
+    { "kj", "Latn" },
+    { "kjh", "Cyrl" },
+    { "kk", "Cyrl" },
+    { "kk", "Arab" },
+    { "kl", "Latn" },
+    { "kln", "Latn" },
+    { "km", "Khmr" },
+    { "kmb", "Latn" },
+    { "kn", "Knda" },
+    { "ko", "Hang", "Kore" },
+    { "koi", "Cyrl" },
+    { "kok", "Deva" },
+    { "kos", "Latn" },
+    { "kpe", "Latn" },
+    { "kpy", "Cyrl" },
+    { "kr", "Latn" },
+    { "krc", "Cyrl" },
+    { "kri", "Latn" },
+    { "krl", "Cyrl", "Latn" },
+    { "kru", "Deva" },
+    { "ks", "Arab", "Deva" },
+    { "ksb", "Latn" },
+    { "ksf", "Latn" },
+    { "ksh", "Latn" },
+    { "ku", "Arab", "Cyrl", "Latn" },
+    { "kum", "Cyrl" },
+    { "kut", "Latn" },
+    { "kv", "Cyrl", "Latn" },
+    { "kw", "Latn" },
+    { "ky", "Arab", "Cyrl" },
+    { "ky", "Latn" },
+    { "kyu", "Kali" },
+    { "la", "Latn" },
+    { "lad", "Hebr" },
+    { "lag", "Latn" },
+    { "lah", "Arab" },
+    { "lam", "Latn" },
+    { "lb", "Latn" },
+    { "lbe", "Cyrl" },
+    { "lcp", "Thai" },
+    { "lep", "Lepc" },
+    { "lez", "Cyrl" },
+    { "lg", "Latn" },
+    { "li", "Latn" },
+    { "lif", "Deva", "Limb" },
+    { "lis", "Lisu" },
+    { "lki", "Arab" },
+    { "lmn", "Telu" },
+    { "ln", "Latn" },
+    { "lo", "Laoo" },
+    { "lol", "Latn" },
+    { "loz", "Latn" },
+    { "lt", "Latn" },
+    { "lu", "Latn" },
+    { "lua", "Latn" },
+    { "lui", "Latn" },
+    { "lun", "Latn" },
+    { "luo", "Latn" },
+    { "lus", "Beng" },
+    { "lut", "Latn" },
+    { "luy", "Latn" },
+    { "lv", "Latn" },
+    { "lwl", "Thai" },
+    { "mad", "Latn" },
+    { "mag", "Deva" },
+    { "mai", "Deva" },
+    { "mak", "Latn" },
+    { "mak", "Bugi" },
+    { "man", "Latn", "Nkoo" },
+    { "mas", "Latn" },
+    { "mdf", "Cyrl" },
+    { "mdh", "Latn" },
+    { "mdr", "Latn" },
+    { "mdr", "Bugi" },
+    { "men", "Latn" },
+    { "mer", "Latn" },
+    { "mfe", "Latn" },
+    { "mg", "Latn" },
+    { "mgh", "Latn" },
+    { "mh", "Latn" },
+    { "mi", "Latn" },
+    { "mic", "Latn" },
+    { "min", "Latn" },
+    { "mk", "Cyrl" },
+    { "ml", "Mlym" },
+    { "mn", "Cyrl", "Mong" },
+    { "mn", "Phag" },
+    { "mnc", "Mong" },
+    { "mni", "Beng" },
+    { "mni", "Mtei" },
+    { "mnk", "Latn" },
+    { "mns", "Cyrl" },
+    { "mnw", "Mymr" },
+    { "moh", "Latn" },
+    { "mos", "Latn" },
+    { "mr", "Deva" },
+    { "ms", "Latn" },
+    { "ms", "Arab" },
+    { "mt", "Latn" },
+    { "mua", "Latn" },
+    { "mus", "Latn" },
+    { "mwl", "Latn" },
+    { "mwr", "Deva" },
+    { "my", "Mymr" },
+    { "myv", "Cyrl" },
+    { "myz", "Mand" },
+    { "na", "Latn" },
+    { "nap", "Latn" },
+    { "naq", "Latn" },
+    { "nb", "Latn" },
+    { "nd", "Latn" },
+    { "nds", "Latn" },
+    { "ne", "Deva" },
+    { "new", "Deva" },
+    { "ng", "Latn" },
+    { "nia", "Latn" },
+    { "niu", "Latn" },
+    { "nl", "Latn" },
+    { "nmg", "Latn" },
+    { "nn", "Latn" },
+    { "nod", "Lana" },
+    { "nog", "Cyrl" },
+    { "nqo", "Nkoo" },
+    { "nr", "Latn" },
+    { "nso", "Latn" },
+    { "nus", "Latn" },
+    { "nv", "Latn" },
+    { "ny", "Latn" },
+    { "nym", "Latn" },
+    { "nyn", "Latn" },
+    { "nyo", "Latn" },
+    { "nzi", "Latn" },
+    { "oc", "Latn" },
+    { "oj", "Cans" },
+    { "oj", "Latn" },
+    { "om", "Latn" },
+    { "om", "Ethi" },
+    { "or", "Orya" },
+    { "os", "Cyrl", "Latn" },
+    { "osa", "Latn" },
+    { "osc", "Ital", "Latn" },
+    { "otk", "Orkh" },
+    { "pa", "Guru" },
+    { "pa", "Arab" },
+    { "pag", "Latn" },
+    { "pal", "Phli" },
+    { "pam", "Latn" },
+    { "pap", "Latn" },
+    { "pau", "Latn" },
+    { "peo", "Xpeo" },
+    { "phn", "Phnx" },
+    { "pi", "Deva", "Sinh", "Thai" },
+    { "pl", "Latn" },
+    { "pon", "Latn" },
+    { "pra", "Brah", "Khar" },
+    { "prd", "Arab" },
+    { "prg", "Latn" },
+    { "prs", "Arab" },
+    { "ps", "Arab" },
+    { "pt", "Latn" },
+    { "qu", "Latn" },
+    { "raj", "Latn" },
+    { "rap", "Latn" },
+    { "rar", "Latn" },
+    { "rcf", "Latn" },
+    { "rej", "Latn" },
+    { "rej", "Rjng" },
+    { "rjs", "Deva" },
+    { "rkt", "Beng" },
+    { "rm", "Latn" },
+    { "rn", "Latn" },
+    { "ro", "Latn" },
+    { "ro", "Cyrl" },
+    { "rof", "Latn" },
+    { "rom", "Cyrl", "Latn" },
+    { "ru", "Cyrl" },
+    { "rup", "Latn" },
+    { "rw", "Latn" },
+    { "rwk", "Latn" },
+    { "sa", "Deva", "Sinh" },
+    { "sad", "Latn" },
+    { "saf", "Latn" },
+    { "sah", "Cyrl" },
+    { "sam", "Hebr", "Samr" },
+    { "saq", "Latn" },
+    { "sas", "Latn" },
+    { "sat", "Latn" },
+    { "sat", "Beng", "Deva", "Olck", "Orya" },
+    { "saz", "Saur" },
+    { "sbp", "Latn" },
+    { "sc", "Latn" },
+    { "scn", "Latn" },
+    { "sco", "Latn" },
+    { "sd", "Arab", "Deva" },
+    { "sdh", "Arab" },
+    { "se", "Latn" },
+    { "se", "Cyrl" },
+    { "see", "Latn" },
+    { "seh", "Latn" },
+    { "sel", "Cyrl" },
+    { "ses", "Latn" },
+    { "sg", "Latn" },
+    { "sga", "Latn", "Ogam" },
+    { "shi", "Arab" },
+    { "shi", "Tfng" },
+    { "shn", "Mymr" },
+    { "si", "Sinh" },
+    { "sid", "Latn" },
+    { "sk", "Latn" },
+    { "sl", "Latn" },
+    { "sm", "Latn" },
+    { "sma", "Latn" },
+    { "smi", "Latn" },
+    { "smj", "Latn" },
+    { "smn", "Latn" },
+    { "sms", "Latn" },
+    { "sn", "Latn" },
+    { "snk", "Latn" },
+    { "so", "Latn" },
+    { "so", "Arab", "Osma" },
+    { "son", "Latn" },
+    { "sq", "Latn" },
+    { "sr", "Cyrl", "Latn" },
+    { "srn", "Latn" },
+    { "srr", "Latn" },
+    { "ss", "Latn" },
+    { "ssy", "Latn" },
+    { "st", "Latn" },
+    { "su", "Latn" },
+    { "su", "Sund" },
+    { "suk", "Latn" },
+    { "sus", "Latn" },
+    { "sus", "Arab" },
+    { "sv", "Latn" },
+    { "sw", "Latn" },
+    { "swb", "Arab" },
+    { "swb", "Latn" },
+    { "swc", "Latn" },
+    { "syl", "Beng" },
+    { "syl", "Sylo" },
+    { "syr", "Syrc" },
+    { "ta", "Taml" },
+    { "tab", "Cyrl" },
+    { "tbw", "Latn" },
+    { "tbw", "Tagb" },
+    { "tcy", "Knda" },
+    { "tdd", "Tale" },
+    { "te", "Telu" },
+    { "tem", "Latn" },
+    { "teo", "Latn" },
+    { "ter", "Latn" },
+    { "tet", "Latn" },
+    { "tg", "Arab", "Cyrl", "Latn" },
+    { "th", "Thai" },
+    { "ti", "Ethi" },
+    { "tig", "Ethi" },
+    { "tiv", "Latn" },
+    { "tk", "Arab", "Cyrl", "Latn" },
+    { "tkl", "Latn" },
+    { "tli", "Latn" },
+    { "tmh", "Latn" },
+    { "tn", "Latn" },
+    { "to", "Latn" },
+    { "tog", "Latn" },
+    { "tpi", "Latn" },
+    { "tr", "Latn" },
+    { "tr", "Arab" },
+    { "tru", "Latn" },
+    { "tru", "Syrc" },
+    { "trv", "Latn" },
+    { "ts", "Latn" },
+    { "tsg", "Latn" },
+    { "tsi", "Latn" },
+    { "tt", "Cyrl" },
+    { "tts", "Thai" },
+    { "tum", "Latn" },
+    { "tut", "Cyrl" },
+    { "tvl", "Latn" },
+    { "twq", "Latn" },
+    { "ty", "Latn" },
+    { "tyv", "Cyrl" },
+    { "tzm", "Latn", "Tfng" },
+    { "ude", "Cyrl" },
+    { "udm", "Cyrl" },
+    { "udm", "Latn" },
+    { "ug", "Arab" },
+    { "ug", "Cyrl", "Latn" },
+    { "uga", "Ugar" },
+    { "uk", "Cyrl" },
+    { "uli", "Latn" },
+    { "umb", "Latn" },
+    { "unr", "Beng", "Deva" },
+    { "unx", "Beng", "Deva" },
+    { "ur", "Arab" },
+    { "uz", "Arab", "Cyrl", "Latn" },
+    { "vai", "Vaii" },
+    { "ve", "Latn" },
+    { "vi", "Latn" },
+    { "vi", "Hani" },
+    { "vo", "Latn" },
+    { "vot", "Latn" },
+    { "vun", "Latn" },
+    { "wa", "Latn" },
+    { "wae", "Latn" },
+    { "wak", "Latn" },
+    { "wal", "Ethi" },
+    { "war", "Latn" },
+    { "was", "Latn" },
+    { "wo", "Latn" },
+    { "wo", "Arab" },
+    { "xal", "Cyrl" },
+    { "xcr", "Cari" },
+    { "xh", "Latn" },
+    { "xog", "Latn" },
+    { "xpr", "Prti" },
+    { "xsa", "Sarb" },
+    { "xsr", "Deva" },
+    { "xum", "Ital", "Latn" },
+    { "yao", "Latn" },
+    { "yap", "Latn" },
+    { "yav", "Latn" },
+    { "yi", "Hebr" },
+    { "yo", "Latn" },
+    { "yrk", "Cyrl" },
+    { "yue", "Hans" },
+    { "za", "Latn" },
+    { "za", "Hans" },
+    { "zap", "Latn" },
+    { "zen", "Tfng" },
+    { "zh", "Hans", "Hant" },
+    { "zh", "Bopo", "Phag" },
+    { "zu", "Latn" },
+    { "zun", "Latn" },
+    { "zza", "Arab" }, };
+
+  private static Map<String, ScriptTag> fontSpecificScript = new HashMap<String, ScriptTag>();
+  private Map<ScriptTag, Set<String>> scriptLangMap = new HashMap<ScriptTag, Set<String>>();
+  static {
+    fontSpecificScript.put("laoo", ScriptTag.lao);
+    fontSpecificScript.put("yiii", ScriptTag.yi);
+    fontSpecificScript.put("jpan", ScriptTag.kana);
+    fontSpecificScript.put("kore", ScriptTag.hang);
+    fontSpecificScript.put("nkoo", ScriptTag.nko);
+    fontSpecificScript.put("vaii", ScriptTag.vai);
+    fontSpecificScript.put("hans", ScriptTag.hani);
+    fontSpecificScript.put("hant", ScriptTag.hani);
+
+  }
+
+  FontLanguages(List<String> availableLangs) {
+    for (String[] entry : langScriptData) {
+      String lang = entry[0];
+      if (!availableLangs.contains(lang)) {
+        continue;
+      }
+      for (int i = 1; i < entry.length; i++) {
+        String script = entry[i].toLowerCase();
+        ScriptTag scriptTag = fontSpecificScript.containsKey(script) ? fontSpecificScript.get(
+            script)
+            : ScriptTag.valueOf(script);
+            addLangScriptMap(lang, scriptTag);
+      }
+    }
+
+    scriptLangMap.put(ScriptTag.DFLT, new HashSet<String>());
+    scriptLangMap.put(ScriptTag.brai, new HashSet<String>());
+    scriptLangMap.put(ScriptTag.math, new HashSet<String>());
+    scriptLangMap.put(ScriptTag.musc, new HashSet<String>());
+    scriptLangMap.put(ScriptTag.musi, new HashSet<String>());
+    scriptLangMap.put(ScriptTag.mly2, scriptLangMap.get(ScriptTag.mlym));
+    scriptLangMap.put(ScriptTag.mlm2, scriptLangMap.get(ScriptTag.mlym));
+    scriptLangMap.put(ScriptTag.dev2, scriptLangMap.get(ScriptTag.deva));
+    scriptLangMap.put(ScriptTag.mym2, scriptLangMap.get(ScriptTag.mymr));
+    scriptLangMap.put(ScriptTag.tml2, scriptLangMap.get(ScriptTag.taml));
+    scriptLangMap.put(ScriptTag.tel2, scriptLangMap.get(ScriptTag.telu));
+    scriptLangMap.put(ScriptTag.knd2, scriptLangMap.get(ScriptTag.knda));
+    scriptLangMap.put(ScriptTag.gur2, scriptLangMap.get(ScriptTag.guru));
+    scriptLangMap.put(ScriptTag.gjr2, scriptLangMap.get(ScriptTag.gujr));
+    scriptLangMap.put(ScriptTag.bng2, scriptLangMap.get(ScriptTag.beng));
+    scriptLangMap.put(ScriptTag.ory2, scriptLangMap.get(ScriptTag.orya));
+    scriptLangMap.put(ScriptTag.jamo, scriptLangMap.get(ScriptTag.hang));
+  }
+
+  private void addLangScriptMap(String lang, ScriptTag scriptTag) {
+    if (!scriptLangMap.containsKey(scriptTag)) {
+      scriptLangMap.put(scriptTag, new HashSet<String>());
+    }
+    Set<String> langs = scriptLangMap.get(scriptTag);
+    langs.add(lang);
+  }
+
+  Set<String> get(Font font) {
+    Set<String> langs = new HashSet<String>();
+    GSubTable gsub = font.getTable(Tag.GSUB);
+    if (gsub == null) {
+      return langs;
+    }
+
+    ScriptListTable scriptList = gsub.scriptList();
+    for (int i = 0; i < scriptList.count(); i++) {
+      ScriptTag script = scriptList.scriptAt(i);
+      if (scriptLangMap.containsKey(script)) {
+        langs.addAll(scriptLangMap.get(script));
+      } else {
+        System.err.println("No language exists for the script: " + script);
+      }
+    }
+    return langs;
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/testing/FontLoader.java b/java/src/com/google/typography/font/sfntly/table/opentype/testing/FontLoader.java
new file mode 100644
index 0000000..ea13d33
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/testing/FontLoader.java
@@ -0,0 +1,57 @@
+package com.google.typography.font.sfntly.table.opentype.testing;
+
+import com.google.typography.font.sfntly.Font;
+import com.google.typography.font.sfntly.FontFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FontLoader {
+  public static List<File> getFontFiles(String fontDir) {
+    List<File> fontFiles = new ArrayList<File>();
+    getFontFiles(fontFiles, new File(fontDir), "", true);
+    return fontFiles;
+  }
+
+  public static Font getFont(File fontFile) throws IOException {
+    Font[] fonts = load(fontFile);
+    if (fonts == null) {
+      throw new IllegalArgumentException("No font found");
+    }
+    return fonts[0];
+  }
+
+  private static void getFontFiles(
+      List<File> fonts, File dir, String startFrom, boolean foundStart) {
+    File[] files = dir.listFiles();
+    for (File file : files) {
+      if (file.getName().endsWith(".ttf")) {
+        if (foundStart || startFrom.endsWith(file.getName())) {
+          foundStart = true;
+          fonts.add(file);
+        }
+      }
+      if (file.isDirectory()) {
+        getFontFiles(fonts, file, startFrom, foundStart);
+      }
+    }
+  }
+
+  private static Font[] load(File file) throws IOException {
+    FontFactory fontFactory = FontFactory.getInstance();
+    fontFactory.fingerprintFont(true);
+    FileInputStream is = new FileInputStream(file);
+    try {
+      return fontFactory.loadFonts(is);
+    } catch (FileNotFoundException e) {
+      System.err.println("Could not load the font : " + file.getName());
+      return null;
+    } finally {
+      is.close();
+    }
+  }
+}
diff --git a/java/src/com/google/typography/font/sfntly/table/opentype/testing/TestLanguagesForFonts.java b/java/src/com/google/typography/font/sfntly/table/opentype/testing/TestLanguagesForFonts.java
new file mode 100644
index 0000000..73b759f
--- /dev/null
+++ b/java/src/com/google/typography/font/sfntly/table/opentype/testing/TestLanguagesForFonts.java
@@ -0,0 +1,46 @@
+package com.google.typography.font.sfntly.table.opentype.testing;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class TestLanguagesForFonts {
+  private static final String FONTS_ROOT = "/usr/local/google/home/cibu/sfntly/fonts";
+  private static final String WORDS_DIR = "/usr/local/google/home/cibu/sfntly/adv_layout/data/testdata/wiki_words";
+  private static final String OUTPUT_FILE = "/tmp/font-languages.txt";
+
+  private static final FontLanguages fontLanguages = new FontLanguages(availableLangs(WORDS_DIR));
+
+  public static void main(String[] args) throws IOException {
+    List<File> fontFiles = FontLoader.getFontFiles(FONTS_ROOT);
+    PrintWriter writer = new PrintWriter(OUTPUT_FILE);
+    for (File fontFile : fontFiles) {
+      writer.print(fontFile.getPath());
+      Set<String> langs = fontLanguages.get(FontLoader.getFont(fontFile));
+      if (langs.isEmpty()) {
+        langs.add("en");
+      }
+      for (String lang : langs) {
+        writer.print("," + lang);
+      }
+      writer.println();
+    }
+    writer.close();
+  }
+
+  private static List<String> availableLangs(String wordsDir) {
+    List<String> langs = new ArrayList<String>();
+    File[] wordFiles = new File(wordsDir).listFiles();
+    for (File file : wordFiles) {
+      String lang = file.getName();
+      if (lang.startsWith(".")) {
+        continue;
+      }
+      langs.add(lang);
+    }
+    return langs;
+  }
+}
diff --git a/java/src/com/google/typography/font/tools/fontinfo/FontInfo.java b/java/src/com/google/typography/font/tools/fontinfo/FontInfo.java
index ba81b67..4b2ddc0 100644
--- a/java/src/com/google/typography/font/tools/fontinfo/FontInfo.java
+++ b/java/src/com/google/typography/font/tools/fontinfo/FontInfo.java
@@ -4,10 +4,10 @@
 
 import com.google.typography.font.sfntly.Font;
 import com.google.typography.font.sfntly.Font.MacintoshEncodingId;
+import com.google.typography.font.sfntly.Font.PlatformId;
 import com.google.typography.font.sfntly.Font.UnicodeEncodingId;
 import com.google.typography.font.sfntly.Font.WindowsEncodingId;
 import com.google.typography.font.sfntly.Tag;
-import com.google.typography.font.sfntly.Font.PlatformId;
 import com.google.typography.font.sfntly.math.Fixed1616;
 import com.google.typography.font.sfntly.table.Table;
 import com.google.typography.font.sfntly.table.core.CMap;
diff --git a/java/test/com/google/typography/font/sfntly/GPosTests.java b/java/test/com/google/typography/font/sfntly/GPosTests.java
new file mode 100644
index 0000000..8c7df0c
--- /dev/null
+++ b/java/test/com/google/typography/font/sfntly/GPosTests.java
@@ -0,0 +1,48 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.typography.font.sfntly;
+
+import com.google.typography.font.sfntly.table.Header;
+import com.google.typography.font.sfntly.table.Table;
+import com.google.typography.font.sfntly.testutils.TestFont.TestFontNames;
+import com.google.typography.font.sfntly.testutils.TestFontUtils;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author [email protected] (Doug Felt)
+ */
+public class GPosTests extends TestCase {
+  public void testGposFiles() {
+    List<Font> gposFontList = new ArrayList<Font>();
+    for (TestFontNames name : TestFontNames.values()) {
+      Font[] fonts;
+      try {
+        fonts = TestFontUtils.loadFont(name.getFile());
+        assertNotNull(fonts);
+      } catch (IOException e) {
+        System.out.format("caught exception (%s) when loading font %s\n", e.getMessage(), name);
+        continue;
+      }
+      for (int i = 0; i < fonts.length; ++i) {
+        Font font = fonts[i];
+        if (font.hasTable(Tag.GPOS)) {
+          System.out.format("Font %s(%d) has GPOS\n", name, i);
+          gposFontList.add(font);
+
+          Table gpos = font.getTable(Tag.GPOS);
+          Header gposHeader = gpos.header();
+          System.out.println(gposHeader);
+        }
+      }
+    }
+    assertTrue("have test gpos file", gposFontList.size() > 0);
+    
+    for (Font font : gposFontList) {
+    }
+  }
+}
diff --git a/java/test/com/google/typography/font/sfntly/table/opentype/RuleTests.java b/java/test/com/google/typography/font/sfntly/table/opentype/RuleTests.java
new file mode 100644
index 0000000..68b7ec5
--- /dev/null
+++ b/java/test/com/google/typography/font/sfntly/table/opentype/RuleTests.java
@@ -0,0 +1,149 @@
+package com.google.typography.font.sfntly.table.opentype;
+
+import com.google.typography.font.sfntly.Font;
+import com.google.typography.font.sfntly.Tag;
+import com.google.typography.font.sfntly.table.core.CMapTable;
+import com.google.typography.font.sfntly.table.core.PostScriptTable;
+import com.google.typography.font.sfntly.table.opentype.component.GlyphGroup;
+import com.google.typography.font.sfntly.table.opentype.component.Rule;
+import com.google.typography.font.sfntly.table.opentype.testing.FontLoader;
+
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.Set;
+
+/**
+ * Comparison data is generated from Harfbuzz by running:
+ * util/hb-ot-shape-closure --no-glyph-names NotoSansMalayalam.ttf <text>
+ */
+public class RuleTests {
+  private static final String FONTS_DIR = "/usr/local/google/home/cibu/sfntly/fonts";
+  private static final String WORDS_DIR =
+      "/usr/local/google/home/cibu/sfntly/adv_layout/data/testdata/wiki_words";
+  private static final String HB_CLOSURE_DIR =
+      "/usr/local/google/home/cibu/sfntly/adv_layout/data/testdata/wiki_words_hb_closure";
+  private static final int TEST_COUNT = 4000;
+  private static final String DEBUG_SPECIFIC_FONT = "";
+  private static final Map<String, List<String>> LANG_WORDS_MAP = langWordsMap();
+
+  @Test
+  public void allFonts() throws IOException {
+    List<File> fontFiles = FontLoader.getFontFiles(FONTS_DIR);
+    for (File fontFile : fontFiles) {
+      Font font = FontLoader.getFont(fontFile);
+      String name = fontFile.getAbsolutePath();
+      if (DEBUG_SPECIFIC_FONT.length() > 0) {
+        if (!name.contains(DEBUG_SPECIFIC_FONT)) {
+          continue;
+        }
+        Rule.dumpLookups(font);
+      }
+      System.out.println(name);
+
+      Map<Integer, Set<Rule>> glyphRulesMap = Rule.glyphRulesMap(font);
+      if (glyphRulesMap == null) {
+        System.err.println("No GSUB");
+        continue;
+      }
+      CMapTable cmapTable = font.getTable(Tag.cmap);
+
+      String osFontPath = name.substring(name.lastIndexOf('/', name.lastIndexOf('/') - 1) + 1);
+      File[] hbOutFiles = new File(HB_CLOSURE_DIR + '/' + osFontPath).listFiles();
+
+      if (hbOutFiles == null) {
+        System.err.println("No test data");
+        continue;
+      }
+
+      for (File hbOutFile : hbOutFiles) {
+        String lang = hbOutFile.getName();
+        if (lang.startsWith(".")) {
+          continue; // for .svn
+        }
+        List<GlyphGroup> hbClosure = hbClosure(hbOutFile);
+        assertClosure(cmapTable, glyphRulesMap, LANG_WORDS_MAP.get(lang), hbClosure);
+      }
+    }
+  }
+
+  @Test
+  public void aFont() throws IOException {
+    Font font = FontLoader.getFont(new File("/usr/local/google/home/cibu/sfntly/fonts/noto/NotoSansBengali-Regular.ttf"));
+    CMapTable cmap = font.getTable(Tag.cmap);
+    PostScriptTable post = font.getTable(Tag.post);
+    Map<Integer, Set<Rule>> glyphRulesMap = Rule.glyphRulesMap(font);
+    GlyphGroup glyphGroup = Rule.glyphGroupForText("য্রী", cmap);
+    GlyphGroup closure = Rule.closure(glyphRulesMap, glyphGroup);
+    Rule.dumpLookups(font);
+    System.err.println(closure);
+  }
+
+  private static void assertClosure(
+      CMapTable cmap, Map<Integer, Set<Rule>> glyphRulesMap,
+      List<String> words, List<GlyphGroup> expecteds) {
+    for (int i = 0; i < expecteds.size() && i < TEST_COUNT; i++) {
+      String word = words.get(i);
+      GlyphGroup expected = expecteds.get(i);
+
+      GlyphGroup glyphGroup = Rule.glyphGroupForText(word, cmap);
+      GlyphGroup closure = Rule.closure(glyphRulesMap, glyphGroup);
+
+      if (expected.size() == 0 && closure.size() > 0) {
+        System.err.println("Skipped: " + word);
+      } else if (!expected.equals(closure)) {
+        System.err.printf("'%s' failed:\n  %s HB\n  %s Snftly\n\n", word, expected, closure);
+        //Assert.assertEquals(word, expected, closure);
+      }
+    }
+  }
+
+  private static Map<String, List<String>> langWordsMap() {
+    Map<String, List<String>> langWordsMap = new HashMap<String, List<String>>();
+    for (File wordsFile : new File(WORDS_DIR).listFiles()) {
+      String lang = wordsFile.getName();
+      if (lang.startsWith(".")) {
+        continue; // .svn
+      }
+      langWordsMap.put(lang, linesFromFile(wordsFile));
+    }
+    return langWordsMap;
+  }
+
+  private static List<GlyphGroup> hbClosure(File file) {
+    List<GlyphGroup> glyphGroups = new ArrayList<GlyphGroup>();
+    for (String line : linesFromFile(file)) {
+      GlyphGroup glyphGroup = new GlyphGroup();
+      if (line.length() > 0) {
+        for (String intStr : line.split(" ")) {
+          glyphGroup.add(Integer.parseInt(intStr));
+        }
+      }
+      glyphGroups.add(glyphGroup);
+    }
+    return glyphGroups;
+  }
+
+  private static List<String> linesFromFile(File file) {
+    List<String> lines = new ArrayList<String>();
+    Scanner scanner;
+    try {
+      scanner = new Scanner(file);
+    } catch (FileNotFoundException e) {
+      System.err.println("File not found: " + file);
+      return lines;
+    }
+    while (scanner.hasNextLine() && lines.size() < TEST_COUNT) {
+      lines.add(scanner.nextLine());
+    }
+    scanner.close();
+    return lines;
+  }
+}