/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

/* $Id$ */

package org.apache.fop.complexscripts.fonts;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.fop.complexscripts.scripts.ScriptProcessor;
import org.apache.fop.fonts.truetype.FontFileReader;
import org.apache.fop.fonts.truetype.OFDirTabEntry;
import org.apache.fop.fonts.truetype.OFTableName;
import org.apache.fop.fonts.truetype.OpenFont;

// CSOFF: LineLengthCheck

/**
 * <p>OpenType Font (OTF) advanced typographic table reader. Used by @{Link org.apache.fop.fonts.truetype.TTFFile}
 * to read advanced typographic tables (GDEF, GSUB, GPOS).</p>
 *
 * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
 */
public final class OTFAdvancedTypographicTableReader {

    // logging state
    private static Log log = LogFactory.getLog(OTFAdvancedTypographicTableReader.class);
    // instance state
    private OpenFont otf;                                        // parent font file reader
    private FontFileReader in;                                  // input reader
    private GlyphDefinitionTable gdef;                          // glyph definition table
    private GlyphSubstitutionTable gsub;                        // glyph substitution table
    private GlyphPositioningTable gpos;                         // glyph positioning table
    // transient parsing state
    private transient Map<String, Object> seScripts;      // script-tag         => Object[3] : { default-language-tag, List(language-tag), seLanguages }
    private transient Map<String, Object> seLanguages;    // language-tag       => Object[2] : { "f<required-feature-index>", List("f<feature-index>")
    private transient Map<String, Object> seFeatures;  // "f<feature-index>" => Object[2] : { feature-tag, List("lu<lookup-index>") }
    private transient GlyphMappingTable seMapping;              // subtable entry mappings
    private transient List seEntries;                           // subtable entry entries
    private transient List seSubtables;                         // subtable entry subtables
    private Map<String, ScriptProcessor> processors = new HashMap<String, ScriptProcessor>();

    /**
     * Construct an <code>OTFAdvancedTypographicTableReader</code> instance.
     * @param otf parent font file reader (must be non-null)
     * @param in font file reader (must be non-null)
     */
    public OTFAdvancedTypographicTableReader(OpenFont otf, FontFileReader in) {
        assert otf != null;
        assert in != null;
        this.otf = otf;
        this.in = in;
    }

    /**
     * Read all advanced typographic tables.
     * @throws AdvancedTypographicTableFormatException if ATT table has invalid format
     */
    public void readAll() throws AdvancedTypographicTableFormatException {
        try {
            readGDEF();
            readGSUB();
            readGPOS();
        } catch (AdvancedTypographicTableFormatException e) {
            resetATStateAll();
            throw e;
        } catch (IOException e) {
            resetATStateAll();
            throw new AdvancedTypographicTableFormatException(e.getMessage(), e);
        } finally {
            resetATState();
        }
    }

    /**
     * Determine if advanced (typographic) table is present.
     * @return true if advanced (typographic) table is present
     */
    public boolean hasAdvancedTable() {
        return (gdef != null) || (gsub != null) || (gpos != null);
    }

    /**
     * Returns the GDEF table or null if none present.
     * @return the GDEF table
     */
    public GlyphDefinitionTable getGDEF() {
        return gdef;
    }

    /**
     * Returns the GSUB table or null if none present.
     * @return the GSUB table
     */
    public GlyphSubstitutionTable getGSUB() {
        return gsub;
    }

    /**
     * Returns the GPOS table or null if none present.
     * @return the GPOS table
     */
    public GlyphPositioningTable getGPOS() {
        return gpos;
    }

