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;
+ }
+}