    private void readLangSysTable(OFTableName tableTag, long langSysTable, String langSysTag)
            throws IOException {
        in.seekSet(langSysTable);
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " lang sys table: " + langSysTag);
        }
        // read lookup order (reorder) table offset
        int lo = in.readTTFUShort();
        // read required feature index
        int rf = in.readTTFUShort();
        String rfi;
        if (rf != 65535) {
            rfi = "f" + rf;
        } else {
            rfi = null;
        }
        // read (non-required) feature count
        int nf = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " lang sys table reorder table: " + lo);
            log.debug(tableTag + " lang sys table required feature index: " + rf);
            log.debug(tableTag + " lang sys table non-required feature count: " + nf);
        }
        // read (non-required) feature indices
        List fl = new java.util.ArrayList();
        for (int i = 0; i < nf; i++) {
            int fi = in.readTTFUShort();
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " lang sys table non-required feature index: " + fi);
            }
            fl.add("f" + fi);
        }
        if (seLanguages == null) {
            seLanguages = new java.util.LinkedHashMap();
        }
        seLanguages.put(langSysTag, new Object[] { rfi, fl });
    }

    private static String defaultTag = "dflt";

    private void readScriptTable(OFTableName tableTag, long scriptTable, String scriptTag) throws IOException {
        in.seekSet(scriptTable);
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " script table: " + scriptTag);
        }
        // read default language system table offset
        int dl = in.readTTFUShort();
        String dt = defaultTag;
        if (dl > 0) {
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " default lang sys tag: " + dt);
                log.debug(tableTag + " default lang sys table offset: " + dl);
            }
        }
        // read language system record count
        int nl = in.readTTFUShort();
        List ll = new java.util.ArrayList();
        if (nl > 0) {
            String[] lta = new String[nl];
            int[] loa = new int[nl];
            // read language system records
            for (int i = 0, n = nl; i < n; i++) {
                String lt = in.readTTFString(4);
                int lo = in.readTTFUShort();
                if (log.isDebugEnabled()) {
                    log.debug(tableTag + " lang sys tag: " + lt);
                    log.debug(tableTag + " lang sys table offset: " + lo);
                }
                lta[i] = lt;
                loa[i] = lo;
                if (dl == lo) {
                    dl = 0;
                    dt = lt;
                }
                ll.add(lt);
            }
            // read non-default language system tables
            for (int i = 0, n = nl; i < n; i++) {
                readLangSysTable(tableTag, scriptTable + loa [ i ], lta [ i ]);
            }
        }
        // read default language system table (if specified)
        if (dl > 0) {
            readLangSysTable(tableTag, scriptTable + dl, dt);
        } else if (dt != null) {
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " lang sys default: " + dt);
            }
        }
        if (seLanguages != null) {
            seScripts.put(scriptTag, new Object[]{dt, ll, seLanguages});
        }
        seLanguages = null;
    }

    private void readScriptList(OFTableName tableTag, long scriptList) throws IOException {
        in.seekSet(scriptList);
        // read script record count
        int ns = in.readTTFUShort();
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " script list record count: " + ns);
        }
        if (ns > 0) {
            String[] sta = new String[ns];
            int[] soa = new int[ns];
            // read script records
            for (int i = 0, n = ns; i < n; i++) {
                String st = in.readTTFString(4);
                int so = in.readTTFUShort();
                if (log.isDebugEnabled()) {
                    log.debug(tableTag + " script tag: " + st);
                    log.debug(tableTag + " script table offset: " + so);
                }
                sta[i] = st;
                soa[i] = so;
            }
            // read script tables
            for (int i = 0, n = ns; i < n; i++) {
                seLanguages = null;
                readScriptTable(tableTag, scriptList + soa [ i ], sta [ i ]);
            }
        }
    }

    private void readFeatureTable(OFTableName tableTag, long featureTable, String featureTag, int featureIndex) throws IOException {
        in.seekSet(featureTable);
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " feature table: " + featureTag);
        }
        // read feature params offset
        int po = in.readTTFUShort();
        // read lookup list indices count
        int nl = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " feature table parameters offset: " + po);
            log.debug(tableTag + " feature table lookup list index count: " + nl);
        }
        // read lookup table indices
        List lul = new java.util.ArrayList();
        for (int i = 0; i < nl; i++) {
            int li = in.readTTFUShort();
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " feature table lookup index: " + li);
            }
            lul.add("lu" + li);
        }
        seFeatures.put("f" + featureIndex, new Object[] { featureTag, lul });
    }

    private void readFeatureList(OFTableName tableTag, long featureList) throws IOException {
        in.seekSet(featureList);
        // read feature record count
        int nf = in.readTTFUShort();
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " feature list record count: " + nf);
        }
        if (nf > 0) {
            String[] fta = new String[nf];
            int[] foa = new int[nf];
            // read feature records
            for (int i = 0, n = nf; i < n; i++) {
                String ft = in.readTTFString(4);
                int fo = in.readTTFUShort();
                if (log.isDebugEnabled()) {
                    log.debug(tableTag + " feature tag: " + ft);
                    log.debug(tableTag + " feature table offset: " + fo);
                }
                fta[i] = ft;
                foa[i] = fo;
            }
            // read feature tables
            for (int i = 0, n = nf; i < n; i++) {
                if (log.isDebugEnabled()) {
                    log.debug(tableTag + " feature index: " + i);
                }
                readFeatureTable(tableTag, featureList + foa [ i ], fta [ i ], i);
            }
        }
    }

    static final class GDEFLookupType {
        static final int GLYPH_CLASS                    = 1;
        static final int ATTACHMENT_POINT               = 2;
        static final int LIGATURE_CARET                 = 3;
        static final int MARK_ATTACHMENT                = 4;
        private GDEFLookupType() {
        }
        public static int getSubtableType(int lt) {
            int st;
            switch (lt) {
            case GDEFLookupType.GLYPH_CLASS:
                st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_GLYPH_CLASS;
                break;
            case GDEFLookupType.ATTACHMENT_POINT:
                st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_ATTACHMENT_POINT;
                break;
            case GDEFLookupType.LIGATURE_CARET:
                st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_LIGATURE_CARET;
                break;
            case GDEFLookupType.MARK_ATTACHMENT:
                st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_MARK_ATTACHMENT;
                break;
            default:
                st = -1;
                break;
            }
            return st;
        }
        public static String toString(int type) {
            String s;
            switch (type) {
            case GLYPH_CLASS:
                s = "GlyphClass";
                break;
            case ATTACHMENT_POINT:
                s = "AttachmentPoint";
                break;
            case LIGATURE_CARET:
                s = "LigatureCaret";
                break;
            case MARK_ATTACHMENT:
                s = "MarkAttachment";
                break;
            default:
                s = "?";
                break;
            }
            return s;
        }
    }

    static final class GSUBLookupType {
        static final int SINGLE                         = 1;
        static final int MULTIPLE                       = 2;
        static final int ALTERNATE                      = 3;
        static final int LIGATURE                       = 4;
        static final int CONTEXTUAL                     = 5;
        static final int CHAINED_CONTEXTUAL             = 6;
        static final int EXTENSION                      = 7;
        static final int REVERSE_CHAINED_SINGLE         = 8;
        private GSUBLookupType() {
        }
        public static int getSubtableType(int lt) {
            int st;
            switch (lt) {
            case GSUBLookupType.SINGLE:
                st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_SINGLE;
                break;
            case GSUBLookupType.MULTIPLE:
                st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_MULTIPLE;
                break;
            case GSUBLookupType.ALTERNATE:
                st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_ALTERNATE;
                break;
            case GSUBLookupType.LIGATURE:
                st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_LIGATURE;
                break;
            case GSUBLookupType.CONTEXTUAL:
                st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CONTEXTUAL;
                break;
            case GSUBLookupType.CHAINED_CONTEXTUAL:
                st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL;
                break;
            case GSUBLookupType.EXTENSION:
                st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION;
                break;
            case GSUBLookupType.REVERSE_CHAINED_SINGLE:
                st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE;
                break;
            default:
                st = -1;
                break;
            }
            return st;
        }
        public static String toString(int type) {
            String s;
            switch (type) {
            case SINGLE:
                s = "Single";
                break;
            case MULTIPLE:
                s = "Multiple";
                break;
            case ALTERNATE:
                s = "Alternate";
                break;
            case LIGATURE:
                s = "Ligature";
                break;
            case CONTEXTUAL:
                s = "Contextual";
                break;
            case CHAINED_CONTEXTUAL:
                s = "ChainedContextual";
                break;
            case EXTENSION:
                s = "Extension";
                break;
            case REVERSE_CHAINED_SINGLE:
                s = "ReverseChainedSingle";
                break;
            default:
                s = "?";
                break;
            }
            return s;
        }
    }

    static final class GPOSLookupType {
        static final int SINGLE                         = 1;
        static final int PAIR                           = 2;
        static final int CURSIVE                        = 3;
        static final int MARK_TO_BASE                   = 4;
        static final int MARK_TO_LIGATURE               = 5;
        static final int MARK_TO_MARK                   = 6;
        static final int CONTEXTUAL                     = 7;
        static final int CHAINED_CONTEXTUAL             = 8;
        static final int EXTENSION                      = 9;
        private GPOSLookupType() {
        }
        public static String toString(int type) {
            String s;
            switch (type) {
            case SINGLE:
                s = "Single";
                break;
            case PAIR:
                s = "Pair";
                break;
            case CURSIVE:
                s = "Cursive";
                break;
            case MARK_TO_BASE:
                s = "MarkToBase";
                break;
            case MARK_TO_LIGATURE:
                s = "MarkToLigature";
                break;
            case MARK_TO_MARK:
                s = "MarkToMark";
                break;
            case CONTEXTUAL:
                s = "Contextual";
                break;
            case CHAINED_CONTEXTUAL:
                s = "ChainedContextual";
                break;
            case EXTENSION:
                s = "Extension";
                break;
            default:
                s = "?";
                break;
            }
            return s;
        }
    }

    static final class LookupFlag {
        static final int RIGHT_TO_LEFT                  = 0x0001;
        static final int IGNORE_BASE_GLYPHS             = 0x0002;
        static final int IGNORE_LIGATURE                = 0x0004;
        static final int IGNORE_MARKS                   = 0x0008;
        static final int USE_MARK_FILTERING_SET         = 0x0010;
        static final int MARK_ATTACHMENT_TYPE           = 0xFF00;
        private LookupFlag() {
        }
        public static String toString(int flags) {
            StringBuffer sb = new StringBuffer();
            boolean first = true;
            if ((flags & RIGHT_TO_LEFT) != 0) {
                if (first) {
                    first = false;
                } else {
                    sb.append('|');
                }
                sb.append("RightToLeft");
            }
            if ((flags & IGNORE_BASE_GLYPHS) != 0) {
                if (first) {
                    first = false;
                } else {
                    sb.append('|');
                }
                sb.append("IgnoreBaseGlyphs");
            }
            if ((flags & IGNORE_LIGATURE) != 0) {
                if (first) {
                    first = false;
                } else {
                    sb.append('|');
                }
                sb.append("IgnoreLigature");
            }
            if ((flags & IGNORE_MARKS) != 0) {
                if (first) {
                    first = false;
                } else {
                    sb.append('|');
                }
                sb.append("IgnoreMarks");
            }
            if ((flags & USE_MARK_FILTERING_SET) != 0) {
                if (first) {
                    first = false;
                } else {
                    sb.append('|');
                }
                sb.append("UseMarkFilteringSet");
            }
            if (sb.length() == 0) {
                sb.append('-');
            }
            return sb.toString();
        }
    }

    private GlyphCoverageTable readCoverageTableFormat1(String label, long tableOffset, int coverageFormat) throws IOException {
        List entries = new java.util.ArrayList();
        in.seekSet(tableOffset);
        // skip over format (already known)
        in.skip(2);
        // read glyph count
        int ng = in.readTTFUShort();
        int[] ga = new int[ng];
        for (int i = 0, n = ng; i < n; i++) {
            int g = in.readTTFUShort();
            ga[i] = g;
            entries.add(g);
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(label + " glyphs: " + toString(ga));
        }
        return GlyphCoverageTable.createCoverageTable(entries);
    }

    private GlyphCoverageTable readCoverageTableFormat2(String label, long tableOffset, int coverageFormat) throws IOException {
        List entries = new java.util.ArrayList();
        in.seekSet(tableOffset);
        // skip over format (already known)
        in.skip(2);
        // read range record count
        int nr = in.readTTFUShort();
        for (int i = 0, n = nr; i < n; i++) {
            // read range start
            int s = in.readTTFUShort();
            // read range end
            int e = in.readTTFUShort();
            // read range coverage (mapping) index
            int m = in.readTTFUShort();
            // dump info if debugging
            if (log.isDebugEnabled()) {
                log.debug(label + " range[" + i + "]: [" + s + "," + e + "]: " + m);
            }
            entries.add(new GlyphCoverageTable.MappingRange(s, e, m));
        }
        return GlyphCoverageTable.createCoverageTable(entries);
    }

    private GlyphCoverageTable readCoverageTable(String label, long tableOffset) throws IOException {
        GlyphCoverageTable gct;
        long cp = in.getCurrentPos();
        in.seekSet(tableOffset);
        // read coverage table format
        int cf = in.readTTFUShort();
        if (cf == 1) {
            gct = readCoverageTableFormat1(label, tableOffset, cf);
        } else if (cf == 2) {
            gct = readCoverageTableFormat2(label, tableOffset, cf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported coverage table format: " + cf);
        }
        in.seekSet(cp);
        return gct;
    }

    private GlyphClassTable readClassDefTableFormat1(String label, long tableOffset, int classFormat) throws IOException {
        List entries = new java.util.ArrayList();
        in.seekSet(tableOffset);
        // skip over format (already known)
        in.skip(2);
        // read start glyph
        int sg = in.readTTFUShort();
        entries.add(sg);
        // read glyph count
        int ng = in.readTTFUShort();
        // read glyph classes
        int[] ca = new int[ng];
        for (int i = 0, n = ng; i < n; i++) {
            int gc = in.readTTFUShort();
            ca[i] = gc;
            entries.add(gc);
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(label + " glyph classes: " + toString(ca));
        }
        return GlyphClassTable.createClassTable(entries);
    }

    private GlyphClassTable readClassDefTableFormat2(String label, long tableOffset, int classFormat) throws IOException {
        List entries = new java.util.ArrayList();
        in.seekSet(tableOffset);
        // skip over format (already known)
        in.skip(2);
        // read range record count
        int nr = in.readTTFUShort();
        for (int i = 0, n = nr; i < n; i++) {
            // read range start
            int s = in.readTTFUShort();
            // read range end
            int e = in.readTTFUShort();
            // read range glyph class (mapping) index
            int m = in.readTTFUShort();
            // dump info if debugging
            if (log.isDebugEnabled()) {
                log.debug(label + " range[" + i + "]: [" + s + "," + e + "]: " + m);
            }
            entries.add(new GlyphClassTable.MappingRange(s, e, m));
        }
        return GlyphClassTable.createClassTable(entries);
    }

    private GlyphClassTable readClassDefTable(String label, long tableOffset) throws IOException {
        GlyphClassTable gct;
        long cp = in.getCurrentPos();
        in.seekSet(tableOffset);
        // read class table format
        int cf = in.readTTFUShort();
        if (cf == 1) {
            gct = readClassDefTableFormat1(label, tableOffset, cf);
        } else if (cf == 2) {
            gct = readClassDefTableFormat2(label, tableOffset, cf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported class definition table format: " + cf);
        }
        in.seekSet(cp);
        return gct;
    }

    private void readSingleSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read delta glyph
        int dg = in.readTTFShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " single substitution subtable format: " + subtableFormat + " (delta)");
            log.debug(tableTag + " single substitution coverage table offset: " + co);
            log.debug(tableTag + " single substitution delta: " + dg);
        }
        // read coverage table
        seMapping = readCoverageTable(tableTag + " single substitution coverage", subtableOffset + co);
        seEntries.add(dg);
    }

    private void readSingleSubTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read glyph count
        int ng = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " single substitution subtable format: " + subtableFormat + " (mapped)");
            log.debug(tableTag + " single substitution coverage table offset: " + co);
            log.debug(tableTag + " single substitution glyph count: " + ng);
        }
        // read coverage table
        seMapping = readCoverageTable(tableTag + " single substitution coverage", subtableOffset + co);
        // read glyph substitutions
        for (int i = 0, n = ng; i < n; i++) {
            int gs = in.readTTFUShort();
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " single substitution glyph[" + i + "]: " + gs);
            }
            seEntries.add(gs);
        }
    }

    private int readSingleSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read substitution subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readSingleSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else if (sf == 2) {
            readSingleSubTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported single substitution subtable format: " + sf);
        }
        return sf;
    }

    private void readMultipleSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read sequence count
        int ns = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " multiple substitution subtable format: " + subtableFormat + " (mapped)");
            log.debug(tableTag + " multiple substitution coverage table offset: " + co);
            log.debug(tableTag + " multiple substitution sequence count: " + ns);
        }
        // read coverage table
        seMapping = readCoverageTable(tableTag + " multiple substitution coverage", subtableOffset + co);
        // read sequence table offsets
        int[] soa = new int[ns];
        for (int i = 0, n = ns; i < n; i++) {
            soa[i] = in.readTTFUShort();
        }
        // read sequence tables
        int[][] gsa = new int [ ns ] [];
        for (int i = 0, n = ns; i < n; i++) {
            int so = soa[i];
            int[] ga;
            if (so > 0) {
                in.seekSet(subtableOffset + so);
                // read glyph count
                int ng = in.readTTFUShort();
                ga = new int[ng];
                for (int j = 0; j < ng; j++) {
                    ga[j] = in.readTTFUShort();
                }
            } else {
                ga = null;
            }
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " multiple substitution sequence[" + i + "]: " + toString(ga));
            }
            gsa [ i ] = ga;
        }
        seEntries.add(gsa);
    }

    private int readMultipleSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read substitution subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readMultipleSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported multiple substitution subtable format: " + sf);
        }
        return sf;
    }

    private void readAlternateSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read alternate set count
        int ns = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " alternate substitution subtable format: " + subtableFormat + " (mapped)");
            log.debug(tableTag + " alternate substitution coverage table offset: " + co);
            log.debug(tableTag + " alternate substitution alternate set count: " + ns);
        }
        // read coverage table
        seMapping = readCoverageTable(tableTag + " alternate substitution coverage", subtableOffset + co);
        // read alternate set table offsets
        int[] soa = new int[ns];
        for (int i = 0, n = ns; i < n; i++) {
            soa[i] = in.readTTFUShort();
        }
        // read alternate set tables
        for (int i = 0, n = ns; i < n; i++) {
            int so = soa[i];
            in.seekSet(subtableOffset + so);
            // read glyph count
            int ng = in.readTTFUShort();
            int[] ga = new int[ng];
            for (int j = 0; j < ng; j++) {
                int gs = in.readTTFUShort();
                ga[j] = gs;
            }
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " alternate substitution alternate set[" + i + "]: " + toString(ga));
            }
            seEntries.add(ga);
        }
    }

    private int readAlternateSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read substitution subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readAlternateSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported alternate substitution subtable format: " + sf);
        }
        return sf;
    }

    private void readLigatureSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read ligature set count
        int ns = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " ligature substitution subtable format: " + subtableFormat + " (mapped)");
            log.debug(tableTag + " ligature substitution coverage table offset: " + co);
            log.debug(tableTag + " ligature substitution ligature set count: " + ns);
        }
        // read coverage table
        seMapping = readCoverageTable(tableTag + " ligature substitution coverage", subtableOffset + co);
        // read ligature set table offsets
        int[] soa = new int[ns];
        for (int i = 0, n = ns; i < n; i++) {
            soa[i] = in.readTTFUShort();
        }
        // read ligature set tables
        for (int i = 0, n = ns; i < n; i++) {
            int so = soa[i];
            in.seekSet(subtableOffset + so);
            // read ligature table count
            int nl = in.readTTFUShort();
            int[] loa = new int[nl];
            for (int j = 0; j < nl; j++) {
                loa[j] = in.readTTFUShort();
            }
            List ligs = new java.util.ArrayList();
            for (int j = 0; j < nl; j++) {
                int lo = loa[j];
                in.seekSet(subtableOffset + so + lo);
                // read ligature glyph id
                int lg = in.readTTFUShort();
                // read ligature (input) component count
                int nc = in.readTTFUShort();
                int[] ca = new int [ nc - 1 ];
                // read ligature (input) component glyph ids
                for (int k = 0; k < nc - 1; k++) {
                    ca[k] = in.readTTFUShort();
                }
                if (log.isDebugEnabled()) {
                    log.debug(tableTag + " ligature substitution ligature set[" + i + "]: ligature(" + lg + "), components: " + toString(ca));
                }
                ligs.add(new GlyphSubstitutionTable.Ligature(lg, ca));
            }
            seEntries.add(new GlyphSubstitutionTable.LigatureSet(ligs));
        }
    }

    private int readLigatureSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read substitution subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readLigatureSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported ligature substitution subtable format: " + sf);
        }
        return sf;
    }

    private GlyphTable.RuleLookup[] readRuleLookups(int numLookups, String header) throws IOException {
        GlyphTable.RuleLookup[] la = new GlyphTable.RuleLookup [ numLookups ];
        for (int i = 0, n = numLookups; i < n; i++) {
            int sequenceIndex = in.readTTFUShort();
            int lookupIndex = in.readTTFUShort();
            la [ i ] = new GlyphTable.RuleLookup(sequenceIndex, lookupIndex);
            // dump info if debugging and header is non-null
            if (log.isDebugEnabled() && (header != null)) {
                log.debug(header + "lookup[" + i + "]: " + la[i]);
            }
        }
        return la;
    }

    private void readContextualSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read rule set count
        int nrs = in.readTTFUShort();
        // read rule set offsets
        int[] rsoa = new int [ nrs ];
        for (int i = 0; i < nrs; i++) {
            rsoa [ i ] = in.readTTFUShort();
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " contextual substitution format: " + subtableFormat + " (glyphs)");
            log.debug(tableTag + " contextual substitution coverage table offset: " + co);
            log.debug(tableTag + " contextual substitution rule set count: " + nrs);
            for (int i = 0; i < nrs; i++) {
                log.debug(tableTag + " contextual substitution rule set offset[" + i + "]: " + rsoa[i]);
            }
        }
        // read coverage table
        GlyphCoverageTable ct;
        if (co > 0) {
            ct = readCoverageTable(tableTag + " contextual substitution coverage", subtableOffset + co);
        } else {
            ct = null;
        }
        // read rule sets
        GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ nrs ];
        String header = null;
        for (int i = 0; i < nrs; i++) {
            GlyphTable.RuleSet rs;
            int rso = rsoa [ i ];
            if (rso > 0) {
                // seek to rule set [ i ]
                in.seekSet(subtableOffset + rso);
                // read rule count
                int nr = in.readTTFUShort();
                // read rule offsets
                int[] roa = new int [ nr ];
                GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ];
                for (int j = 0; j < nr; j++) {
                    roa [ j ] = in.readTTFUShort();
                }
                // read glyph sequence rules
                for (int j = 0; j < nr; j++) {
                    GlyphTable.GlyphSequenceRule r;
                    int ro = roa [ j ];
                    if (ro > 0) {
                        // seek to rule [ j ]
                        in.seekSet(subtableOffset + rso + ro);
                        // read glyph count
                        int ng = in.readTTFUShort();
                        // read rule lookup count
                        int nl = in.readTTFUShort();
                        // read glyphs
                        int[] glyphs = new int [ ng - 1 ];
                        for (int k = 0, nk = glyphs.length; k < nk; k++) {
                            glyphs [ k ] = in.readTTFUShort();
                        }
                        // read rule lookups
                        if (log.isDebugEnabled()) {
                            header = tableTag + " contextual substitution lookups @rule[" + i + "][" + j + "]: ";
                        }
                        GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
                        r = new GlyphTable.GlyphSequenceRule(lookups, ng, glyphs);
                    } else {
                        r = null;
                    }
                    ra [ j ] = r;
                }
                rs = new GlyphTable.HomogeneousRuleSet(ra);
            } else {
                rs = null;
            }
            rsa [ i ] = rs;
        }
        // store results
        seMapping = ct;
        seEntries.add(rsa);
    }

    private void readContextualSubTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read class def table offset
        int cdo = in.readTTFUShort();
        // read class rule set count
        int ngc = in.readTTFUShort();
        // read class rule set offsets
        int[] csoa = new int [ ngc ];
        for (int i = 0; i < ngc; i++) {
            csoa [ i ] = in.readTTFUShort();
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " contextual substitution format: " + subtableFormat + " (glyph classes)");
            log.debug(tableTag + " contextual substitution coverage table offset: " + co);
            log.debug(tableTag + " contextual substitution class set count: " + ngc);
            for (int i = 0; i < ngc; i++) {
                log.debug(tableTag + " contextual substitution class set offset[" + i + "]: " + csoa[i]);
            }
        }
        // read coverage table
        GlyphCoverageTable ct;
        if (co > 0) {
            ct = readCoverageTable(tableTag + " contextual substitution coverage", subtableOffset + co);
        } else {
            ct = null;
        }
        // read class definition table
        GlyphClassTable cdt;
        if (cdo > 0) {
            cdt = readClassDefTable(tableTag + " contextual substitution class definition", subtableOffset + cdo);
        } else {
            cdt = null;
        }
        // read rule sets
        GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ ngc ];
        String header = null;
        for (int i = 0; i < ngc; i++) {
            int cso = csoa [ i ];
            GlyphTable.RuleSet rs;
            if (cso > 0) {
                // seek to rule set [ i ]
                in.seekSet(subtableOffset + cso);
                // read rule count
                int nr = in.readTTFUShort();
                // read rule offsets
                int[] roa = new int [ nr ];
                GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ];
                for (int j = 0; j < nr; j++) {
                    roa [ j ] = in.readTTFUShort();
                }
                // read glyph sequence rules
                for (int j = 0; j < nr; j++) {
                    int ro = roa [ j ];
                    GlyphTable.ClassSequenceRule r;
                    if (ro > 0) {
                        // seek to rule [ j ]
                        in.seekSet(subtableOffset + cso + ro);
                        // read glyph count
                        int ng = in.readTTFUShort();
                        // read rule lookup count
                        int nl = in.readTTFUShort();
                        // read classes
                        int[] classes = new int [ ng - 1 ];
                        for (int k = 0, nk = classes.length; k < nk; k++) {
                            classes [ k ] = in.readTTFUShort();
                        }
                        // read rule lookups
                        if (log.isDebugEnabled()) {
                            header = tableTag + " contextual substitution lookups @rule[" + i + "][" + j + "]: ";
                        }
                        GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
                        r = new GlyphTable.ClassSequenceRule(lookups, ng, classes);
                    } else {
                        assert ro > 0 : "unexpected null subclass rule offset";
                        r = null;
                    }
                    ra [ j ] = r;
                }
                rs = new GlyphTable.HomogeneousRuleSet(ra);
            } else {
                rs = null;
            }
            rsa [ i ] = rs;
        }
        // store results
        seMapping = ct;
        seEntries.add(cdt);
        seEntries.add(ngc);
        seEntries.add(rsa);
    }

    private void readContextualSubTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read glyph (input sequence length) count
        int ng = in.readTTFUShort();
        // read substitution lookup count
        int nl = in.readTTFUShort();
        // read glyph coverage offsets, one per glyph input sequence length count
        int[] gcoa = new int [ ng ];
        for (int i = 0; i < ng; i++) {
            gcoa [ i ] = in.readTTFUShort();
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " contextual substitution format: " + subtableFormat + " (glyph sets)");
            log.debug(tableTag + " contextual substitution glyph input sequence length count: " + ng);
            log.debug(tableTag + " contextual substitution lookup count: " + nl);
            for (int i = 0; i < ng; i++) {
                log.debug(tableTag + " contextual substitution coverage table offset[" + i + "]: " + gcoa[i]);
            }
        }
        // read coverage tables
        GlyphCoverageTable[] gca = new GlyphCoverageTable [ ng ];
        for (int i = 0; i < ng; i++) {
            int gco = gcoa [ i ];
            GlyphCoverageTable gct;
            if (gco > 0) {
                gct = readCoverageTable(tableTag + " contextual substitution coverage[" + i + "]", subtableOffset + gco);
            } else {
                gct = null;
            }
            gca [ i ] = gct;
        }
        // read rule lookups
        String header = null;
        if (log.isDebugEnabled()) {
            header = tableTag + " contextual substitution lookups: ";
        }
        GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
        // construct rule, rule set, and rule set array
        GlyphTable.Rule r = new GlyphTable.CoverageSequenceRule(lookups, ng, gca);
        GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet(new GlyphTable.Rule[] {r});
        GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[] {rs};
        // store results
        assert (gca != null) && (gca.length > 0);
        seMapping = gca[0];
        seEntries.add(rsa);
    }

    private int readContextualSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read substitution subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readContextualSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else if (sf == 2) {
            readContextualSubTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
        } else if (sf == 3) {
            readContextualSubTableFormat3(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported contextual substitution subtable format: " + sf);
        }
        return sf;
    }

    private void readChainedContextualSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read rule set count
        int nrs = in.readTTFUShort();
        // read rule set offsets
        int[] rsoa = new int [ nrs ];
        for (int i = 0; i < nrs; i++) {
            rsoa [ i ] = in.readTTFUShort();
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " chained contextual substitution format: " + subtableFormat + " (glyphs)");
            log.debug(tableTag + " chained contextual substitution coverage table offset: " + co);
            log.debug(tableTag + " chained contextual substitution rule set count: " + nrs);
            for (int i = 0; i < nrs; i++) {
                log.debug(tableTag + " chained contextual substitution rule set offset[" + i + "]: " + rsoa[i]);
            }
        }
        // read coverage table
        GlyphCoverageTable ct;
        if (co > 0) {
            ct = readCoverageTable(tableTag + " chained contextual substitution coverage", subtableOffset + co);
        } else {
            ct = null;
        }
        // read rule sets
        GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ nrs ];
        String header = null;
        for (int i = 0; i < nrs; i++) {
            GlyphTable.RuleSet rs;
            int rso = rsoa [ i ];
            if (rso > 0) {
                // seek to rule set [ i ]
                in.seekSet(subtableOffset + rso);
                // read rule count
                int nr = in.readTTFUShort();
                // read rule offsets
                int[] roa = new int [ nr ];
                GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ];
                for (int j = 0; j < nr; j++) {
                    roa [ j ] = in.readTTFUShort();
                }
                // read glyph sequence rules
                for (int j = 0; j < nr; j++) {
                    GlyphTable.ChainedGlyphSequenceRule r;
                    int ro = roa [ j ];
                    if (ro > 0) {
                        // seek to rule [ j ]
                        in.seekSet(subtableOffset + rso + ro);
                        // read backtrack glyph count
                        int nbg = in.readTTFUShort();
                        // read backtrack glyphs
                        int[] backtrackGlyphs = new int [ nbg ];
                        for (int k = 0, nk = backtrackGlyphs.length; k < nk; k++) {
                            backtrackGlyphs [ k ] = in.readTTFUShort();
                        }
                        // read input glyph count
                        int nig = in.readTTFUShort();
                        // read glyphs
                        int[] glyphs = new int [ nig - 1 ];
                        for (int k = 0, nk = glyphs.length; k < nk; k++) {
                            glyphs [ k ] = in.readTTFUShort();
                        }
                        // read lookahead glyph count
                        int nlg = in.readTTFUShort();
                        // read lookahead glyphs
                        int[] lookaheadGlyphs = new int [ nlg ];
                        for (int k = 0, nk = lookaheadGlyphs.length; k < nk; k++) {
                            lookaheadGlyphs [ k ] = in.readTTFUShort();
                        }
                        // read rule lookup count
                        int nl = in.readTTFUShort();
                        // read rule lookups
                        if (log.isDebugEnabled()) {
                            header = tableTag + " contextual substitution lookups @rule[" + i + "][" + j + "]: ";
                        }
                        GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
                        r = new GlyphTable.ChainedGlyphSequenceRule(lookups, nig, glyphs, backtrackGlyphs, lookaheadGlyphs);
                    } else {
                        r = null;
                    }
                    ra [ j ] = r;
                }
                rs = new GlyphTable.HomogeneousRuleSet(ra);
            } else {
                rs = null;
            }
            rsa [ i ] = rs;
        }
        // store results
        seMapping = ct;
        seEntries.add(rsa);
    }

    private void readChainedContextualSubTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read backtrack class def table offset
        int bcdo = in.readTTFUShort();
        // read input class def table offset
        int icdo = in.readTTFUShort();
        // read lookahead class def table offset
        int lcdo = in.readTTFUShort();
        // read class set count
        int ngc = in.readTTFUShort();
        // read class set offsets
        int[] csoa = new int [ ngc ];
        for (int i = 0; i < ngc; i++) {
            csoa [ i ] = in.readTTFUShort();
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " chained contextual substitution format: " + subtableFormat + " (glyph classes)");
            log.debug(tableTag + " chained contextual substitution coverage table offset: " + co);
            log.debug(tableTag + " chained contextual substitution class set count: " + ngc);
            for (int i = 0; i < ngc; i++) {
                log.debug(tableTag + " chained contextual substitution class set offset[" + i + "]: " + csoa[i]);
            }
        }
        // read coverage table
        GlyphCoverageTable ct;
        if (co > 0) {
            ct = readCoverageTable(tableTag + " chained contextual substitution coverage", subtableOffset + co);
        } else {
            ct = null;
        }
        // read backtrack class definition table
        GlyphClassTable bcdt;
        if (bcdo > 0) {
            bcdt = readClassDefTable(tableTag + " contextual substitution backtrack class definition", subtableOffset + bcdo);
        } else {
            bcdt = null;
        }
        // read input class definition table
        GlyphClassTable icdt;
        if (icdo > 0) {
            icdt = readClassDefTable(tableTag + " contextual substitution input class definition", subtableOffset + icdo);
        } else {
            icdt = null;
        }
        // read lookahead class definition table
        GlyphClassTable lcdt;
        if (lcdo > 0) {
            lcdt = readClassDefTable(tableTag + " contextual substitution lookahead class definition", subtableOffset + lcdo);
        } else {
            lcdt = null;
        }
        // read rule sets
        GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ ngc ];
        String header = null;
        for (int i = 0; i < ngc; i++) {
            int cso = csoa [ i ];
            GlyphTable.RuleSet rs;
            if (cso > 0) {
                // seek to rule set [ i ]
                in.seekSet(subtableOffset + cso);
                // read rule count
                int nr = in.readTTFUShort();
                // read rule offsets
                int[] roa = new int [ nr ];
                GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ];
                for (int j = 0; j < nr; j++) {
                    roa [ j ] = in.readTTFUShort();
                }
                // read glyph sequence rules
                for (int j = 0; j < nr; j++) {
                    int ro = roa [ j ];
                    GlyphTable.ChainedClassSequenceRule r;
                    if (ro > 0) {
                        // seek to rule [ j ]
                        in.seekSet(subtableOffset + cso + ro);
                        // read backtrack glyph class count
                        int nbc = in.readTTFUShort();
                        // read backtrack glyph classes
                        int[] backtrackClasses = new int [ nbc ];
                        for (int k = 0, nk = backtrackClasses.length; k < nk; k++) {
                            backtrackClasses [ k ] = in.readTTFUShort();
                        }
                        // read input glyph class count
                        int nic = in.readTTFUShort();
                        // read input glyph classes
                        int[] classes = new int [ nic - 1 ];
                        for (int k = 0, nk = classes.length; k < nk; k++) {
                            classes [ k ] = in.readTTFUShort();
                        }
                        // read lookahead glyph class count
                        int nlc = in.readTTFUShort();
                        // read lookahead glyph classes
                        int[] lookaheadClasses = new int [ nlc ];
                        for (int k = 0, nk = lookaheadClasses.length; k < nk; k++) {
                            lookaheadClasses [ k ] = in.readTTFUShort();
                        }
                        // read rule lookup count
                        int nl = in.readTTFUShort();
                        // read rule lookups
                        if (log.isDebugEnabled()) {
                            header = tableTag + " contextual substitution lookups @rule[" + i + "][" + j + "]: ";
                        }
                        GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
                        r = new GlyphTable.ChainedClassSequenceRule(lookups, nic, classes, backtrackClasses, lookaheadClasses);
                    } else {
                        r = null;
                    }
                    ra [ j ] = r;
                }
                rs = new GlyphTable.HomogeneousRuleSet(ra);
            } else {
                rs = null;
            }
            rsa [ i ] = rs;
        }
        // store results
        seMapping = ct;
        seEntries.add(icdt);
        seEntries.add(bcdt);
        seEntries.add(lcdt);
        seEntries.add(ngc);
        seEntries.add(rsa);
    }

    private void readChainedContextualSubTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read backtrack glyph count
        int nbg = in.readTTFUShort();
        // read backtrack glyph coverage offsets
        int[] bgcoa = new int [ nbg ];
        for (int i = 0; i < nbg; i++) {
            bgcoa [ i ] = in.readTTFUShort();
        }
        // read input glyph count
        int nig = in.readTTFUShort();
        // read input glyph coverage offsets
        int[] igcoa = new int [ nig ];
        for (int i = 0; i < nig; i++) {
            igcoa [ i ] = in.readTTFUShort();
        }
        // read lookahead glyph count
        int nlg = in.readTTFUShort();
        // read lookahead glyph coverage offsets
        int[] lgcoa = new int [ nlg ];
        for (int i = 0; i < nlg; i++) {
            lgcoa [ i ] = in.readTTFUShort();
        }
        // read substitution lookup count
        int nl = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " chained contextual substitution format: " + subtableFormat + " (glyph sets)");
            log.debug(tableTag + " chained contextual substitution backtrack glyph count: " + nbg);
            for (int i = 0; i < nbg; i++) {
                log.debug(tableTag + " chained contextual substitution backtrack coverage table offset[" + i + "]: " + bgcoa[i]);
            }
            log.debug(tableTag + " chained contextual substitution input glyph count: " + nig);
            for (int i = 0; i < nig; i++) {
                log.debug(tableTag + " chained contextual substitution input coverage table offset[" + i + "]: " + igcoa[i]);
            }
            log.debug(tableTag + " chained contextual substitution lookahead glyph count: " + nlg);
            for (int i = 0; i < nlg; i++) {
                log.debug(tableTag + " chained contextual substitution lookahead coverage table offset[" + i + "]: " + lgcoa[i]);
            }
            log.debug(tableTag + " chained contextual substitution lookup count: " + nl);
        }
        // read backtrack coverage tables
        GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg];
        for (int i = 0; i < nbg; i++) {
            int bgco = bgcoa [ i ];
            GlyphCoverageTable bgct;
            if (bgco > 0) {
                bgct = readCoverageTable(tableTag + " chained contextual substitution backtrack coverage[" + i + "]", subtableOffset + bgco);
            } else {
                bgct = null;
            }
            bgca[i] = bgct;
        }
        // read input coverage tables
        GlyphCoverageTable[] igca = new GlyphCoverageTable[nig];
        for (int i = 0; i < nig; i++) {
            int igco = igcoa [ i ];
            GlyphCoverageTable igct;
            if (igco > 0) {
                igct = readCoverageTable(tableTag + " chained contextual substitution input coverage[" + i + "]", subtableOffset + igco);
            } else {
                igct = null;
            }
            igca[i] = igct;
        }
        // read lookahead coverage tables
        GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg];
        for (int i = 0; i < nlg; i++) {
            int lgco = lgcoa [ i ];
            GlyphCoverageTable lgct;
            if (lgco > 0) {
                lgct = readCoverageTable(tableTag + " chained contextual substitution lookahead coverage[" + i + "]", subtableOffset + lgco);
            } else {
                lgct = null;
            }
            lgca[i] = lgct;
        }
        // read rule lookups
        String header = null;
        if (log.isDebugEnabled()) {
            header = tableTag + " chained contextual substitution lookups: ";
        }
        GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
        // construct rule, rule set, and rule set array
        GlyphTable.Rule r = new GlyphTable.ChainedCoverageSequenceRule(lookups, nig, igca, bgca, lgca);
        GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet(new GlyphTable.Rule[] {r});
        GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[] {rs};
        // store results
        assert (igca != null) && (igca.length > 0);
        seMapping = igca[0];
        seEntries.add(rsa);
    }

    private int readChainedContextualSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read substitution subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readChainedContextualSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else if (sf == 2) {
            readChainedContextualSubTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
        } else if (sf == 3) {
            readChainedContextualSubTableFormat3(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported chained contextual substitution subtable format: " + sf);
        }
        return sf;
    }

    private void readExtensionSubTableFormat1(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read extension lookup type
        int lt = in.readTTFUShort();
        // read extension offset
        long eo = in.readTTFULong();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " extension substitution subtable format: " + subtableFormat);
            log.debug(tableTag + " extension substitution lookup type: " + lt);
            log.debug(tableTag + " extension substitution lookup table offset: " + eo);
        }
        // read referenced subtable from extended offset
        readGSUBSubtable(lt, lookupFlags, lookupSequence, subtableSequence, subtableOffset + eo);
    }

    private int readExtensionSubTable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read substitution subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readExtensionSubTableFormat1(lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported extension substitution subtable format: " + sf);
        }
        return sf;
    }

    private void readReverseChainedSingleSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GSUB";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read backtrack glyph count
        int nbg = in.readTTFUShort();
        // read backtrack glyph coverage offsets
        int[] bgcoa = new int [ nbg ];
        for (int i = 0; i < nbg; i++) {
            bgcoa [ i ] = in.readTTFUShort();
        }
        // read lookahead glyph count
        int nlg = in.readTTFUShort();
        // read backtrack glyph coverage offsets
        int[] lgcoa = new int [ nlg ];
        for (int i = 0; i < nlg; i++) {
            lgcoa [ i ] = in.readTTFUShort();
        }
        // read substitution (output) glyph count
        int ng = in.readTTFUShort();
        // read substitution (output) glyphs
        int[] glyphs = new int [ ng ];
        for (int i = 0, n = ng; i < n; i++) {
            glyphs [ i ] = in.readTTFUShort();
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " reverse chained contextual substitution format: " + subtableFormat);
            log.debug(tableTag + " reverse chained contextual substitution coverage table offset: " + co);
            log.debug(tableTag + " reverse chained contextual substitution backtrack glyph count: " + nbg);
            for (int i = 0; i < nbg; i++) {
                log.debug(tableTag + " reverse chained contextual substitution backtrack coverage table offset[" + i + "]: " + bgcoa[i]);
            }
            log.debug(tableTag + " reverse chained contextual substitution lookahead glyph count: " + nlg);
            for (int i = 0; i < nlg; i++) {
                log.debug(tableTag + " reverse chained contextual substitution lookahead coverage table offset[" + i + "]: " + lgcoa[i]);
            }
            log.debug(tableTag + " reverse chained contextual substitution glyphs: " + toString(glyphs));
        }
        // read coverage table
        GlyphCoverageTable ct = readCoverageTable(tableTag + " reverse chained contextual substitution coverage", subtableOffset + co);
        // read backtrack coverage tables
        GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg];
        for (int i = 0; i < nbg; i++) {
            int bgco = bgcoa[i];
            GlyphCoverageTable bgct;
            if (bgco > 0) {
                bgct = readCoverageTable(tableTag + " reverse chained contextual substitution backtrack coverage[" + i + "]", subtableOffset + bgco);
            } else {
                bgct = null;
            }
            bgca[i] = bgct;
        }
        // read lookahead coverage tables
        GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg];
        for (int i = 0; i < nlg; i++) {
            int lgco = lgcoa[i];
            GlyphCoverageTable lgct;
            if (lgco > 0) {
                lgct = readCoverageTable(tableTag + " reverse chained contextual substitution lookahead coverage[" + i + "]", subtableOffset + lgco);
            } else {
                lgct = null;
            }
            lgca[i] = lgct;
        }
        // store results
        seMapping = ct;
        seEntries.add(bgca);
        seEntries.add(lgca);
        seEntries.add(glyphs);
    }

    private int readReverseChainedSingleSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read substitution subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readReverseChainedSingleSubTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported reverse chained single substitution subtable format: " + sf);
        }
        return sf;
    }

    private void readGSUBSubtable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException {
        initATSubState();
        int subtableFormat = -1;
        switch (lookupType) {
        case GSUBLookupType.SINGLE:
            subtableFormat = readSingleSubTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GSUBLookupType.MULTIPLE:
            subtableFormat = readMultipleSubTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GSUBLookupType.ALTERNATE:
            subtableFormat = readAlternateSubTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GSUBLookupType.LIGATURE:
            subtableFormat = readLigatureSubTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GSUBLookupType.CONTEXTUAL:
            subtableFormat = readContextualSubTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GSUBLookupType.CHAINED_CONTEXTUAL:
            subtableFormat = readChainedContextualSubTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GSUBLookupType.REVERSE_CHAINED_SINGLE:
            subtableFormat = readReverseChainedSingleSubTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GSUBLookupType.EXTENSION:
            subtableFormat = readExtensionSubTable(lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset);
            break;
        default:
            break;
        }
        extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION, lookupType, lookupFlags, lookupSequence, subtableSequence, subtableFormat);
        resetATSubState();
    }

    private GlyphPositioningTable.DeviceTable readPosDeviceTable(long subtableOffset, long deviceTableOffset) throws IOException {
        long cp = in.getCurrentPos();
        in.seekSet(subtableOffset + deviceTableOffset);
        // read start size
        int ss = in.readTTFUShort();
        // read end size
        int es = in.readTTFUShort();
        // read delta format
        int df = in.readTTFUShort();
        int s1;
        int m1;
        int dm;
        int dd;
        int s2;
        if (df == 1) {
            s1 = 14;
            m1 = 0x3;
            dm = 1;
            dd = 4;
            s2 = 2;
        } else if (df == 2) {
            s1 = 12;
            m1 = 0xF;
            dm = 7;
            dd = 16;
            s2 = 4;
        } else if (df == 3) {
            s1 = 8;
            m1 = 0xFF;
            dm = 127;
            dd = 256;
            s2 = 8;
        } else {
            log.debug("unsupported device table delta format: " + df + ", ignoring device table");
            return null;
        }
        // read deltas
        int n = (es - ss) + 1;
        if (n < 0) {
            log.debug("invalid device table delta count: " + n + ", ignoring device table");
            return null;
        }
        int[] da = new int [ n ];
        for (int i = 0; (i < n) && (s2 > 0);) {
            int p = in.readTTFUShort();
            for (int j = 0, k = 16 / s2; j < k; j++) {
                int d = (p >> s1) & m1;
                if (d > dm) {
                    d -= dd;
                }
                if (i < n) {
                    da [ i++ ] = d;
                } else {
                    break;
                }
                p <<= s2;
            }
        }
        in.seekSet(cp);
        return new GlyphPositioningTable.DeviceTable(ss, es, da);
    }

    private GlyphPositioningTable.Value readPosValue(long subtableOffset, int valueFormat) throws IOException {
        // XPlacement
        int xp;
        if ((valueFormat & GlyphPositioningTable.Value.X_PLACEMENT) != 0) {
            xp = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
        } else {
            xp = 0;
        }
        // YPlacement
        int yp;
        if ((valueFormat & GlyphPositioningTable.Value.Y_PLACEMENT) != 0) {
            yp = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
        } else {
            yp = 0;
        }
        // XAdvance
        int xa;
        if ((valueFormat & GlyphPositioningTable.Value.X_ADVANCE) != 0) {
            xa = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
        } else {
            xa = 0;
        }
        // YAdvance
        int ya;
        if ((valueFormat & GlyphPositioningTable.Value.Y_ADVANCE) != 0) {
            ya = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
        } else {
            ya = 0;
        }
        // XPlaDevice
        GlyphPositioningTable.DeviceTable xpd;
        if ((valueFormat & GlyphPositioningTable.Value.X_PLACEMENT_DEVICE) != 0) {
            int xpdo = in.readTTFUShort();
            xpd = readPosDeviceTable(subtableOffset, xpdo);
        } else {
            xpd = null;
        }
        // YPlaDevice
        GlyphPositioningTable.DeviceTable ypd;
        if ((valueFormat & GlyphPositioningTable.Value.Y_PLACEMENT_DEVICE) != 0) {
            int ypdo = in.readTTFUShort();
            ypd = readPosDeviceTable(subtableOffset, ypdo);
        } else {
            ypd = null;
        }
        // XAdvDevice
        GlyphPositioningTable.DeviceTable xad;
        if ((valueFormat & GlyphPositioningTable.Value.X_ADVANCE_DEVICE) != 0) {
            int xado = in.readTTFUShort();
            xad = readPosDeviceTable(subtableOffset, xado);
        } else {
            xad = null;
        }
        // YAdvDevice
        GlyphPositioningTable.DeviceTable yad;
        if ((valueFormat & GlyphPositioningTable.Value.Y_ADVANCE_DEVICE) != 0) {
            int yado = in.readTTFUShort();
            yad = readPosDeviceTable(subtableOffset, yado);
        } else {
            yad = null;
        }
        return new GlyphPositioningTable.Value(xp, yp, xa, ya, xpd, ypd, xad, yad);
    }

    private void readSinglePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read value format
        int vf = in.readTTFUShort();
        // read value
        GlyphPositioningTable.Value v = readPosValue(subtableOffset, vf);
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " single positioning subtable format: " + subtableFormat + " (delta)");
            log.debug(tableTag + " single positioning coverage table offset: " + co);
            log.debug(tableTag + " single positioning value: " + v);
        }
        // read coverage table
        GlyphCoverageTable ct = readCoverageTable(tableTag + " single positioning coverage", subtableOffset + co);
        // store results
        seMapping = ct;
        seEntries.add(v);
    }

    private void readSinglePosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read value format
        int vf = in.readTTFUShort();
        // read value count
        int nv = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " single positioning subtable format: " + subtableFormat + " (mapped)");
            log.debug(tableTag + " single positioning coverage table offset: " + co);
            log.debug(tableTag + " single positioning value count: " + nv);
        }
        // read coverage table
        GlyphCoverageTable ct = readCoverageTable(tableTag + " single positioning coverage", subtableOffset + co);
        // read positioning values
        GlyphPositioningTable.Value[] pva = new GlyphPositioningTable.Value[nv];
        for (int i = 0, n = nv; i < n; i++) {
            GlyphPositioningTable.Value pv = readPosValue(subtableOffset, vf);
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " single positioning value[" + i + "]: " + pv);
            }
            pva[i] = pv;
        }
        // store results
        seMapping = ct;
        seEntries.add(pva);
    }

    private int readSinglePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read positionining subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readSinglePosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else if (sf == 2) {
            readSinglePosTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported single positioning subtable format: " + sf);
        }
        return sf;
    }

    private GlyphPositioningTable.PairValues readPosPairValues(long subtableOffset, boolean hasGlyph, int vf1, int vf2) throws IOException {
        // read glyph (if present)
        int glyph;
        if (hasGlyph) {
            glyph = in.readTTFUShort();
        } else {
            glyph = 0;
        }
        // read first value (if present)
        GlyphPositioningTable.Value v1;
        if (vf1 != 0) {
            v1 = readPosValue(subtableOffset, vf1);
        } else {
            v1 = null;
        }
        // read second value (if present)
        GlyphPositioningTable.Value v2;
        if (vf2 != 0) {
            v2 = readPosValue(subtableOffset, vf2);
        } else {
            v2 = null;
        }
        return new GlyphPositioningTable.PairValues(glyph, v1, v2);
    }

    private GlyphPositioningTable.PairValues[] readPosPairSetTable(long subtableOffset, int pairSetTableOffset, int vf1, int vf2) throws IOException {
        String tableTag = "GPOS";
        long cp = in.getCurrentPos();
        in.seekSet(subtableOffset + pairSetTableOffset);
        // read pair values count
        int npv = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " pair set table offset: " + pairSetTableOffset);
            log.debug(tableTag + " pair set table values count: " + npv);
        }
        // read pair values
        GlyphPositioningTable.PairValues[] pva = new GlyphPositioningTable.PairValues [ npv ];
        for (int i = 0, n = npv; i < n; i++) {
            GlyphPositioningTable.PairValues pv = readPosPairValues(subtableOffset, true, vf1, vf2);
            pva [ i ] = pv;
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " pair set table value[" + i + "]: " + pv);
            }
        }
        in.seekSet(cp);
        return pva;
    }

    private void readPairPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read value format for first glyph
        int vf1 = in.readTTFUShort();
        // read value format for second glyph
        int vf2 = in.readTTFUShort();
        // read number (count) of pair sets
        int nps = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " pair positioning subtable format: " + subtableFormat + " (glyphs)");
            log.debug(tableTag + " pair positioning coverage table offset: " + co);
            log.debug(tableTag + " pair positioning value format #1: " + vf1);
            log.debug(tableTag + " pair positioning value format #2: " + vf2);
        }
        // read coverage table
        GlyphCoverageTable ct = readCoverageTable(tableTag + " pair positioning coverage", subtableOffset + co);
        // read pair value matrix
        GlyphPositioningTable.PairValues[][] pvm = new GlyphPositioningTable.PairValues [ nps ][];
        for (int i = 0, n = nps; i < n; i++) {
            // read pair set offset
            int pso = in.readTTFUShort();
            // read pair set table at offset
            pvm [ i ] = readPosPairSetTable(subtableOffset, pso, vf1, vf2);
        }
        // store results
        seMapping = ct;
        seEntries.add(pvm);
    }

    private void readPairPosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read value format for first glyph
        int vf1 = in.readTTFUShort();
        // read value format for second glyph
        int vf2 = in.readTTFUShort();
        // read class def 1 offset
        int cd1o = in.readTTFUShort();
        // read class def 2 offset
        int cd2o = in.readTTFUShort();
        // read number (count) of classes in class def 1 table
        int nc1 = in.readTTFUShort();
        // read number (count) of classes in class def 2 table
        int nc2 = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " pair positioning subtable format: " + subtableFormat + " (glyph classes)");
            log.debug(tableTag + " pair positioning coverage table offset: " + co);
            log.debug(tableTag + " pair positioning value format #1: " + vf1);
            log.debug(tableTag + " pair positioning value format #2: " + vf2);
            log.debug(tableTag + " pair positioning class def table #1 offset: " + cd1o);
            log.debug(tableTag + " pair positioning class def table #2 offset: " + cd2o);
            log.debug(tableTag + " pair positioning class #1 count: " + nc1);
            log.debug(tableTag + " pair positioning class #2 count: " + nc2);
        }
        // read coverage table
        GlyphCoverageTable ct = readCoverageTable(tableTag + " pair positioning coverage", subtableOffset + co);
        // read class definition table #1
        GlyphClassTable cdt1 = readClassDefTable(tableTag + " pair positioning class definition #1", subtableOffset + cd1o);
        // read class definition table #2
        GlyphClassTable cdt2 = readClassDefTable(tableTag + " pair positioning class definition #2", subtableOffset + cd2o);
        // read pair value matrix
        GlyphPositioningTable.PairValues[][] pvm = new GlyphPositioningTable.PairValues [ nc1 ] [ nc2 ];
        for (int i = 0; i < nc1; i++) {
            for (int j = 0; j < nc2; j++) {
                GlyphPositioningTable.PairValues pv = readPosPairValues(subtableOffset, false, vf1, vf2);
                pvm [ i ] [ j ] = pv;
                if (log.isDebugEnabled()) {
                    log.debug(tableTag + " pair set table value[" + i + "][" + j + "]: " + pv);
                }
            }
        }
        // store results
        seMapping = ct;
        seEntries.add(cdt1);
        seEntries.add(cdt2);
        seEntries.add(nc1);
        seEntries.add(nc2);
        seEntries.add(pvm);
    }

    private int readPairPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read positioning subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readPairPosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else if (sf == 2) {
            readPairPosTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported pair positioning subtable format: " + sf);
        }
        return sf;
    }

    private GlyphPositioningTable.Anchor readPosAnchor(long anchorTableOffset) throws IOException {
        GlyphPositioningTable.Anchor a;
        long cp = in.getCurrentPos();
        in.seekSet(anchorTableOffset);
        // read anchor table format
        int af = in.readTTFUShort();
        if (af == 1) {
            // read x coordinate
            int x = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
            // read y coordinate
            int y = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
            a = new GlyphPositioningTable.Anchor(x, y);
        } else if (af == 2) {
            // read x coordinate
            int x = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
            // read y coordinate
            int y = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
            // read anchor point index
            int ap = in.readTTFUShort();
            a = new GlyphPositioningTable.Anchor(x, y, ap);
        } else if (af == 3) {
            // read x coordinate
            int x = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
            // read y coordinate
            int y = otf.convertTTFUnit2PDFUnit(in.readTTFShort());
            // read x device table offset
            int xdo = in.readTTFUShort();
            // read y device table offset
            int ydo = in.readTTFUShort();
            // read x device table (if present)
            GlyphPositioningTable.DeviceTable xd;
            if (xdo != 0) {
                xd = readPosDeviceTable(cp, xdo);
            } else {
                xd = null;
            }
            // read y device table (if present)
            GlyphPositioningTable.DeviceTable yd;
            if (ydo != 0) {
                yd = readPosDeviceTable(cp, ydo);
            } else {
                yd = null;
            }
            a = new GlyphPositioningTable.Anchor(x, y, xd, yd);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported positioning anchor format: " + af);
        }
        in.seekSet(cp);
        return a;
    }

    private void readCursivePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read entry/exit count
        int ec = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " cursive positioning subtable format: " + subtableFormat);
            log.debug(tableTag + " cursive positioning coverage table offset: " + co);
            log.debug(tableTag + " cursive positioning entry/exit count: " + ec);
        }
        // read coverage table
        GlyphCoverageTable ct = readCoverageTable(tableTag + " cursive positioning coverage", subtableOffset + co);
        // read entry/exit records
        GlyphPositioningTable.Anchor[] aa = new GlyphPositioningTable.Anchor [ ec * 2 ];
        for (int i = 0, n = ec; i < n; i++) {
            // read entry anchor offset
            int eno = in.readTTFUShort();
            // read exit anchor offset
            int exo = in.readTTFUShort();
            // read entry anchor
            GlyphPositioningTable.Anchor ena;
            if (eno > 0) {
                ena = readPosAnchor(subtableOffset + eno);
            } else {
                ena = null;
            }
            // read exit anchor
            GlyphPositioningTable.Anchor exa;
            if (exo > 0) {
                exa = readPosAnchor(subtableOffset + exo);
            } else {
                exa = null;
            }
            aa [ (i * 2) + 0 ] = ena;
            aa [ (i * 2) + 1 ] = exa;
            if (log.isDebugEnabled()) {
                if (ena != null) {
                    log.debug(tableTag + " cursive entry anchor [" + i + "]: " + ena);
                }
                if (exa != null) {
                    log.debug(tableTag + " cursive exit anchor  [" + i + "]: " + exa);
                }
            }
        }
        // store results
        seMapping = ct;
        seEntries.add(aa);
    }

    private int readCursivePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read positioning subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readCursivePosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported cursive positioning subtable format: " + sf);
        }
        return sf;
    }

    private void readMarkToBasePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read mark coverage offset
        int mco = in.readTTFUShort();
        // read base coverage offset
        int bco = in.readTTFUShort();
        // read mark class count
        int nmc = in.readTTFUShort();
        // read mark array offset
        int mao = in.readTTFUShort();
        // read base array offset
        int bao = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " mark-to-base positioning subtable format: " + subtableFormat);
            log.debug(tableTag + " mark-to-base positioning mark coverage table offset: " + mco);
            log.debug(tableTag + " mark-to-base positioning base coverage table offset: " + bco);
            log.debug(tableTag + " mark-to-base positioning mark class count: " + nmc);
            log.debug(tableTag + " mark-to-base positioning mark array offset: " + mao);
            log.debug(tableTag + " mark-to-base positioning base array offset: " + bao);
        }
        // read mark coverage table
        GlyphCoverageTable mct = readCoverageTable(tableTag + " mark-to-base positioning mark coverage", subtableOffset + mco);
        // read base coverage table
        GlyphCoverageTable bct = readCoverageTable(tableTag + " mark-to-base positioning base coverage", subtableOffset + bco);
        // read mark anchor array
        // seek to mark array
        in.seekSet(subtableOffset + mao);
        // read mark count
        int nm = in.readTTFUShort();
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " mark-to-base positioning mark count: " + nm);
        }
        // read mark anchor array, where i:{0...markCount}
        GlyphPositioningTable.MarkAnchor[] maa = new GlyphPositioningTable.MarkAnchor [ nm ];
        for (int i = 0; i < nm; i++) {
            // read mark class
            int mc = in.readTTFUShort();
            // read mark anchor offset
            int ao = in.readTTFUShort();
            GlyphPositioningTable.Anchor a;
            if (ao > 0) {
                a = readPosAnchor(subtableOffset + mao + ao);
            } else {
                a = null;
            }
            GlyphPositioningTable.MarkAnchor ma;
            if (a != null) {
                ma = new GlyphPositioningTable.MarkAnchor(mc, a);
            } else {
                ma = null;
            }
            maa [ i ] = ma;
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " mark-to-base positioning mark anchor[" + i + "]: " + ma);
            }

        }
        // read base anchor matrix
        // seek to base array
        in.seekSet(subtableOffset + bao);
        // read base count
        int nb = in.readTTFUShort();
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " mark-to-base positioning base count: " + nb);
        }
        // read anchor matrix, where i:{0...baseCount - 1}, j:{0...markClassCount - 1}
        GlyphPositioningTable.Anchor[][] bam = new GlyphPositioningTable.Anchor [ nb ] [ nmc ];
        for (int i = 0; i < nb; i++) {
            for (int j = 0; j < nmc; j++) {
                // read base anchor offset
                int ao = in.readTTFUShort();
                GlyphPositioningTable.Anchor a;
                if (ao > 0) {
                    a = readPosAnchor(subtableOffset + bao + ao);
                } else {
                    a = null;
                }
                bam [ i ] [ j ] = a;
                if (log.isDebugEnabled()) {
                    log.debug(tableTag + " mark-to-base positioning base anchor[" + i + "][" + j + "]: " + a);
                }
            }
        }
        // store results
        seMapping = mct;
        seEntries.add(bct);
        seEntries.add(nmc);
        seEntries.add(maa);
        seEntries.add(bam);
    }

    private int readMarkToBasePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read positioning subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readMarkToBasePosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported mark-to-base positioning subtable format: " + sf);
        }
        return sf;
    }

    private void readMarkToLigaturePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read mark coverage offset
        int mco = in.readTTFUShort();
        // read ligature coverage offset
        int lco = in.readTTFUShort();
        // read mark class count
        int nmc = in.readTTFUShort();
        // read mark array offset
        int mao = in.readTTFUShort();
        // read ligature array offset
        int lao = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " mark-to-ligature positioning subtable format: " + subtableFormat);
            log.debug(tableTag + " mark-to-ligature positioning mark coverage table offset: " + mco);
            log.debug(tableTag + " mark-to-ligature positioning ligature coverage table offset: " + lco);
            log.debug(tableTag + " mark-to-ligature positioning mark class count: " + nmc);
            log.debug(tableTag + " mark-to-ligature positioning mark array offset: " + mao);
            log.debug(tableTag + " mark-to-ligature positioning ligature array offset: " + lao);
        }
        // read mark coverage table
        GlyphCoverageTable mct = readCoverageTable(tableTag + " mark-to-ligature positioning mark coverage", subtableOffset + mco);
        // read ligature coverage table
        GlyphCoverageTable lct = readCoverageTable(tableTag + " mark-to-ligature positioning ligature coverage", subtableOffset + lco);
        // read mark anchor array
        // seek to mark array
        in.seekSet(subtableOffset + mao);
        // read mark count
        int nm = in.readTTFUShort();
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " mark-to-ligature positioning mark count: " + nm);
        }
        // read mark anchor array, where i:{0...markCount}
        GlyphPositioningTable.MarkAnchor[] maa = new GlyphPositioningTable.MarkAnchor [ nm ];
        for (int i = 0; i < nm; i++) {
            // read mark class
            int mc = in.readTTFUShort();
            // read mark anchor offset
            int ao = in.readTTFUShort();
            GlyphPositioningTable.Anchor a;
            if (ao > 0) {
                a = readPosAnchor(subtableOffset + mao + ao);
            } else {
                a = null;
            }
            GlyphPositioningTable.MarkAnchor ma;
            if (a != null) {
                ma = new GlyphPositioningTable.MarkAnchor(mc, a);
            } else {
                ma = null;
            }
            maa [ i ] = ma;
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " mark-to-ligature positioning mark anchor[" + i + "]: " + ma);
            }
        }
        // read ligature anchor matrix
        // seek to ligature array
        in.seekSet(subtableOffset + lao);
        // read ligature count
        int nl = in.readTTFUShort();
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " mark-to-ligature positioning ligature count: " + nl);
        }
        // read ligature attach table offsets
        int[] laoa = new int [ nl ];
        for (int i = 0; i < nl; i++) {
            laoa [ i ] = in.readTTFUShort();
        }
        // iterate over ligature attach tables, recording maximum component count
        int mxc = 0;
        for (int i = 0; i < nl; i++) {
            int lato = laoa [ i ];
            in.seekSet(subtableOffset + lao + lato);
            // read component count
            int cc = in.readTTFUShort();
            if (cc > mxc) {
                mxc = cc;
            }
        }
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " mark-to-ligature positioning maximum component count: " + mxc);
        }
        // read anchor matrix, where i:{0...ligatureCount - 1}, j:{0...maxComponentCount - 1}, k:{0...markClassCount - 1}
        GlyphPositioningTable.Anchor[][][] lam = new GlyphPositioningTable.Anchor [ nl ][][];
        for (int i = 0; i < nl; i++) {
            int lato = laoa [ i ];
            // seek to ligature attach table for ligature[i]
            in.seekSet(subtableOffset + lao + lato);
            // read component count
            int cc = in.readTTFUShort();
            GlyphPositioningTable.Anchor[][] lcm = new GlyphPositioningTable.Anchor [ cc ] [ nmc ];
            for (int j = 0; j < cc; j++) {
                for (int k = 0; k < nmc; k++) {
                    // read ligature anchor offset
                    int ao = in.readTTFUShort();
                    GlyphPositioningTable.Anchor a;
                    if (ao > 0) {
                        a  = readPosAnchor(subtableOffset + lao + lato + ao);
                    } else {
                        a = null;
                    }
                    lcm [ j ] [ k ] = a;
                    if (log.isDebugEnabled()) {
                        log.debug(tableTag + " mark-to-ligature positioning ligature anchor[" + i + "][" + j + "][" + k + "]: " + a);
                    }
                }
            }
            lam [ i ] = lcm;
        }
        // store results
        seMapping = mct;
        seEntries.add(lct);
        seEntries.add(nmc);
        seEntries.add(mxc);
        seEntries.add(maa);
        seEntries.add(lam);
    }

    private int readMarkToLigaturePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read positioning subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readMarkToLigaturePosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported mark-to-ligature positioning subtable format: " + sf);
        }
        return sf;
    }

    private void readMarkToMarkPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read mark #1 coverage offset
        int m1co = in.readTTFUShort();
        // read mark #2 coverage offset
        int m2co = in.readTTFUShort();
        // read mark class count
        int nmc = in.readTTFUShort();
        // read mark #1 array offset
        int m1ao = in.readTTFUShort();
        // read mark #2 array offset
        int m2ao = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " mark-to-mark positioning subtable format: " + subtableFormat);
            log.debug(tableTag + " mark-to-mark positioning mark #1 coverage table offset: " + m1co);
            log.debug(tableTag + " mark-to-mark positioning mark #2 coverage table offset: " + m2co);
            log.debug(tableTag + " mark-to-mark positioning mark class count: " + nmc);
            log.debug(tableTag + " mark-to-mark positioning mark #1 array offset: " + m1ao);
            log.debug(tableTag + " mark-to-mark positioning mark #2 array offset: " + m2ao);
        }
        // read mark #1 coverage table
        GlyphCoverageTable mct1 = readCoverageTable(tableTag + " mark-to-mark positioning mark #1 coverage", subtableOffset + m1co);
        // read mark #2 coverage table
        GlyphCoverageTable mct2 = readCoverageTable(tableTag + " mark-to-mark positioning mark #2 coverage", subtableOffset + m2co);
        // read mark #1 anchor array
        // seek to mark array
        in.seekSet(subtableOffset + m1ao);
        // read mark count
        int nm1 = in.readTTFUShort();
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " mark-to-mark positioning mark #1 count: " + nm1);
        }
        // read mark anchor array, where i:{0...mark1Count}
        GlyphPositioningTable.MarkAnchor[] maa = new GlyphPositioningTable.MarkAnchor [ nm1 ];
        for (int i = 0; i < nm1; i++) {
            // read mark class
            int mc = in.readTTFUShort();
            // read mark anchor offset
            int ao = in.readTTFUShort();
            GlyphPositioningTable.Anchor a;
            if (ao > 0) {
                a = readPosAnchor(subtableOffset + m1ao + ao);
            } else {
                a = null;
            }
            GlyphPositioningTable.MarkAnchor ma;
            if (a != null) {
                ma = new GlyphPositioningTable.MarkAnchor(mc, a);
            } else {
                ma = null;
            }
            maa [ i ] = ma;
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " mark-to-mark positioning mark #1 anchor[" + i + "]: " + ma);
            }
        }
        // read mark #2 anchor matrix
        // seek to mark #2 array
        in.seekSet(subtableOffset + m2ao);
        // read mark #2 count
        int nm2 = in.readTTFUShort();
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " mark-to-mark positioning mark #2 count: " + nm2);
        }
        // read anchor matrix, where i:{0...mark2Count - 1}, j:{0...markClassCount - 1}
        GlyphPositioningTable.Anchor[][] mam = new GlyphPositioningTable.Anchor [ nm2 ] [ nmc ];
        for (int i = 0; i < nm2; i++) {
            for (int j = 0; j < nmc; j++) {
                // read mark anchor offset
                int ao = in.readTTFUShort();
                GlyphPositioningTable.Anchor a;
                if (ao > 0) {
                    a = readPosAnchor(subtableOffset + m2ao + ao);
                } else {
                    a = null;
                }
                mam [ i ] [ j ] = a;
                if (log.isDebugEnabled()) {
                    log.debug(tableTag + " mark-to-mark positioning mark #2 anchor[" + i + "][" + j + "]: " + a);
                }
            }
        }
        // store results
        seMapping = mct1;
        seEntries.add(mct2);
        seEntries.add(nmc);
        seEntries.add(maa);
        seEntries.add(mam);
    }

    private int readMarkToMarkPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read positioning subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readMarkToMarkPosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported mark-to-mark positioning subtable format: " + sf);
        }
        return sf;
    }

    private void readContextualPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read rule set count
        int nrs = in.readTTFUShort();
        // read rule set offsets
        int[] rsoa = new int [ nrs ];
        for (int i = 0; i < nrs; i++) {
            rsoa [ i ] = in.readTTFUShort();
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " contextual positioning subtable format: " + subtableFormat + " (glyphs)");
            log.debug(tableTag + " contextual positioning coverage table offset: " + co);
            log.debug(tableTag + " contextual positioning rule set count: " + nrs);
            for (int i = 0; i < nrs; i++) {
                log.debug(tableTag + " contextual positioning rule set offset[" + i + "]: " + rsoa[i]);
            }
        }
        // read coverage table
        GlyphCoverageTable ct;
        if (co > 0) {
            ct = readCoverageTable(tableTag + " contextual positioning coverage", subtableOffset + co);
        } else {
            ct = null;
        }
        // read rule sets
        GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ nrs ];
        String header = null;
        for (int i = 0; i < nrs; i++) {
            GlyphTable.RuleSet rs;
            int rso = rsoa [ i ];
            if (rso > 0) {
                // seek to rule set [ i ]
                in.seekSet(subtableOffset + rso);
                // read rule count
                int nr = in.readTTFUShort();
                // read rule offsets
                int[] roa = new int [ nr ];
                GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ];
                for (int j = 0; j < nr; j++) {
                    roa [ j ] = in.readTTFUShort();
                }
                // read glyph sequence rules
                for (int j = 0; j < nr; j++) {
                    GlyphTable.GlyphSequenceRule r;
                    int ro = roa [ j ];
                    if (ro > 0) {
                        // seek to rule [ j ]
                        in.seekSet(subtableOffset + rso + ro);
                        // read glyph count
                        int ng = in.readTTFUShort();
                        // read rule lookup count
                        int nl = in.readTTFUShort();
                        // read glyphs
                        int[] glyphs = new int [ ng - 1 ];
                        for (int k = 0, nk = glyphs.length; k < nk; k++) {
                            glyphs [ k ] = in.readTTFUShort();
                        }
                        // read rule lookups
                        if (log.isDebugEnabled()) {
                            header = tableTag + " contextual positioning lookups @rule[" + i + "][" + j + "]: ";
                        }
                        GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
                        r = new GlyphTable.GlyphSequenceRule(lookups, ng, glyphs);
                    } else {
                        r = null;
                    }
                    ra [ j ] = r;
                }
                rs = new GlyphTable.HomogeneousRuleSet(ra);
            } else {
                rs = null;
            }
            rsa [ i ] = rs;
        }
        // store results
        seMapping = ct;
        seEntries.add(rsa);
    }

    private void readContextualPosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read class def table offset
        int cdo = in.readTTFUShort();
        // read class rule set count
        int ngc = in.readTTFUShort();
        // read class rule set offsets
        int[] csoa = new int [ ngc ];
        for (int i = 0; i < ngc; i++) {
            csoa [ i ] = in.readTTFUShort();
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " contextual positioning subtable format: " + subtableFormat + " (glyph classes)");
            log.debug(tableTag + " contextual positioning coverage table offset: " + co);
            log.debug(tableTag + " contextual positioning class set count: " + ngc);
            for (int i = 0; i < ngc; i++) {
                log.debug(tableTag + " contextual positioning class set offset[" + i + "]: " + csoa[i]);
            }
        }
        // read coverage table
        GlyphCoverageTable ct;
        if (co > 0) {
            ct = readCoverageTable(tableTag + " contextual positioning coverage", subtableOffset + co);
        } else {
            ct = null;
        }
        // read class definition table
        GlyphClassTable cdt;
        if (cdo > 0) {
            cdt = readClassDefTable(tableTag + " contextual positioning class definition", subtableOffset + cdo);
        } else {
            cdt = null;
        }
        // read rule sets
        GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ ngc ];
        String header = null;
        for (int i = 0; i < ngc; i++) {
            int cso = csoa [ i ];
            GlyphTable.RuleSet rs;
            if (cso > 0) {
                // seek to rule set [ i ]
                in.seekSet(subtableOffset + cso);
                // read rule count
                int nr = in.readTTFUShort();
                // read rule offsets
                int[] roa = new int [ nr ];
                GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ];
                for (int j = 0; j < nr; j++) {
                    roa [ j ] = in.readTTFUShort();
                }
                // read glyph sequence rules
                for (int j = 0; j < nr; j++) {
                    int ro = roa [ j ];
                    GlyphTable.ClassSequenceRule r;
                    if (ro > 0) {
                        // seek to rule [ j ]
                        in.seekSet(subtableOffset + cso + ro);
                        // read glyph count
                        int ng = in.readTTFUShort();
                        // read rule lookup count
                        int nl = in.readTTFUShort();
                        // read classes
                        int[] classes = new int [ ng - 1 ];
                        for (int k = 0, nk = classes.length; k < nk; k++) {
                            classes [ k ] = in.readTTFUShort();
                        }
                        // read rule lookups
                        if (log.isDebugEnabled()) {
                            header = tableTag + " contextual positioning lookups @rule[" + i + "][" + j + "]: ";
                        }
                        GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
                        r = new GlyphTable.ClassSequenceRule(lookups, ng, classes);
                    } else {
                        r = null;
                    }
                    ra [ j ] = r;
                }
                rs = new GlyphTable.HomogeneousRuleSet(ra);
            } else {
                rs = null;
            }
            rsa [ i ] = rs;
        }
        // store results
        seMapping = ct;
        seEntries.add(cdt);
        seEntries.add(ngc);
        seEntries.add(rsa);
    }

    private void readContextualPosTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read glyph (input sequence length) count
        int ng = in.readTTFUShort();
        // read positioning lookup count
        int nl = in.readTTFUShort();
        // read glyph coverage offsets, one per glyph input sequence length count
        int[] gcoa = new int [ ng ];
        for (int i = 0; i < ng; i++) {
            gcoa [ i ] = in.readTTFUShort();
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " contextual positioning subtable format: " + subtableFormat + " (glyph sets)");
            log.debug(tableTag + " contextual positioning glyph input sequence length count: " + ng);
            log.debug(tableTag + " contextual positioning lookup count: " + nl);
            for (int i = 0; i < ng; i++) {
                log.debug(tableTag + " contextual positioning coverage table offset[" + i + "]: " + gcoa[i]);
            }
        }
        // read coverage tables
        GlyphCoverageTable[] gca = new GlyphCoverageTable [ ng ];
        for (int i = 0; i < ng; i++) {
            int gco = gcoa [ i ];
            GlyphCoverageTable gct;
            if (gco > 0) {
                gct = readCoverageTable(tableTag + " contextual positioning coverage[" + i + "]", subtableOffset + gcoa[i]);
            } else {
                gct = null;
            }
            gca [ i ] = gct;
        }
        // read rule lookups
        String header = null;
        if (log.isDebugEnabled()) {
            header = tableTag + " contextual positioning lookups: ";
        }
        GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
        // construct rule, rule set, and rule set array
        GlyphTable.Rule r = new GlyphTable.CoverageSequenceRule(lookups, ng, gca);
        GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet(new GlyphTable.Rule[] {r});
        GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[] {rs};
        // store results
        assert (gca != null) && (gca.length > 0);
        seMapping = gca[0];
        seEntries.add(rsa);
    }

    private int readContextualPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read positioning subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readContextualPosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else if (sf == 2) {
            readContextualPosTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
        } else if (sf == 3) {
            readContextualPosTableFormat3(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported contextual positioning subtable format: " + sf);
        }
        return sf;
    }

    private void readChainedContextualPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read rule set count
        int nrs = in.readTTFUShort();
        // read rule set offsets
        int[] rsoa = new int [ nrs ];
        for (int i = 0; i < nrs; i++) {
            rsoa [ i ] = in.readTTFUShort();
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " chained contextual positioning subtable format: " + subtableFormat + " (glyphs)");
            log.debug(tableTag + " chained contextual positioning coverage table offset: " + co);
            log.debug(tableTag + " chained contextual positioning rule set count: " + nrs);
            for (int i = 0; i < nrs; i++) {
                log.debug(tableTag + " chained contextual positioning rule set offset[" + i + "]: " + rsoa[i]);
            }
        }
        // read coverage table
        GlyphCoverageTable ct;
        if (co > 0) {
            ct = readCoverageTable(tableTag + " chained contextual positioning coverage", subtableOffset + co);
        } else {
            ct = null;
        }
        // read rule sets
        GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ nrs ];
        String header = null;
        for (int i = 0; i < nrs; i++) {
            GlyphTable.RuleSet rs;
            int rso = rsoa [ i ];
            if (rso > 0) {
                // seek to rule set [ i ]
                in.seekSet(subtableOffset + rso);
                // read rule count
                int nr = in.readTTFUShort();
                // read rule offsets
                int[] roa = new int [ nr ];
                GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ];
                for (int j = 0; j < nr; j++) {
                    roa [ j ] = in.readTTFUShort();
                }
                // read glyph sequence rules
                for (int j = 0; j < nr; j++) {
                    GlyphTable.ChainedGlyphSequenceRule r;
                    int ro = roa [ j ];
                    if (ro > 0) {
                        // seek to rule [ j ]
                        in.seekSet(subtableOffset + rso + ro);
                        // read backtrack glyph count
                        int nbg = in.readTTFUShort();
                        // read backtrack glyphs
                        int[] backtrackGlyphs = new int [ nbg ];
                        for (int k = 0, nk = backtrackGlyphs.length; k < nk; k++) {
                            backtrackGlyphs [ k ] = in.readTTFUShort();
                        }
                        // read input glyph count
                        int nig = in.readTTFUShort();
                        // read glyphs
                        int[] glyphs = new int [ nig - 1 ];
                        for (int k = 0, nk = glyphs.length; k < nk; k++) {
                            glyphs [ k ] = in.readTTFUShort();
                        }
                        // read lookahead glyph count
                        int nlg = in.readTTFUShort();
                        // read lookahead glyphs
                        int[] lookaheadGlyphs = new int [ nlg ];
                        for (int k = 0, nk = lookaheadGlyphs.length; k < nk; k++) {
                            lookaheadGlyphs [ k ] = in.readTTFUShort();
                        }
                        // read rule lookup count
                        int nl = in.readTTFUShort();
                        // read rule lookups
                        if (log.isDebugEnabled()) {
                            header = tableTag + " contextual positioning lookups @rule[" + i + "][" + j + "]: ";
                        }
                        GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
                        r = new GlyphTable.ChainedGlyphSequenceRule(lookups, nig, glyphs, backtrackGlyphs, lookaheadGlyphs);
                    } else {
                        r = null;
                    }
                    ra [ j ] = r;
                }
                rs = new GlyphTable.HomogeneousRuleSet(ra);
            } else {
                rs = null;
            }
            rsa [ i ] = rs;
        }
        // store results
        seMapping = ct;
        seEntries.add(rsa);
    }

    private void readChainedContextualPosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read coverage offset
        int co = in.readTTFUShort();
        // read backtrack class def table offset
        int bcdo = in.readTTFUShort();
        // read input class def table offset
        int icdo = in.readTTFUShort();
        // read lookahead class def table offset
        int lcdo = in.readTTFUShort();
        // read class set count
        int ngc = in.readTTFUShort();
        // read class set offsets
        int[] csoa = new int [ ngc ];
        for (int i = 0; i < ngc; i++) {
            csoa [ i ] = in.readTTFUShort();
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " chained contextual positioning subtable format: " + subtableFormat + " (glyph classes)");
            log.debug(tableTag + " chained contextual positioning coverage table offset: " + co);
            log.debug(tableTag + " chained contextual positioning class set count: " + ngc);
            for (int i = 0; i < ngc; i++) {
                log.debug(tableTag + " chained contextual positioning class set offset[" + i + "]: " + csoa[i]);
            }
        }
        // read coverage table
        GlyphCoverageTable ct;
        if (co > 0) {
            ct = readCoverageTable(tableTag + " chained contextual positioning coverage", subtableOffset + co);
        } else {
            ct = null;
        }
        // read backtrack class definition table
        GlyphClassTable bcdt;
        if (bcdo > 0) {
            bcdt = readClassDefTable(tableTag + " contextual positioning backtrack class definition", subtableOffset + bcdo);
        } else {
            bcdt = null;
        }
        // read input class definition table
        GlyphClassTable icdt;
        if (icdo > 0) {
            icdt = readClassDefTable(tableTag + " contextual positioning input class definition", subtableOffset + icdo);
        } else {
            icdt = null;
        }
        // read lookahead class definition table
        GlyphClassTable lcdt;
        if (lcdo > 0) {
            lcdt = readClassDefTable(tableTag + " contextual positioning lookahead class definition", subtableOffset + lcdo);
        } else {
            lcdt = null;
        }
        // read rule sets
        GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ ngc ];
        String header = null;
        for (int i = 0; i < ngc; i++) {
            int cso = csoa [ i ];
            GlyphTable.RuleSet rs;
            if (cso > 0) {
                // seek to rule set [ i ]
                in.seekSet(subtableOffset + cso);
                // read rule count
                int nr = in.readTTFUShort();
                // read rule offsets
                int[] roa = new int [ nr ];
                GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ];
                for (int j = 0; j < nr; j++) {
                    roa [ j ] = in.readTTFUShort();
                }
                // read glyph sequence rules
                for (int j = 0; j < nr; j++) {
                    GlyphTable.ChainedClassSequenceRule r;
                    int ro = roa [ j ];
                    if (ro > 0) {
                        // seek to rule [ j ]
                        in.seekSet(subtableOffset + cso + ro);
                        // read backtrack glyph class count
                        int nbc = in.readTTFUShort();
                        // read backtrack glyph classes
                        int[] backtrackClasses = new int [ nbc ];
                        for (int k = 0, nk = backtrackClasses.length; k < nk; k++) {
                            backtrackClasses [ k ] = in.readTTFUShort();
                        }
                        // read input glyph class count
                        int nic = in.readTTFUShort();
                        // read input glyph classes
                        int[] classes = new int [ nic - 1 ];
                        for (int k = 0, nk = classes.length; k < nk; k++) {
                            classes [ k ] = in.readTTFUShort();
                        }
                        // read lookahead glyph class count
                        int nlc = in.readTTFUShort();
                        // read lookahead glyph classes
                        int[] lookaheadClasses = new int [ nlc ];
                        for (int k = 0, nk = lookaheadClasses.length; k < nk; k++) {
                            lookaheadClasses [ k ] = in.readTTFUShort();
                        }
                        // read rule lookup count
                        int nl = in.readTTFUShort();
                        // read rule lookups
                        if (log.isDebugEnabled()) {
                            header = tableTag + " contextual positioning lookups @rule[" + i + "][" + j + "]: ";
                        }
                        GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
                        r = new GlyphTable.ChainedClassSequenceRule(lookups, nic, classes, backtrackClasses, lookaheadClasses);
                    } else {
                        r = null;
                    }
                    ra [ j ] = r;
                }
                rs = new GlyphTable.HomogeneousRuleSet(ra);
            } else {
                rs = null;
            }
            rsa [ i ] = rs;
        }
        // store results
        seMapping = ct;
        seEntries.add(icdt);
        seEntries.add(bcdt);
        seEntries.add(lcdt);
        seEntries.add(ngc);
        seEntries.add(rsa);
    }

    private void readChainedContextualPosTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read backtrack glyph count
        int nbg = in.readTTFUShort();
        // read backtrack glyph coverage offsets
        int[] bgcoa = new int [ nbg ];
        for (int i = 0; i < nbg; i++) {
            bgcoa [ i ] = in.readTTFUShort();
        }
        // read input glyph count
        int nig = in.readTTFUShort();
        // read backtrack glyph coverage offsets
        int[] igcoa = new int [ nig ];
        for (int i = 0; i < nig; i++) {
            igcoa [ i ] = in.readTTFUShort();
        }
        // read lookahead glyph count
        int nlg = in.readTTFUShort();
        // read backtrack glyph coverage offsets
        int[] lgcoa = new int [ nlg ];
        for (int i = 0; i < nlg; i++) {
            lgcoa [ i ] = in.readTTFUShort();
        }
        // read positioning lookup count
        int nl = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " chained contextual positioning subtable format: " + subtableFormat + " (glyph sets)");
            log.debug(tableTag + " chained contextual positioning backtrack glyph count: " + nbg);
            for (int i = 0; i < nbg; i++) {
                log.debug(tableTag + " chained contextual positioning backtrack coverage table offset[" + i + "]: " + bgcoa[i]);
            }
            log.debug(tableTag + " chained contextual positioning input glyph count: " + nig);
            for (int i = 0; i < nig; i++) {
                log.debug(tableTag + " chained contextual positioning input coverage table offset[" + i + "]: " + igcoa[i]);
            }
            log.debug(tableTag + " chained contextual positioning lookahead glyph count: " + nlg);
            for (int i = 0; i < nlg; i++) {
                log.debug(tableTag + " chained contextual positioning lookahead coverage table offset[" + i + "]: " + lgcoa[i]);
            }
            log.debug(tableTag + " chained contextual positioning lookup count: " + nl);
        }
        // read backtrack coverage tables
        GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg];
        for (int i = 0; i < nbg; i++) {
            int bgco = bgcoa [ i ];
            GlyphCoverageTable bgct;
            if (bgco > 0) {
                bgct = readCoverageTable(tableTag + " chained contextual positioning backtrack coverage[" + i + "]", subtableOffset + bgco);
            } else {
                bgct = null;
            }
            bgca[i] = bgct;
        }
        // read input coverage tables
        GlyphCoverageTable[] igca = new GlyphCoverageTable[nig];
        for (int i = 0; i < nig; i++) {
            int igco = igcoa [ i ];
            GlyphCoverageTable igct;
            if (igco > 0) {
                igct = readCoverageTable(tableTag + " chained contextual positioning input coverage[" + i + "]", subtableOffset + igco);
            } else {
                igct = null;
            }
            igca[i] = igct;
        }
        // read lookahead coverage tables
        GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg];
        for (int i = 0; i < nlg; i++) {
            int lgco = lgcoa [ i ];
            GlyphCoverageTable lgct;
            if (lgco > 0) {
                lgct = readCoverageTable(tableTag + " chained contextual positioning lookahead coverage[" + i + "]", subtableOffset + lgco);
            } else {
                lgct = null;
            }
            lgca[i] = lgct;
        }
        // read rule lookups
        String header = null;
        if (log.isDebugEnabled()) {
            header = tableTag + " chained contextual positioning lookups: ";
        }
        GlyphTable.RuleLookup[] lookups = readRuleLookups(nl, header);
        // construct rule, rule set, and rule set array
        GlyphTable.Rule r = new GlyphTable.ChainedCoverageSequenceRule(lookups, nig, igca, bgca, lgca);
        GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet(new GlyphTable.Rule[] {r});
        GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[] {rs};
        // store results
        assert (igca != null) && (igca.length > 0);
        seMapping = igca[0];
        seEntries.add(rsa);
    }

    private int readChainedContextualPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read positioning subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readChainedContextualPosTableFormat1(lookupType, lookupFlags, subtableOffset, sf);
        } else if (sf == 2) {
            readChainedContextualPosTableFormat2(lookupType, lookupFlags, subtableOffset, sf);
        } else if (sf == 3) {
            readChainedContextualPosTableFormat3(lookupType, lookupFlags, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported chained contextual positioning subtable format: " + sf);
        }
        return sf;
    }

    private void readExtensionPosTableFormat1(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset, int subtableFormat) throws IOException {
        String tableTag = "GPOS";
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read extension lookup type
        int lt = in.readTTFUShort();
        // read extension offset
        long eo = in.readTTFULong();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " extension positioning subtable format: " + subtableFormat);
            log.debug(tableTag + " extension positioning lookup type: " + lt);
            log.debug(tableTag + " extension positioning lookup table offset: " + eo);
        }
        // read referenced subtable from extended offset
        readGPOSSubtable(lt, lookupFlags, lookupSequence, subtableSequence, subtableOffset + eo);
    }

    private int readExtensionPosTable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read positioning subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readExtensionPosTableFormat1(lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported extension positioning subtable format: " + sf);
        }
        return sf;
    }

    private void readGPOSSubtable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException {
        initATSubState();
        int subtableFormat = -1;
        switch (lookupType) {
        case GPOSLookupType.SINGLE:
            subtableFormat = readSinglePosTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GPOSLookupType.PAIR:
            subtableFormat = readPairPosTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GPOSLookupType.CURSIVE:
            subtableFormat = readCursivePosTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GPOSLookupType.MARK_TO_BASE:
            subtableFormat = readMarkToBasePosTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GPOSLookupType.MARK_TO_LIGATURE:
            subtableFormat = readMarkToLigaturePosTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GPOSLookupType.MARK_TO_MARK:
            subtableFormat = readMarkToMarkPosTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GPOSLookupType.CONTEXTUAL:
            subtableFormat = readContextualPosTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GPOSLookupType.CHAINED_CONTEXTUAL:
            subtableFormat = readChainedContextualPosTable(lookupType, lookupFlags, subtableOffset);
            break;
        case GPOSLookupType.EXTENSION:
            subtableFormat = readExtensionPosTable(lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset);
            break;
        default:
            break;
        }
        extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_POSITIONING, lookupType, lookupFlags, lookupSequence, subtableSequence, subtableFormat);
        resetATSubState();
    }

    private void readLookupTable(OFTableName tableTag, int lookupSequence, long lookupTable) throws IOException {
        boolean isGSUB = tableTag.equals(OFTableName.GSUB);
        boolean isGPOS = tableTag.equals(OFTableName.GPOS);
        in.seekSet(lookupTable);
        // read lookup type
        int lt = in.readTTFUShort();
        // read lookup flags
        int lf = in.readTTFUShort();
        // read sub-table count
        int ns = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            String lts;
            if (isGSUB) {
                lts = GSUBLookupType.toString(lt);
            } else if (isGPOS) {
                lts = GPOSLookupType.toString(lt);
            } else {
                lts = "?";
            }
            log.debug(tableTag + " lookup table type: " + lt + " (" + lts + ")");
            log.debug(tableTag + " lookup table flags: " + lf + " (" + LookupFlag.toString(lf) + ")");
            log.debug(tableTag + " lookup table subtable count: " + ns);
        }
        // read subtable offsets
        int[] soa = new int[ns];
        for (int i = 0; i < ns; i++) {
            int so = in.readTTFUShort();
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " lookup table subtable offset: " + so);
            }
            soa[i] = so;
        }
        // read mark filtering set
        if ((lf & LookupFlag.USE_MARK_FILTERING_SET) != 0) {
            // read mark filtering set
            int fs = in.readTTFUShort();
            // dump info if debugging
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " lookup table mark filter set: " + fs);
            }
        }
        // read subtables
        for (int i = 0; i < ns; i++) {
            int so = soa[i];
            if (isGSUB) {
                readGSUBSubtable(lt, lf, lookupSequence, i, lookupTable + so);
            } else if (isGPOS) {
                readGPOSSubtable(lt, lf, lookupSequence, i, lookupTable + so);
            }
        }
    }

    private void readLookupList(OFTableName tableTag, long lookupList) throws IOException {
        in.seekSet(lookupList);
        // read lookup record count
        int nl = in.readTTFUShort();
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " lookup list record count: " + nl);
        }
        if (nl > 0) {
            int[] loa = new int[nl];
            // read lookup records
            for (int i = 0, n = nl; i < n; i++) {
                int lo = in.readTTFUShort();
                if (log.isDebugEnabled()) {
                    log.debug(tableTag + " lookup table offset: " + lo);
                }
                loa[i] = lo;
            }
            // read lookup tables
            for (int i = 0, n = nl; i < n; i++) {
                if (log.isDebugEnabled()) {
                    log.debug(tableTag + " lookup index: " + i);
                }
                readLookupTable(tableTag, i, lookupList + loa [ i ]);
            }
        }
    }

    /**
     * Read the common layout tables (used by GSUB and GPOS).
     * @param tableTag tag of table being read
     * @param scriptList offset to script list from beginning of font file
     * @param featureList offset to feature list from beginning of font file
     * @param lookupList offset to lookup list from beginning of font file
     * @throws IOException In case of a I/O problem
     */
    private void readCommonLayoutTables(OFTableName tableTag, long scriptList, long featureList, long lookupList) throws IOException {
        if (scriptList > 0) {
            readScriptList(tableTag, scriptList);
        }
        if (featureList > 0) {
            readFeatureList(tableTag, featureList);
        }
        if (lookupList > 0) {
            readLookupList(tableTag, lookupList);
        }
    }

    private void readGDEFClassDefTable(OFTableName tableTag, int lookupSequence, long subtableOffset) throws IOException {
        initATSubState();
        in.seekSet(subtableOffset);
        // subtable is a bare class definition table
        GlyphClassTable ct = readClassDefTable(tableTag + " glyph class definition table", subtableOffset);
        // store results
        seMapping = ct;
        // extract subtable
        extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.GLYPH_CLASS, 0, lookupSequence, 0, 1);
        resetATSubState();
    }

    private void readGDEFAttachmentTable(OFTableName tableTag, int lookupSequence, long subtableOffset) throws IOException {
        initATSubState();
        in.seekSet(subtableOffset);
        // read coverage offset
        int co = in.readTTFUShort();
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " attachment point coverage table offset: " + co);
        }
        // read coverage table
        GlyphCoverageTable ct = readCoverageTable(tableTag + " attachment point coverage", subtableOffset + co);
        // store results
        seMapping = ct;
        // extract subtable
        extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.ATTACHMENT_POINT, 0, lookupSequence, 0, 1);
        resetATSubState();
    }

    private void readGDEFLigatureCaretTable(OFTableName tableTag, int lookupSequence, long subtableOffset) throws IOException {
        initATSubState();
        in.seekSet(subtableOffset);
        // read coverage offset
        int co = in.readTTFUShort();
        // read ligature glyph count
        int nl = in.readTTFUShort();
        // read ligature glyph table offsets
        int[] lgto = new int [ nl ];
        for (int i = 0; i < nl; i++) {
            lgto [ i ] = in.readTTFUShort();
        }

        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " ligature caret coverage table offset: " + co);
            log.debug(tableTag + " ligature caret ligature glyph count: " + nl);
            for (int i = 0; i < nl; i++) {
                log.debug(tableTag + " ligature glyph table offset[" + i + "]: " + lgto[i]);
            }
        }
        // read coverage table
        GlyphCoverageTable ct = readCoverageTable(tableTag + " ligature caret coverage", subtableOffset + co);
        // store results
        seMapping = ct;
        // extract subtable
        extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.LIGATURE_CARET, 0, lookupSequence, 0, 1);
        resetATSubState();
    }

    private void readGDEFMarkAttachmentTable(OFTableName tableTag, int lookupSequence, long subtableOffset) throws IOException {
        initATSubState();
        in.seekSet(subtableOffset);
        // subtable is a bare class definition table
        GlyphClassTable ct = readClassDefTable(tableTag + " glyph class definition table", subtableOffset);
        // store results
        seMapping = ct;
        // extract subtable
        extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.MARK_ATTACHMENT, 0, lookupSequence, 0, 1);
        resetATSubState();
    }

    private void readGDEFMarkGlyphsTableFormat1(OFTableName tableTag, int lookupSequence, long subtableOffset, int subtableFormat) throws IOException {
        initATSubState();
        in.seekSet(subtableOffset);
        // skip over format (already known)
        in.skip(2);
        // read mark set class count
        int nmc = in.readTTFUShort();
        long[] mso = new long [ nmc ];
        // read mark set coverage offsets
        for (int i = 0; i < nmc; i++) {
            mso [ i ] = in.readTTFULong();
        }
        // dump info if debugging
        if (log.isDebugEnabled()) {
            log.debug(tableTag + " mark set subtable format: " + subtableFormat + " (glyph sets)");
            log.debug(tableTag + " mark set class count: " + nmc);
            for (int i = 0; i < nmc; i++) {
                log.debug(tableTag + " mark set coverage table offset[" + i + "]: " + mso[i]);
            }
        }
        // read mark set coverage tables, one per class
        GlyphCoverageTable[] msca = new GlyphCoverageTable[nmc];
        for (int i = 0; i < nmc; i++) {
            msca[i] = readCoverageTable(tableTag + " mark set coverage[" + i + "]", subtableOffset + mso[i]);
        }
        // create combined class table from per-class coverage tables
        GlyphClassTable ct = GlyphClassTable.createClassTable(Arrays.asList(msca));
        // store results
        seMapping = ct;
        // extract subtable
        extractSESubState(GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.MARK_ATTACHMENT, 0, lookupSequence, 0, 1);
        resetATSubState();
    }

    private void readGDEFMarkGlyphsTable(OFTableName tableTag, int lookupSequence, long subtableOffset) throws IOException {
        in.seekSet(subtableOffset);
        // read mark set subtable format
        int sf = in.readTTFUShort();
        if (sf == 1) {
            readGDEFMarkGlyphsTableFormat1(tableTag, lookupSequence, subtableOffset, sf);
        } else {
            throw new AdvancedTypographicTableFormatException("unsupported mark glyph sets subtable format: " + sf);
        }
    }

    /**
     * Read the GDEF table.
     * @throws IOException In case of a I/O problem
     */
    private void readGDEF() throws IOException {
        OFTableName tableTag = OFTableName.GDEF;
        // Initialize temporary state
        initATState();
        // Read glyph definition (GDEF) table
        OFDirTabEntry dirTab = otf.getDirectoryEntry(tableTag);
        if (gdef != null) {
            if (log.isDebugEnabled()) {
                log.debug(tableTag + ": ignoring duplicate table");
            }
        } else if (dirTab != null) {
            otf.seekTab(in, tableTag, 0);
            long version = in.readTTFULong();
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " version: " + (version / 65536) + "." + (version % 65536));
            }
            // glyph class definition table offset (may be null)
            int cdo = in.readTTFUShort();
            // attach point list offset (may be null)
            int apo = in.readTTFUShort();
            // ligature caret list offset (may be null)
            int lco = in.readTTFUShort();
            // mark attach class definition table offset (may be null)
            int mao = in.readTTFUShort();
            // mark glyph sets definition table offset (may be null)
            int mgo;
            if (version >= 0x00010002) {
                mgo = in.readTTFUShort();
            } else {
                mgo = 0;
            }
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " glyph class definition table offset: " + cdo);
                log.debug(tableTag + " attachment point list offset: " + apo);
                log.debug(tableTag + " ligature caret list offset: " + lco);
                log.debug(tableTag + " mark attachment class definition table offset: " + mao);
                log.debug(tableTag + " mark glyph set definitions table offset: " + mgo);
            }
            // initialize subtable sequence number
            int seqno = 0;
            // obtain offset to start of gdef table
            long to = dirTab.getOffset();
            // (optionally) read glyph class definition subtable
            if (cdo != 0) {
                readGDEFClassDefTable(tableTag, seqno++, to + cdo);
            }
            // (optionally) read glyph attachment point subtable
            if (apo != 0) {
                readGDEFAttachmentTable(tableTag, seqno++, to + apo);
            }
            // (optionally) read ligature caret subtable
            if (lco != 0) {
                readGDEFLigatureCaretTable(tableTag, seqno++, to + lco);
            }
            // (optionally) read mark attachment class subtable
            if (mao != 0) {
                readGDEFMarkAttachmentTable(tableTag, seqno++, to + mao);
            }
            // (optionally) read mark glyph sets subtable
            if (mgo != 0) {
                readGDEFMarkGlyphsTable(tableTag, seqno++, to + mgo);
            }
            GlyphDefinitionTable gdef;
            if ((gdef = constructGDEF()) != null) {
                this.gdef = gdef;
            }
        }
    }

    /**
     * Read the GSUB table.
     * @throws IOException In case of a I/O problem
     */
    private void readGSUB() throws IOException {
        OFTableName tableTag = OFTableName.GSUB;
        // Initialize temporary state
        initATState();
        // Read glyph substitution (GSUB) table
        OFDirTabEntry dirTab = otf.getDirectoryEntry(tableTag);
        if (gpos != null) {
            if (log.isDebugEnabled()) {
                log.debug(tableTag + ": ignoring duplicate table");
            }
        } else if (dirTab != null) {
            otf.seekTab(in, tableTag, 0);
            int version = in.readTTFLong();
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " version: " + (version / 65536) + "." + (version % 65536));
            }
            int slo = in.readTTFUShort();
            int flo = in.readTTFUShort();
            int llo = in.readTTFUShort();
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " script list offset: " + slo);
                log.debug(tableTag + " feature list offset: " + flo);
                log.debug(tableTag + " lookup list offset: " + llo);
            }
            long to = dirTab.getOffset();
            readCommonLayoutTables(tableTag, to + slo, to + flo, to + llo);
            GlyphSubstitutionTable gsub;
            if ((gsub = constructGSUB()) != null) {
                this.gsub = gsub;
            }
        }
    }

    /**
     * Read the GPOS table.
     * @throws IOException In case of a I/O problem
     */
    private void readGPOS() throws IOException {
        OFTableName tableTag = OFTableName.GPOS;
        // Initialize temporary state
        initATState();
        // Read glyph positioning (GPOS) table
        OFDirTabEntry dirTab = otf.getDirectoryEntry(tableTag);
        if (gpos != null) {
            if (log.isDebugEnabled()) {
                log.debug(tableTag + ": ignoring duplicate table");
            }
        } else if (dirTab != null) {
            otf.seekTab(in, tableTag, 0);
            int version = in.readTTFLong();
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " version: " + (version / 65536) + "." + (version % 65536));
            }
            int slo = in.readTTFUShort();
            int flo = in.readTTFUShort();
            int llo = in.readTTFUShort();
            if (log.isDebugEnabled()) {
                log.debug(tableTag + " script list offset: " + slo);
                log.debug(tableTag + " feature list offset: " + flo);
                log.debug(tableTag + " lookup list offset: " + llo);
            }
            long to = dirTab.getOffset();
            readCommonLayoutTables(tableTag, to + slo, to + flo, to + llo);
            GlyphPositioningTable gpos;
            if ((gpos = constructGPOS()) != null) {
                this.gpos = gpos;
            }
        }
    }

    /**
     * Construct the (internal representation of the) GDEF table based on previously
     * parsed state.
     * @returns glyph definition table or null if insufficient or invalid state
     */
    private GlyphDefinitionTable constructGDEF() {
        GlyphDefinitionTable gdef = null;
        List subtables;
        if ((subtables = constructGDEFSubtables()) != null) {
            if (subtables.size() > 0) {
                gdef = new GlyphDefinitionTable(subtables, processors);
            }
        }
        resetATState();
        return gdef;
    }

    /**
     * Construct the (internal representation of the) GSUB table based on previously
     * parsed state.
     * @returns glyph substitution table or null if insufficient or invalid state
     */
    private GlyphSubstitutionTable constructGSUB() {
        GlyphSubstitutionTable gsub = null;
        Map lookups;
        if ((lookups = constructLookups()) != null) {
            List subtables;
            if ((subtables = constructGSUBSubtables()) != null) {
                if ((lookups.size() > 0) && (subtables.size() > 0)) {
                    gsub = new GlyphSubstitutionTable(gdef, lookups, subtables, processors);
                }
            }
        }
        resetATState();
        return gsub;
    }

    /**
     * Construct the (internal representation of the) GPOS table based on previously
     * parsed state.
     * @returns glyph positioning table or null if insufficient or invalid state
     */
    private GlyphPositioningTable constructGPOS() {
        GlyphPositioningTable gpos = null;
        Map lookups;
        if ((lookups = constructLookups()) != null) {
            List subtables;
            if ((subtables = constructGPOSSubtables()) != null) {
                if ((lookups.size() > 0) && (subtables.size() > 0)) {
                    gpos = new GlyphPositioningTable(gdef, lookups, subtables, processors);
                }
            }
        }
        resetATState();
        return gpos;
    }

    private void constructLookupsFeature(Map lookups, String st, String lt, String fid) {
        Object[] fp = (Object[]) seFeatures.get(fid);
        if (fp != null) {
            assert fp.length == 2;
            String ft = (String) fp[0];                 // feature tag
            List<String> lul = (List) fp[1];        // list of lookup table ids
            if ((ft != null) && (lul != null) && (lul.size() > 0)) {
                GlyphTable.LookupSpec ls = new GlyphTable.LookupSpec(st, lt, ft);
                lookups.put(ls, lul);
            }
        }
    }

    private void constructLookupsFeatures(Map lookups, String st, String lt, List<String> fids) {
        for (Object fid1 : fids) {
            String fid = (String) fid1;
            constructLookupsFeature(lookups, st, lt, fid);
        }
    }

    private void constructLookupsLanguage(Map lookups, String st, String lt, Map<String, Object> languages) {
        Object[] lp = (Object[]) languages.get(lt);
        if (lp != null) {
            assert lp.length == 2;
            if (lp[0] != null) {                      // required feature id
                constructLookupsFeature(lookups, st, lt, (String) lp[0]);
            }
            if (lp[1] != null) {                      // non-required features ids
                constructLookupsFeatures(lookups, st, lt, (List) lp[1]);
            }
        }
    }

    private void constructLookupsLanguages(Map lookups, String st, List<String> ll, Map<String, Object> languages) {
        for (Object aLl : ll) {
            String lt = (String) aLl;
            constructLookupsLanguage(lookups, st, lt, languages);
        }
    }

    private Map constructLookups() {
        Map<GlyphTable.LookupSpec, List<String>> lookups = new java.util.LinkedHashMap();
        for (Object o : seScripts.keySet()) {
            String st = (String) o;
            Object[] sp = (Object[]) seScripts.get(st);
            if (sp != null) {
                assert sp.length == 3;
                Map<String, Object> languages = (Map) sp[2];
                if (sp[0] != null) {                  // default language
                    constructLookupsLanguage(lookups, st, (String) sp[0], languages);
                }
                if (sp[1] != null) {                  // non-default languages
                    constructLookupsLanguages(lookups, st, (List) sp[1], languages);
                }
            }
        }
        return lookups;
    }

    private List constructGDEFSubtables() {
        List<GlyphSubtable> subtables = new java.util.ArrayList();
        if (seSubtables != null) {
            for (Object seSubtable : seSubtables) {
                Object[] stp = (Object[]) seSubtable;
                GlyphSubtable st;
                if ((st = constructGDEFSubtable(stp)) != null) {
                    subtables.add(st);
                }
            }
        }
        return subtables;
    }

    private GlyphSubtable constructGDEFSubtable(Object[] stp) {
        GlyphSubtable st = null;
        assert (stp != null) && (stp.length == 8);
        Integer tt = (Integer) stp[0];          // table type
        Integer lt = (Integer) stp[1];          // lookup type
        Integer ln = (Integer) stp[2];          // lookup sequence number
        Integer lf = (Integer) stp[3];          // lookup flags
        Integer sn = (Integer) stp[4];          // subtable sequence number
        Integer sf = (Integer) stp[5];          // subtable format
        GlyphMappingTable mapping = (GlyphMappingTable) stp[6];
        List entries = (List) stp[7];
        if (tt == GlyphTable.GLYPH_TABLE_TYPE_DEFINITION) {
            int type = GDEFLookupType.getSubtableType(lt);
            String lid = "lu" + ln;
            int sequence = sn;
            int flags = lf;
            int format = sf;
            st = GlyphDefinitionTable.createSubtable(type, lid, sequence, flags, format, mapping, entries);
        }
        return st;
    }

    private List constructGSUBSubtables() {
        List<GlyphSubtable> subtables = new java.util.ArrayList();
        if (seSubtables != null) {
            for (Object seSubtable : seSubtables) {
                Object[] stp = (Object[]) seSubtable;
                GlyphSubtable st;
                if ((st = constructGSUBSubtable(stp)) != null) {
                    subtables.add(st);
                }
            }
        }
        return subtables;
    }

    private GlyphSubtable constructGSUBSubtable(Object[] stp) {
        GlyphSubtable st = null;
        assert (stp != null) && (stp.length == 8);
        Integer tt = (Integer) stp[0];          // table type
        Integer lt = (Integer) stp[1];          // lookup type
        Integer ln = (Integer) stp[2];          // lookup sequence number
        Integer lf = (Integer) stp[3];          // lookup flags
        Integer sn = (Integer) stp[4];          // subtable sequence number
        Integer sf = (Integer) stp[5];          // subtable format
        GlyphCoverageTable coverage = (GlyphCoverageTable) stp[6];
        List entries = (List) stp[7];
        if (tt == GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION) {
            int type = GSUBLookupType.getSubtableType(lt);
            String lid = "lu" + ln;
            int sequence = sn;
            int flags = lf;
            int format = sf;
            st = GlyphSubstitutionTable.createSubtable(type, lid, sequence, flags, format, coverage, entries);
        }
        return st;
    }

    private List constructGPOSSubtables() {
        List<GlyphSubtable> subtables = new java.util.ArrayList();
        if (seSubtables != null) {
            for (Object seSubtable : seSubtables) {
                Object[] stp = (Object[]) seSubtable;
                GlyphSubtable st;
                if ((st = constructGPOSSubtable(stp)) != null) {
                    subtables.add(st);
                }
            }
        }
        return subtables;
    }

    private GlyphSubtable constructGPOSSubtable(Object[] stp) {
        GlyphSubtable st = null;
        assert (stp != null) && (stp.length == 8);
        Integer tt = (Integer) stp[0];          // table type
        Integer lt = (Integer) stp[1];          // lookup type
        Integer ln = (Integer) stp[2];          // lookup sequence number
        Integer lf = (Integer) stp[3];          // lookup flags
        Integer sn = (Integer) stp[4];          // subtable sequence number
        Integer sf = (Integer) stp[5];          // subtable format
        GlyphCoverageTable coverage = (GlyphCoverageTable) stp[6];
        List entries = (List) stp[7];
        if (tt == GlyphTable.GLYPH_TABLE_TYPE_POSITIONING) {
            int type = GSUBLookupType.getSubtableType(lt);
            String lid = "lu" + ln;
            int sequence = sn;
            int flags = lf;
            int format = sf;
            st = GlyphPositioningTable.createSubtable(type, lid, sequence, flags, format, coverage, entries);
        }
        return st;
    }

    private void initATState() {
        seScripts = new java.util.LinkedHashMap();
        seLanguages = new java.util.LinkedHashMap();
        seFeatures = new java.util.LinkedHashMap();
        seSubtables = new java.util.ArrayList();
        resetATSubState();
    }

    private void resetATState() {
        seScripts = null;
        seLanguages = null;
        seFeatures = null;
        seSubtables = null;
        resetATSubState();
    }

    private void initATSubState() {
        seMapping = null;
        seEntries = new java.util.ArrayList();
    }

    private void extractSESubState(int tableType, int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, int subtableFormat) {
        if (seEntries != null) {
            if ((tableType == GlyphTable.GLYPH_TABLE_TYPE_DEFINITION) || (seEntries.size() > 0)) {
                if (seSubtables != null) {
                    Integer tt = tableType;
                    Integer lt = lookupType;
                    Integer ln = lookupSequence;
                    Integer lf = lookupFlags;
                    Integer sn = subtableSequence;
                    Integer sf = subtableFormat;
                    seSubtables.add(new Object[] { tt, lt, ln, lf, sn, sf, seMapping, seEntries });
                }
            }
        }
    }

    private void resetATSubState() {
        seMapping = null;
        seEntries = null;
    }

    private void resetATStateAll() {
        resetATState();
        gdef = null;
        gsub = null;
        gpos = null;
    }

    /** helper method for formatting an integer array for output */
    private String toString(int[] ia) {
        StringBuffer sb = new StringBuffer();
        if ((ia == null) || (ia.length == 0)) {
            sb.append('-');
        } else {
            boolean first = true;
            for (int anIa : ia) {
                if (!first) {
                    sb.append(' ');
                } else {
                    first = false;
                }
                sb.append(anIa);
            }
        }
        return sb.toString();
    }

}
