]> source.dussan.org Git - xmlgraphics-fop.git/commitdiff
Apply patch to revision 1002949, in bug 49687, Complex Script Support, by Glenn Adams
authorSimon Pepping <spepping@apache.org>
Thu, 30 Sep 2010 09:47:15 +0000 (09:47 +0000)
committerSimon Pepping <spepping@apache.org>
Thu, 30 Sep 2010 09:47:15 +0000 (09:47 +0000)
git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/branches/Temp_ComplexScripts@1002978 13f79535-47bb-0310-9956-ffa450edef68

62 files changed:
build.xml
checkstyle-suppressions.xml
findbugs-exclude.xml
src/java/org/apache/fop/area/AreaTreeParser.java
src/java/org/apache/fop/area/inline/TextArea.java
src/java/org/apache/fop/area/inline/WordArea.java
src/java/org/apache/fop/fo/FOText.java
src/java/org/apache/fop/fonts/ArabicScriptProcessor.java
src/java/org/apache/fop/fonts/BFEntry.java
src/java/org/apache/fop/fonts/DefaultScriptProcessor.java
src/java/org/apache/fop/fonts/Font.java
src/java/org/apache/fop/fonts/FontManagerConfigurator.java
src/java/org/apache/fop/fonts/FontReader.java
src/java/org/apache/fop/fonts/GlyphClassMapping.java [new file with mode: 0644]
src/java/org/apache/fop/fonts/GlyphClassTable.java [new file with mode: 0644]
src/java/org/apache/fop/fonts/GlyphContextTester.java
src/java/org/apache/fop/fonts/GlyphCoverageMapping.java [new file with mode: 0644]
src/java/org/apache/fop/fonts/GlyphCoverageTable.java
src/java/org/apache/fop/fonts/GlyphDefinition.java [new file with mode: 0644]
src/java/org/apache/fop/fonts/GlyphDefinitionSubtable.java [new file with mode: 0644]
src/java/org/apache/fop/fonts/GlyphDefinitionTable.java [new file with mode: 0644]
src/java/org/apache/fop/fonts/GlyphMappingTable.java [new file with mode: 0644]
src/java/org/apache/fop/fonts/GlyphPositioning.java
src/java/org/apache/fop/fonts/GlyphPositioningState.java [new file with mode: 0644]
src/java/org/apache/fop/fonts/GlyphPositioningSubtable.java
src/java/org/apache/fop/fonts/GlyphPositioningTable.java
src/java/org/apache/fop/fonts/GlyphProcessingState.java [new file with mode: 0644]
src/java/org/apache/fop/fonts/GlyphSequence.java
src/java/org/apache/fop/fonts/GlyphSubstitution.java
src/java/org/apache/fop/fonts/GlyphSubstitutionState.java [new file with mode: 0644]
src/java/org/apache/fop/fonts/GlyphSubstitutionSubtable.java
src/java/org/apache/fop/fonts/GlyphSubstitutionTable.java
src/java/org/apache/fop/fonts/GlyphSubtable.java
src/java/org/apache/fop/fonts/GlyphTable.java
src/java/org/apache/fop/fonts/GlyphTester.java [new file with mode: 0644]
src/java/org/apache/fop/fonts/GlyphUtils.java [deleted file]
src/java/org/apache/fop/fonts/LazyFont.java
src/java/org/apache/fop/fonts/MultiByteFont.java
src/java/org/apache/fop/fonts/Positionable.java
src/java/org/apache/fop/fonts/ScriptContextTester.java [new file with mode: 0644]
src/java/org/apache/fop/fonts/ScriptProcessor.java
src/java/org/apache/fop/fonts/apps/AbstractFontReader.java
src/java/org/apache/fop/fonts/apps/TTFReader.java
src/java/org/apache/fop/fonts/truetype/TTFFile.java
src/java/org/apache/fop/fonts/truetype/TTFFontLoader.java
src/java/org/apache/fop/layoutmgr/inline/CharacterLayoutManager.java
src/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java
src/java/org/apache/fop/pdf/PDFTextUtil.java
src/java/org/apache/fop/render/afp/AFPPainter.java
src/java/org/apache/fop/render/intermediate/IFPainter.java
src/java/org/apache/fop/render/intermediate/IFParser.java
src/java/org/apache/fop/render/intermediate/IFRenderer.java
src/java/org/apache/fop/render/intermediate/IFSerializer.java
src/java/org/apache/fop/render/intermediate/IFUtil.java
src/java/org/apache/fop/render/java2d/Java2DPainter.java
src/java/org/apache/fop/render/pcl/PCLPainter.java
src/java/org/apache/fop/render/pdf/PDFPainter.java
src/java/org/apache/fop/render/ps/PSPainter.java
src/java/org/apache/fop/render/xml/XMLRenderer.java
src/java/org/apache/fop/util/CharUtilities.java
src/java/org/apache/fop/util/XMLUtil.java
src/sandbox/org/apache/fop/render/svg/SVGPainter.java

index c22be0c344e5a38c50032b37c1fabae797029668..f101dddd5395656dff369e044c4bed885ce3c20c 100644 (file)
--- a/build.xml
+++ b/build.xml
@@ -771,6 +771,7 @@ list of possible build targets.
   <target name="junit-transcoder" depends="junit-compile" description="Runs FOP's JUnit transcoder tests" if="junit.present">
     <echo message="Running basic functionality tests for fop-transcoder.jar"/>
     <junit dir="${basedir}" haltonfailure="${junit.haltonfailure}" fork="${junit.fork}" printsummary="${junit.printsummary}">
+      <jvmarg value="-Xmx1024m"/>
       <sysproperty key="basedir" value="${basedir}"/>
       <sysproperty key="jawa.awt.headless" value="true"/>
       <formatter type="brief" usefile="false" if="junit.formatter.brief.use"/>
index 217299870d842512b5139a40e0761b22bd58b794..e2809e63a7e5c3795f52396015b2669774c697a3 100644 (file)
@@ -2,6 +2,7 @@
 <!DOCTYPE suppressions PUBLIC "-//Puppy Crawl//DTD Suppressions 1.1//EN" "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
 <suppressions>
     <suppress files="org/apache/fop/fo/FOPropertyMapping.java" checks="FileLengthCheck"/>
+    <suppress files="org/apache/fop/fonts/GlyphPositioningTable.java" checks="FileLengthCheck"/>
     <suppress files="org/apache/fop/fonts/truetype/TTFFile.java" checks="FileLengthCheck"/>
     <suppress files="org/apache/fop/Version.java" lines="40-50" checks="LineLengthCheck"/>
 </suppressions>
index 8885c497a873de17c39ee80a7bb86667adf15105..14e262a7cb6f01f45463873474a497539203d68f 100644 (file)
@@ -3,7 +3,10 @@
   <!-- use of null is preferred over zero length array -->
   <Match>
     <Class name="org.apache.fop.area.inline.WordArea"/>
-    <Method name="getBidiLevels"/>
+    <Or>
+      <Method name="getBidiLevels"/>
+      <Method name="glyphPositionAdjustmentsAt"/>
+    </Or>
     <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
   </Match>
   <Match>
     <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
   </Match>
   <!-- string not exposed to end user -->
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphDefinitionTable"/>
+    <Method name="getLookupTypeFromName"/>
+    <Bug pattern="DM_CONVERT_CASE"/>
+  </Match>
   <Match>
     <Class name="org.apache.fop.fonts.GlyphPositioningTable"/>
     <Method name="getLookupTypeFromName"/>
     <Bug pattern="DM_CONVERT_CASE"/>
   </Match>
   <!-- performance optimizations -->
+  <Match>
+    <Class name="org.apache.fop.fonts.ArabicScriptProcessor"/>
+    <Or>
+      <Method name="getPositioningFeatures"/>
+      <Method name="getSubstitutionFeatures"/>
+    </Or>
+    <Bug pattern="EI_EXPOSE_REP"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.DefaultScriptProcessor"/>
+    <Or>
+      <Method name="getPositioningFeatures"/>
+      <Method name="getSubstitutionFeatures"/>
+    </Or>
+    <Bug pattern="EI_EXPOSE_REP"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphPositioningState"/>
+    <Method name="&lt;init&gt;"/>
+    <Bug pattern="EI_EXPOSE_REP2"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphPositioningTable$ChainedContextualSubtableFormat1"/>
+    <Method name="getLookups"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphPositioningTable$ChainedContextualSubtableFormat2"/>
+    <Method name="getLookups"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphPositioningTable$ChainedContextualSubtableFormat3"/>
+    <Method name="getLookups"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphPositioningTable$ContextualSubtableFormat1"/>
+    <Method name="getLookups"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphPositioningTable$ContextualSubtableFormat2"/>
+    <Method name="getLookups"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphPositioningTable$ContextualSubtableFormat3"/>
+    <Method name="getLookups"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphPositioningTable$CursiveSubtableFormat1"/>
+    <Method name="getExitEntryAnchors"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphPositioningTable$DeviceTable"/>
+    <Method name="getDeltas"/>
+    <Bug pattern="EI_EXPOSE_REP"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphPositioningTable$DeviceTable"/>
+    <Method name="&lt;init&gt;"/>
+    <Bug pattern="EI_EXPOSE_REP2"/>
+  </Match>
   <Match>
     <Class name="org.apache.fop.fonts.GlyphSequence"/>
     <Method name="getAssociations"/>
     <Bug pattern="EI_EXPOSE_REP"/>
   </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphSequence$CharAssociation"/>
+    <Method name="getSubIntervals"/>
+    <Bug pattern="EI_EXPOSE_REP"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphSubstitutionState"/>
+    <Method name="setAlternates"/>
+    <Bug pattern="EI_EXPOSE_REP2"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphSubstitutionTable$AlternateSubtableFormat1"/>
+    <Method name="getAlternatesForCoverageIndex"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphSubstitutionTable$ChainedContextualSubtableFormat1"/>
+    <Method name="getLookups"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphSubstitutionTable$ChainedContextualSubtableFormat2"/>
+    <Method name="getLookups"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphSubstitutionTable$ChainedContextualSubtableFormat3"/>
+    <Method name="getLookups"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphSubstitutionTable$ContextualSubtableFormat1"/>
+    <Method name="getLookups"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphSubstitutionTable$ContextualSubtableFormat2"/>
+    <Method name="getLookups"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphSubstitutionTable$ContextualSubtableFormat3"/>
+    <Method name="getLookups"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphSubstitutionTable$MultipleSubtableFormat1"/>
+    <Method name="getGlyphsForCoverageIndex"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
   <Match>
     <Class name="org.apache.fop.fonts.GlyphSubstitutionTable$Ligature"/>
     <Or>
   </Match>
   <Match>
     <Class name="org.apache.fop.fonts.GlyphSubstitutionTable$Ligature"/>
-    <Or>
-        <Method name="&lt;init&gt;" params="int, int[]" returns="void"/>
-    </Or>
+    <Method name="&lt;init&gt;" params="int, int[]" returns="void"/>
     <Bug pattern="EI_EXPOSE_REP2"/>
   </Match>
   <Match>
     <Method name="&lt;init&gt;" params="org.apache.fop.fonts.GlyphSubstitutionTable$Ligature[]" returns="void"/>
     <Bug pattern="EI_EXPOSE_REP2"/>
   </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$ChainedClassSequenceRule"/>
+    <Method name="&lt;init&gt;"/>
+    <Bug pattern="EI_EXPOSE_REP2"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$ChainedClassSequenceRule"/>
+    <Or>
+      <Method name="getBacktrackClasses"/>
+      <Method name="getLookaheadClasses"/>
+    </Or>
+    <Bug pattern="EI_EXPOSE_REP"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$ChainedCoverageSequenceRule"/>
+    <Method name="&lt;init&gt;"/>
+    <Bug pattern="EI_EXPOSE_REP2"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$ChainedCoverageSequenceRule"/>
+    <Or>
+      <Method name="getBacktrackCoverages"/>
+      <Method name="getLookaheadCoverages"/>
+    </Or>
+    <Bug pattern="EI_EXPOSE_REP"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$ChainedGlyphSequenceRule"/>
+    <Method name="&lt;init&gt;"/>
+    <Bug pattern="EI_EXPOSE_REP2"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$ChainedGlyphSequenceRule"/>
+    <Or>
+      <Method name="getBacktrackGlyphs"/>
+      <Method name="getLookaheadGlyphs"/>
+    </Or>
+    <Bug pattern="EI_EXPOSE_REP"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$ClassSequenceRule"/>
+    <Method name="&lt;init&gt;"/>
+    <Bug pattern="EI_EXPOSE_REP2"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$ClassSequenceRule"/>
+    <Method name="getClasses"/>
+    <Bug pattern="EI_EXPOSE_REP"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$CoverageSequenceRule"/>
+    <Method name="&lt;init&gt;"/>
+    <Bug pattern="EI_EXPOSE_REP2"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$CoverageSequenceRule"/>
+    <Method name="getCoverages"/>
+    <Bug pattern="EI_EXPOSE_REP"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$GlyphSequenceRule"/>
+    <Method name="&lt;init&gt;"/>
+    <Bug pattern="EI_EXPOSE_REP2"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$GlyphSequenceRule"/>
+    <Method name="getGlyphs"/>
+    <Bug pattern="EI_EXPOSE_REP"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$LookupTable"/>
+    <Method name="getSubtables"/>
+    <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$Rule"/>
+    <Method name="getLookups"/>
+    <Bug pattern="EI_EXPOSE_REP"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$RuleSet"/>
+    <Method name="&lt;init&gt;"/>
+    <Bug pattern="EI_EXPOSE_REP2"/>
+  </Match>
+  <Match>
+    <Class name="org.apache.fop.fonts.GlyphTable$RuleSet"/>
+    <Method name="getRules"/>
+    <Bug pattern="EI_EXPOSE_REP"/>
+  </Match>
   <Match>
     <Class name="org.apache.fop.area.inline.WordArea"/>
-    <Method name="getBidiLevels"/>
+    <Or>
+      <Method name="getBidiLevels"/>
+      <Method name="getGlyphPositionAdjustments"/>
+    </Or>
     <Bug pattern="EI_EXPOSE_REP"/>
   </Match>
   <Match>
index 184020b1a999ac393164252896bf936a3d406830..b6efdcbb5a81dc4f1ecefa0e121f892a36724af4 100644 (file)
@@ -836,9 +836,12 @@ public class AreaTreeParser {
                         = ConversionUtils.toIntArray(
                             lastAttributes.getValue("letter-adjust"), "\\s");
                 int level = XMLUtil.getAttributeAsInt(lastAttributes, "level", -1);
+                int[][] gposAdjustments
+                    = XMLUtil.getAttributeAsPositionAdjustments(lastAttributes, "position-adjust");
                 content.flip();
                 WordArea word = new WordArea
-                    ( offset, level, content.toString().trim(), letterAdjust, null );
+                    ( offset, level, content.toString().trim(), letterAdjust,
+                      null, gposAdjustments );
                 AbstractTextArea text = getCurrentText();
                 word.setParentArea(text);
                 text.addChildArea(word);
index 9d18b8d44809b6454d0ced90c6bd36e0d7cad0d3..b6831fc31d1507786f17eb6a0569ac0371b98712 100644 (file)
@@ -57,7 +57,7 @@ public class TextArea extends AbstractTextArea {
      * @param offset the offset for the next area
      */
     public void addWord(String word, int offset) {
-        addWord(word, 0, null, null, offset);
+        addWord(word, 0, null, null, null, offset);
     }
 
     /**
@@ -68,13 +68,15 @@ public class TextArea extends AbstractTextArea {
      * @param letterAdjust the letter adjustment array (may be null)
      * @param levels array of resolved bidirection levels of word characters,
      * or null if default level
+     * @param gposAdjustments array of general position adjustments or null if none apply
      * @param blockProgressionOffset the offset for the next area
      */
     public void addWord
-        ( String word, int ipd, int[] letterAdjust, int[] levels, int blockProgressionOffset ) {
+        ( String word, int ipd, int[] letterAdjust, int[] levels,
+          int[][] gposAdjustments, int blockProgressionOffset ) {
         int minWordLevel = findMinLevel ( levels );
         WordArea wordArea = new WordArea
-            ( blockProgressionOffset, minWordLevel, word, letterAdjust, levels );
+            ( blockProgressionOffset, minWordLevel, word, letterAdjust, levels, gposAdjustments );
         wordArea.setIPD ( ipd );
         addChildArea(wordArea);
         wordArea.setParentArea(this);
index 690a32e17ab0d0e9f253ec1c711ce7f65af0c148..552ca27ac6c82c1ac81b5ececc2e8ce7931d7ae0 100644 (file)
@@ -40,6 +40,11 @@ public class WordArea extends InlineArea {
      */
     protected int[] levels;
 
+    /**
+     * An array of glyph positioning adjustments to apply to each glyph 'char' in word (optional)
+     */
+    protected int[][] gposAdjustments;
+
     /**
      * A flag indicating whether the content of word is reversed in relation to
      * its original logical order.
@@ -54,14 +59,17 @@ public class WordArea extends InlineArea {
      * @param letterAdjust the letter adjust array (may be null)
      * @param levels array of per-character (glyph) bidirectional levels,
      * in case word area is heterogenously leveled
+     * @param gposAdjustments array of general position adjustments or null if none apply
      */
     public WordArea
-        ( int blockProgressionOffset, int level, String word, int[] letterAdjust, int[] levels ) {
+        ( int blockProgressionOffset, int level, String word, int[] letterAdjust, int[] levels,
+          int[][] gposAdjustments ) {
         super ( blockProgressionOffset, level );
-        assert word != null;
+        int length = ( word != null ) ? word.length() : 0;
         this.word = word;
-        this.letterAdjust = letterAdjust;
-        this.levels = maybePopulateLevels ( levels, level, word.length() );
+        this.letterAdjust = maybeAdjustLength ( letterAdjust, length );
+        this.levels = maybePopulateLevels ( levels, level, length );
+        this.gposAdjustments = maybeAdjustLength ( gposAdjustments, length );
         this.reversed = false;
     }
 
@@ -125,8 +133,34 @@ public class WordArea extends InlineArea {
     }
 
     /**
-     * <p>Reverse characters and corresponding per-character levels if word's length is greater
-     * than one.</p>
+     * Obtain per-character (glyph) position adjustments.
+     * @return a (possibly empty) array of adjustments, each having four elements, or null
+     * if no adjustments apply
+     */
+    public int[][] getGlyphPositionAdjustments() {
+        return gposAdjustments;
+    }
+
+    /**
+     * <p>Obtain per-character (glyph) position adjustments at a specified index position.</p>
+     * <p>If word has been reversed, then the position is relative to the reversed word.</p>
+     * @param position the index of the (possibly reversed) character from which to obtain the
+     * level
+     * @return an array of adjustments or null if none applies
+     */
+    public int[] glyphPositionAdjustmentsAt ( int position ) {
+        if ( position > word.length() ) {
+            throw new IndexOutOfBoundsException();
+        } else if ( gposAdjustments != null ) {
+            return gposAdjustments [ position ];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * <p>Reverse characters and corresponding per-character levels and glyph position
+     * adjustments.</p>
      * @param mirror if true, then perform mirroring if mirrorred characters
      */
     public void reverse ( boolean mirror ) {
@@ -135,6 +169,9 @@ public class WordArea extends InlineArea {
             if ( levels != null ) {
                 reverse ( levels );
             }
+            if ( gposAdjustments != null ) {
+                reverse ( gposAdjustments );
+            }
             reversed = !reversed;
             if ( mirror ) {
                 word = CharUtilities.mirror ( word );
@@ -163,12 +200,60 @@ public class WordArea extends InlineArea {
         return reversed;
     }
 
+    /*
+     * If int[] array is not of specified length, then create
+     * a new copy of the first length entries.
+     */
+    private static int[] maybeAdjustLength ( int[] ia, int length ) {
+        if ( ia != null ) {
+            if ( ia.length == length ) {
+                return ia;
+            } else {
+                int[] iaNew = new int [ length ];
+                for ( int i = 0, n = ia.length; i < n; i++ ) {
+                    if ( i < length ) {
+                        iaNew [ i ] = ia [ i ];
+                    } else {
+                        break;
+                    }
+                }
+                return iaNew;
+            }
+        } else {
+            return ia;
+        }
+    }
+
+    /*
+     * If int[][] matrix is not of specified length, then create
+     * a new shallow copy of the first length entries.
+     */
+    private static int[][] maybeAdjustLength ( int[][] im, int length ) {
+        if ( im != null ) {
+            if ( im.length == length ) {
+                return im;
+            } else {
+                int[][] imNew = new int [ length ][];
+                for ( int i = 0, n = im.length; i < n; i++ ) {
+                    if ( i < length ) {
+                        imNew [ i ] = im [ i ];
+                    } else {
+                        break;
+                    }
+                }
+                return imNew;
+            }
+        } else {
+            return im;
+        }
+    }
+
     private static int[] maybePopulateLevels ( int[] levels, int level, int count ) {
         if ( ( levels == null ) && ( level >= 0 ) ) {
             levels = new int[count];
             Arrays.fill ( levels, level );
         }
-        return levels;
+        return maybeAdjustLength ( levels, count );
     }
 
     private static void reverse ( int[] a ) {
@@ -180,4 +265,13 @@ public class WordArea extends InlineArea {
         }
     }
 
+    private static void reverse ( int[][] aa ) {
+        for ( int i = 0, n = aa.length, m = n / 2; i < m; i++ ) {
+            int k = n - i - 1;
+            int[] t = aa [ k ];
+            aa [ k ] = aa [ i ];
+            aa [ i ] = t;
+        }
+    }
+
 }
index bae359e63830169aecaa6fda21dbc95cdf2b1191..a803eba9a2d1e6dea7c6a7393bd38d8b97d2e67f 100644 (file)
@@ -819,6 +819,20 @@ public class FOText extends FONode implements CharSequence {
         }
     }
 
+    /**
+     * Obtain length of mapping of characters over specific interval.
+     * @param start index in character buffer
+     * @param end index in character buffer
+     * @return the length of the mapping (if present) or zero
+     */
+    public int getMappingLength ( int start, int end ) {
+        if ( mappings != null ) {
+            return ( (String) mappings.get ( new MapRange ( start, end ) ) ) .length();
+        } else {
+            return 0;
+        }
+    }
+
     /**
      * Obtain bidirectional levels of mapping of characters over specific interval.
      * @param start index in character buffer
@@ -827,8 +841,25 @@ public class FOText extends FONode implements CharSequence {
      * in case no bidi levels have been assigned
      */
     public int[] getMappingBidiLevels ( int start, int end ) {
-        if ( mappings != null ) {
-            return getBidiLevels ( start, end ); // [TBD] FIX ME
+        if ( hasMapping ( start, end ) ) {
+            int   nc = end - start;
+            int   nm = getMappingLength ( start, end );
+            int[] la = getBidiLevels ( start, end );
+            if ( nm == nc ) {                   // mapping is same length as mapped range
+                return la;
+            } else if ( nm > nc ) {             // mapping is longer than mapped range
+                int[] ma = new int [ nm ];
+                System.arraycopy ( la, 0, ma, 0, la.length );
+                for ( int i = la.length,
+                          n = ma.length, l = ( i > 0 ) ? la [ i - 1 ] : 0; i < n; i++ ) {
+                    ma [ i ] = l;
+                }
+                return ma;
+            } else {                            // mapping is shorter than mapped range
+                int[] ma = new int [ nm ];
+                System.arraycopy ( la, 0, ma, 0, ma.length );
+                return ma;
+            }
         } else {
             return getBidiLevels ( start, end );
         }
index 1e10147d2d991b6aa2b98794b34c73a9061bae0c..2701fa4bfbf09dc6cef49f8e3a43783412efa17e 100644 (file)
@@ -19,9 +19,8 @@
 
 package org.apache.fop.fonts;
 
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
+import java.util.HashMap;
 import java.util.Map;
 
 import org.apache.commons.logging.Log;
@@ -37,241 +36,190 @@ import org.apache.fop.util.BidiConstants;
 // CSOFF: LineLengthCheck
 
 /**
- * <p>The <code>ArabicScriptProcessor</code> class implements script processor for
- * performing glypph substitution and positioning operations on content associated with the Arabic script.</p>
+ * <p>The <code>ArabicScriptProcessor</code> class implements script processor for
+ * performing glyph substitution and positioning operations on content associated with the Arabic script.</p>
  * @author Glenn Adams
  */
-public class ArabicScriptProcessor extends ScriptProcessor {
-
-    /**
-     * logging instance
-     */
-    protected static final Log log = LogFactory.getLog(ArabicScriptProcessor.class);                                    // CSOK: ConstantNameCheck
-
-    ArabicScriptProcessor ( String script ) {
-        super ( script );
-    }
-
-    /** {@inheritDoc} */
-    public GlyphSequence substitute ( GlyphSequence gs, String script, String language, Map/*<LookupSpec,GlyphSubtable[]>*/ lookups ) {
-        // finals
-        gs = subFina ( gs, script, language, (GlyphSubtable[]) lookups.get ( new GlyphTable.LookupSpec ( script, language, "fina" ) ) );
-
-        // medials
-        gs = subMedi ( gs, script, language, (GlyphSubtable[]) lookups.get ( new GlyphTable.LookupSpec ( script, language, "medi" ) ) );
-
-        // initials
-        gs = subInit ( gs, script, language, (GlyphSubtable[]) lookups.get ( new GlyphTable.LookupSpec ( script, language, "init" ) ) );
-
-        // isolates
-        gs = subIsol ( gs, script, language, (GlyphSubtable[]) lookups.get ( new GlyphTable.LookupSpec ( script, language, "isol" ) ) );
-
-        // required ligatures
-        gs = subLiga ( gs, script, language, (GlyphSubtable[]) lookups.get ( new GlyphTable.LookupSpec ( script, language, "rlig" ) ) );
-
-        // standard ligatures
-        gs = subLiga ( gs, script, language, (GlyphSubtable[]) lookups.get ( new GlyphTable.LookupSpec ( script, language, "liga" ) ) );
-
-        return gs;
-    }
-
-    /** {@inheritDoc} */
-    public int[] position ( GlyphSequence gs, String script, String language, Map/*<LookupSpec,GlyphSubtable[]>*/ lookups ) {
-        return null;
-    }
+public class ArabicScriptProcessor extends DefaultScriptProcessor {
+
+    /** logging instance */
+    private static final Log log = LogFactory.getLog(ArabicScriptProcessor.class);                                      // CSOK: ConstantNameCheck
+
+    /** features to use for substitutions */
+    private static final String[] gsubFeatures =                                                                        // CSOK: ConstantNameCheck
+    {
+        "calt",                                                 // contextual alternates
+        "ccmp",                                                 // glyph composition/decomposition
+        "fina",                                                 // final (terminal) forms
+        "init",                                                 // initial forms
+        "isol",                                                 // isolated formas
+        "liga",                                                 // standard ligatures
+        "medi",                                                 // medial forms
+        "rlig"                                                  // required ligatures
+    };
 
-    private static GlyphContextTester finalContextTester
-        = new GlyphContextTester() { public boolean test ( GlyphSequence gs, GlyphSequence.CharAssociation ca ) { return inFinalContext ( gs, ca ); } };
+    /** features to use for positioning */
+    private static final String[] gposFeatures =                                                                        // CSOK: ConstantNameCheck
+    {
+        "curs",                                                 // cursive positioning
+        "kern",                                                 // kerning
+        "mark",                                                 // mark to base or ligature positioning
+        "mkmk"                                                  // mark to mark positioning
+    };
 
-    private GlyphSequence subFina ( GlyphSequence gs, String script, String language, GlyphSubtable[] sta ) {
-        return substituteSingle ( gs, script, language, "fina", sta, finalContextTester, false );
+    private static class SubstitutionScriptContextTester implements ScriptContextTester {
+        private static Map/*<String,GlyphContextTester>*/ testerMap = new HashMap/*<String,GlyphContextTester>*/();
+        static {
+            testerMap.put ( "fina", new GlyphContextTester() { public boolean test ( GlyphSequence gs, int index ) { return inFinalContext ( gs, index ); } } );
+            testerMap.put ( "init", new GlyphContextTester() { public boolean test ( GlyphSequence gs, int index ) { return inInitialContext ( gs, index ); } } );
+            testerMap.put ( "isol", new GlyphContextTester() { public boolean test ( GlyphSequence gs, int index ) { return inIsolateContext ( gs, index ); } } );
+            testerMap.put ( "medi", new GlyphContextTester() { public boolean test ( GlyphSequence gs, int index ) { return inMedialContext ( gs, index ); } } );
+            testerMap.put ( "liga", new GlyphContextTester() { public boolean test ( GlyphSequence gs, int index ) { return inLigatureContext ( gs, index ); } } );
+        }
+        public GlyphContextTester getTester ( String feature ) {
+            return (GlyphContextTester) testerMap.get ( feature );
+        }
     }
 
-    private static GlyphContextTester medialContextTester
-        = new GlyphContextTester() { public boolean test ( GlyphSequence gs, GlyphSequence.CharAssociation ca ) { return inMedialContext ( gs, ca ); } };
-
-    private GlyphSequence subMedi ( GlyphSequence gs, String script, String language, GlyphSubtable[] sta ) {
-        return substituteSingle ( gs, script, language, "medi", sta, medialContextTester, false );
+    private static class PositioningScriptContextTester implements ScriptContextTester {
+        private static Map/*<String,GlyphContextTester>*/ testerMap = new HashMap/*<String,GlyphContextTester>*/();
+        public GlyphContextTester getTester ( String feature ) {
+            return (GlyphContextTester) testerMap.get ( feature );
+        }
     }
-    
-    private static GlyphContextTester initialContextTester
-        = new GlyphContextTester() { public boolean test ( GlyphSequence gs, GlyphSequence.CharAssociation ca ) { return inInitialContext ( gs, ca ); } };
 
-    private GlyphSequence subInit ( GlyphSequence gs, String script, String language, GlyphSubtable[] sta ) {
-        return substituteSingle ( gs, script, language, "init", sta, initialContextTester, false );
-    }
-    
-    private static GlyphContextTester isolateContextTester
-        = new GlyphContextTester() { public boolean test ( GlyphSequence gs, GlyphSequence.CharAssociation ca ) { return inIsolateContext ( gs, ca ); } };
+    private final ScriptContextTester subContextTester;
+    private final ScriptContextTester posContextTester;
 
-    private GlyphSequence subIsol ( GlyphSequence gs, String script, String language, GlyphSubtable[] sta ) {
-        return substituteSingle ( gs, script, language, "isol", sta, isolateContextTester, false );
+    ArabicScriptProcessor ( String script ) {
+        super ( script );
+        this.subContextTester = new SubstitutionScriptContextTester();
+        this.posContextTester = new PositioningScriptContextTester();
     }
-    
-    private static GlyphContextTester ligatureContextTester
-        = new GlyphContextTester() { public boolean test ( GlyphSequence gs, GlyphSequence.CharAssociation ca ) { return inLigatureContext ( gs, ca ); } };
 
-    private GlyphSequence subLiga ( GlyphSequence gs, String script, String language, GlyphSubtable[] sta ) {
-        return substituteMultiple ( gs, script, language, "liga", sta, ligatureContextTester, false );
+    /** {@inheritDoc} */
+    public String[] getSubstitutionFeatures() {
+        return gsubFeatures;
     }
 
-    private GlyphSequence substituteSingle ( GlyphSequence gs, String script, String language, String feature, GlyphSubtable[] sta, GlyphContextTester tester, boolean reverse ) {
-        if ( ( sta != null ) && ( sta.length > 0 ) ) {
-            // enforce subtable type constraints
-            for ( int i = 0, n = sta.length; i < n; i++ ) {
-                GlyphSubtable st = sta [ i ];
-                if ( ! ( st instanceof GlyphSubstitutionSubtable ) ) {
-                    throw new IncompatibleSubtableException ( "'" + feature + "' feature requires glyph substitution subtable" );
-                }
-            }
-            CharSequence ga = gs.getGlyphs();
-            GlyphSequence.CharAssociation[] aa = gs.getAssociations();
-            List gsl = new ArrayList();
-            List cal = new ArrayList();
-            for ( int i = 0, n = ga.length(); i < n; i++ ) {
-                int k = reverse ? ( n - i - 1 ) : i;
-                GlyphSequence.CharAssociation a = aa [ k ];
-                GlyphSequence iss = gs.getGlyphSubsequence ( k, k + 1 );
-                GlyphSequence oss;
-                if ( tester.test ( iss, a ) ) {
-                    oss = doSubstitutions ( iss, script, language, sta );
-                } else {
-                    oss = iss;
-                }
-                gsl.add ( oss );
-                cal.add ( a );
-            }
-            gs = new GlyphSequence ( gs.getCharacters(), gsl, cal, reverse );
-        }
-        return gs;
+    /** {@inheritDoc} */
+    public ScriptContextTester getSubstitutionContextTester() {
+        return subContextTester;
     }
 
-    private GlyphSequence substituteMultiple ( GlyphSequence gs, String script, String language, String feature, GlyphSubtable[] sta, GlyphContextTester tester, boolean reverse ) {
-        if ( ( sta != null ) && ( sta.length > 0 ) ) {
-            gs = doSubstitutions ( gs, script, language, sta );
-        }
-        return gs;
+    /** {@inheritDoc} */
+    public String[] getPositioningFeatures() {
+        return gposFeatures;
     }
 
-    private GlyphSequence doSubstitutions ( GlyphSequence gs, String script, String language, GlyphSubtable[] sta ) {
-        for ( int i = 0, n = sta.length; i < n; i++ ) {
-            GlyphSubtable st = sta [ i ];
-            assert st instanceof GlyphSubstitutionSubtable;
-            gs = ( (GlyphSubstitutionSubtable) st ) . substitute ( gs, script, language );
-        }
-        return gs;
+    /** {@inheritDoc} */
+    public ScriptContextTester getPositioningContextTester() {
+        return posContextTester;
     }
 
-    private static boolean inFinalContext ( GlyphSequence gs, GlyphSequence.CharAssociation a ) {
-        CharSequence cs = gs.getCharacters();
-        if ( cs.length() == 0 ) {
+    private static boolean inFinalContext ( GlyphSequence gs, int index ) {
+        GlyphSequence.CharAssociation a = gs.getAssociation ( index );
+        int[] ca = gs.getCharacterArray ( false );
+        int   nc = gs.getCharacterCount();
+        if ( nc == 0 ) {
             return false;
         } else {
             int s = a.getStart();
             int e = a.getEnd();
-            if ( ! hasFinalPrecedingContext ( cs, s, e ) ) {
+            if ( ! hasFinalPrecedingContext ( ca, nc, s, e ) ) {
                 return false;
-            } else if ( forcesFinalThisContext ( cs, s, e ) ) {
-                if (log.isDebugEnabled()) {
-                    log.debug ( "+FIN: [" + a.getStart() + "," + a.getEnd() + "]: " + GlyphUtils.toString ( (CharSequence) gs ) );
-                }
+            } else if ( forcesFinalThisContext ( ca, nc, s, e ) ) {
                 return true;
-            } else if ( ! hasFinalFollowingContext ( cs, s, e ) ) {
+            } else if ( ! hasFinalFollowingContext ( ca, nc, s, e ) ) {
                 return false;
             } else {
-                if (log.isDebugEnabled()) {
-                    log.debug ( "+FIN: [" + a.getStart() + "," + a.getEnd() + "]: " + GlyphUtils.toString ( (CharSequence) gs ) );
-                }
                 return true;
             }
         }
     }
 
-    private static boolean inMedialContext ( GlyphSequence gs, GlyphSequence.CharAssociation a ) {
-        CharSequence cs = gs.getCharacters();
-        if ( cs.length() == 0 ) {
+    private static boolean inMedialContext ( GlyphSequence gs, int index ) {
+        GlyphSequence.CharAssociation a = gs.getAssociation ( index );
+        int[] ca = gs.getCharacterArray ( false );
+        int   nc = gs.getCharacterCount();
+        if ( nc == 0 ) {
             return false;
         } else {
             int s = a.getStart();
             int e = a.getEnd();
-            if ( ! hasMedialPrecedingContext ( cs, s, e ) ) {
+            if ( ! hasMedialPrecedingContext ( ca, nc, s, e ) ) {
                 return false;
-            } else if ( ! hasMedialThisContext ( cs, s, e ) ) {
+            } else if ( ! hasMedialThisContext ( ca, nc, s, e ) ) {
                 return false;
-            } else if ( ! hasMedialFollowingContext ( cs, s, e ) ) {
+            } else if ( ! hasMedialFollowingContext ( ca, nc, s, e ) ) {
                 return false;
             } else {
-                if (log.isDebugEnabled()) {
-                    log.debug ( "+MED: [" + a.getStart() + "," + a.getEnd() + "]: " + GlyphUtils.toString ( (CharSequence) gs ) );
-                }
                 return true;
             }
         }
     }
 
-    private static boolean inInitialContext ( GlyphSequence gs, GlyphSequence.CharAssociation a ) {
-        CharSequence cs = gs.getCharacters();
-        if ( cs.length() == 0 ) {
+    private static boolean inInitialContext ( GlyphSequence gs, int index ) {
+        GlyphSequence.CharAssociation a = gs.getAssociation ( index );
+        int[] ca = gs.getCharacterArray ( false );
+        int   nc = gs.getCharacterCount();
+        if ( nc == 0 ) {
             return false;
         } else {
             int s = a.getStart();
             int e = a.getEnd();
-            if ( ! hasInitialPrecedingContext ( cs, s, e ) ) {
+            if ( ! hasInitialPrecedingContext ( ca, nc, s, e ) ) {
                 return false;
-            } else if ( ! hasInitialFollowingContext ( cs, s, e ) ) {
+            } else if ( ! hasInitialFollowingContext ( ca, nc, s, e ) ) {
                 return false;
             } else {
-                if (log.isDebugEnabled()) {
-                    log.debug ( "+INI: [" + a.getStart() + "," + a.getEnd() + "]: " + GlyphUtils.toString ( (CharSequence) gs ) );
-                }
                 return true;
             }
         }
     }
 
-    private static boolean inIsolateContext ( GlyphSequence gs, GlyphSequence.CharAssociation a ) {
-        CharSequence cs = gs.getCharacters();
-        int n;
-        if ( ( n = cs.length() ) == 0 ) {
+    private static boolean inIsolateContext ( GlyphSequence gs, int index ) {
+        GlyphSequence.CharAssociation a = gs.getAssociation ( index );
+        int   nc = gs.getCharacterCount();
+        if ( nc == 0 ) {
             return false;
-        } else if ( ( a.getStart() == 0 ) && ( a.getEnd() == n ) ) {
-            if (log.isDebugEnabled()) {
-                log.debug ( "+ISO: [" + a.getStart() + "," + a.getEnd() + "]: " + GlyphUtils.toString ( (CharSequence) gs ) );
-            }
+        } else if ( ( a.getStart() == 0 ) && ( a.getEnd() == nc ) ) {
             return true;
         } else {
             return false;
         }
     }
 
-    private static boolean inLigatureContext ( GlyphSequence gs, GlyphSequence.CharAssociation a ) {
-        CharSequence cs = gs.getCharacters();
-        if ( cs.length() == 0 ) {
+    private static boolean inLigatureContext ( GlyphSequence gs, int index ) {
+        GlyphSequence.CharAssociation a = gs.getAssociation ( index );
+        int[] ca = gs.getCharacterArray ( false );
+        int   nc = gs.getCharacterCount();
+        if ( nc == 0 ) {
             return false;
         } else {
             int s = a.getStart();
             int e = a.getEnd();
-            if ( ! hasLigaturePrecedingContext ( cs, s, e ) ) {
+            if ( ! hasLigaturePrecedingContext ( ca, nc, s, e ) ) {
                 return false;
-            } else if ( ! hasLigatureFollowingContext ( cs, s, e ) ) {
+            } else if ( ! hasLigatureFollowingContext ( ca, nc, s, e ) ) {
                 return false;
             } else {
-                if (log.isDebugEnabled()) {
-                    log.debug ( "+LIG: [" + a.getStart() + "," + a.getEnd() + "]: " + GlyphUtils.toString ( (CharSequence) gs ) );
-                }
                 return true;
             }
         }
     }
 
-    private static boolean hasFinalPrecedingContext ( CharSequence cs, int s, int e ) {
+    private static boolean hasFinalPrecedingContext ( int[] ca, int nc, int s, int e ) {
         int chp = 0;
         int clp = 0;
         for ( int i = s; i > 0; i-- ) {
-            chp = cs.charAt ( i - 1 );
-            clp = BidiClassUtils.getBidiClass ( chp );
-            if ( clp != BidiConstants.NSM ) {
-                break;
+            int k = i - 1;
+            if ( ( k >= 0 ) && ( k < nc ) ) {
+                chp = ca [ k ];
+                clp = BidiClassUtils.getBidiClass ( chp );
+                if ( clp != BidiConstants.NSM ) {
+                    break;
+                }
             }
         }
         if ( clp != BidiConstants.AL ) {
@@ -283,15 +231,18 @@ public class ArabicScriptProcessor extends ScriptProcessor {
         }
     }
 
-    private static boolean forcesFinalThisContext ( CharSequence cs, int s, int e ) {
+    private static boolean forcesFinalThisContext ( int[] ca, int nc, int s, int e ) {
         int chl = 0;
         int cll = 0;
         for ( int i = 0, n = e - s; i < n; i++ ) {
             int k = n - i - 1;
-            chl = cs.charAt ( s + k );
-            cll = BidiClassUtils.getBidiClass ( chl );
-            if ( cll != BidiConstants.NSM ) {
-                break;
+            int j = s + k;
+            if ( ( j >= 0 ) && ( j < nc ) ) {
+                chl = ca [ j ];
+                cll = BidiClassUtils.getBidiClass ( chl );
+                if ( cll != BidiConstants.NSM ) {
+                    break;
+                }
             }
         }
         if ( cll != BidiConstants.AL ) {
@@ -304,11 +255,11 @@ public class ArabicScriptProcessor extends ScriptProcessor {
         }
     }
 
-    private static boolean hasFinalFollowingContext ( CharSequence cs, int s, int e ) {
+    private static boolean hasFinalFollowingContext ( int[] ca, int nc, int s, int e ) {
         int chf = 0;
         int clf = 0;
-        for ( int i = e, n = cs.length(); i < n; i++ ) {
-            chf = cs.charAt ( i );
+        for ( int i = e, n = nc; i < n; i++ ) {
+            chf = ca [ i ];
             clf = BidiClassUtils.getBidiClass ( chf );
             if ( clf != BidiConstants.NSM ) {
                 break;
@@ -323,14 +274,17 @@ public class ArabicScriptProcessor extends ScriptProcessor {
         }
     }
 
-    private static boolean hasInitialPrecedingContext ( CharSequence cs, int s, int e ) {
+    private static boolean hasInitialPrecedingContext ( int[] ca, int nc, int s, int e ) {
         int chp = 0;
         int clp = 0;
         for ( int i = s; i > 0; i-- ) {
-            chp = cs.charAt ( i - 1 );
-            clp = BidiClassUtils.getBidiClass ( chp );
-            if ( clp != BidiConstants.NSM ) {
-                break;
+            int k = i - 1;
+            if ( ( k >= 0 ) && ( k < nc ) ) {
+                chp = ca [ k ];
+                clp = BidiClassUtils.getBidiClass ( chp );
+                if ( clp != BidiConstants.NSM ) {
+                    break;
+                }
             }
         }
         if ( clp != BidiConstants.AL ) {
@@ -342,11 +296,11 @@ public class ArabicScriptProcessor extends ScriptProcessor {
         }
     }
 
-    private static boolean hasInitialFollowingContext ( CharSequence cs, int s, int e ) {
+    private static boolean hasInitialFollowingContext ( int[] ca, int nc, int s, int e ) {
         int chf = 0;
         int clf = 0;
-        for ( int i = e, n = cs.length(); i < n; i++ ) {
-            chf = cs.charAt ( i );
+        for ( int i = e, n = nc; i < n; i++ ) {
+            chf = ca [ i ];
             clf = BidiClassUtils.getBidiClass ( chf );
             if ( clf != BidiConstants.NSM ) {
                 break;
@@ -361,14 +315,17 @@ public class ArabicScriptProcessor extends ScriptProcessor {
         }
     }
 
-    private static boolean hasMedialPrecedingContext ( CharSequence cs, int s, int e ) {
+    private static boolean hasMedialPrecedingContext ( int[] ca, int nc, int s, int e ) {
         int chp = 0;
         int clp = 0;
         for ( int i = s; i > 0; i-- ) {
-            chp = cs.charAt ( i - 1 );
-            clp = BidiClassUtils.getBidiClass ( chp );
-            if ( clp != BidiConstants.NSM ) {
-                break;
+            int k = i - 1;
+            if ( ( k >= 0 ) && ( k < nc ) ) {
+                chp = ca [ k ];
+                clp = BidiClassUtils.getBidiClass ( chp );
+                if ( clp != BidiConstants.NSM ) {
+                    break;
+                }
             }
         }
         if ( clp != BidiConstants.AL ) {
@@ -380,27 +337,33 @@ public class ArabicScriptProcessor extends ScriptProcessor {
         }
     }
 
-    private static boolean hasMedialThisContext ( CharSequence cs, int s, int e ) {
-        int chf = 0;
+    private static boolean hasMedialThisContext ( int[] ca, int nc, int s, int e ) {
+        int chf = 0;    // first non-NSM char in [s,e)
         int clf = 0;
         for ( int i = 0, n = e - s; i < n; i++ ) {
-            chf = cs.charAt ( s + i );
-            clf = BidiClassUtils.getBidiClass ( chf );
-            if ( clf != BidiConstants.NSM ) {
-                break;
+            int k = s + i;
+            if ( ( k >= 0 ) && ( k < nc ) ) {
+                chf = ca [ s + i ];
+                clf = BidiClassUtils.getBidiClass ( chf );
+                if ( clf != BidiConstants.NSM ) {
+                    break;
+                }
             }
         }
         if ( clf != BidiConstants.AL ) {
             return false;
         }
-        int chl = 0;
+        int chl = 0;    // last non-NSM char in [s,e)
         int cll = 0;
         for ( int i = 0, n = e - s; i < n; i++ ) {
             int k = n - i - 1;
-            chl = cs.charAt ( s + k );
-            cll = BidiClassUtils.getBidiClass ( chl );
-            if ( cll != BidiConstants.NSM ) {
-                break;
+            int j = s + k;
+            if ( ( j >= 0 ) && ( j < nc ) ) {
+                chl = ca [ j ];
+                cll = BidiClassUtils.getBidiClass ( chl );
+                if ( cll != BidiConstants.NSM ) {
+                    break;
+                }
             }
         }
         if ( cll != BidiConstants.AL ) {
@@ -415,11 +378,11 @@ public class ArabicScriptProcessor extends ScriptProcessor {
         }
     }
 
-    private static boolean hasMedialFollowingContext ( CharSequence cs, int s, int e ) {
+    private static boolean hasMedialFollowingContext ( int[] ca, int nc, int s, int e ) {
         int chf = 0;
         int clf = 0;
-        for ( int i = e, n = cs.length(); i < n; i++ ) {
-            chf = cs.charAt ( i );
+        for ( int i = e, n = nc; i < n; i++ ) {
+            chf = ca [ i ];
             clf = BidiClassUtils.getBidiClass ( chf );
             if ( clf != BidiConstants.NSM ) {
                 break;
@@ -434,15 +397,15 @@ public class ArabicScriptProcessor extends ScriptProcessor {
         }
     }
 
-    private static boolean hasLigaturePrecedingContext ( CharSequence cs, int s, int e ) {
+    private static boolean hasLigaturePrecedingContext ( int[] ca, int nc, int s, int e ) {
         return true;
     }
 
-    private static boolean hasLigatureFollowingContext ( CharSequence cs, int s, int e ) {
+    private static boolean hasLigatureFollowingContext ( int[] ca, int nc, int s, int e ) {
         int chf = 0;
         int clf = 0;
-        for ( int i = e, n = cs.length(); i < n; i++ ) {
-            chf = cs.charAt ( i );
+        for ( int i = e, n = nc; i < n; i++ ) {
+            chf = ca [ i ];
             clf = BidiClassUtils.getBidiClass ( chf );
             if ( clf != BidiConstants.NSM ) {
                 break;
index 4e0b169fdbf507c8823a3d7f382c002b50aecbe3..2ae978b26a28a63136a3329fde8bd6ab1cfbbda7 100644 (file)
@@ -64,4 +64,19 @@ public class BFEntry {
         return glyphStartIndex;
     }
 
+    /** {@inheritDoc} */
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        sb.append ( "{ UC[" );
+        sb.append ( unicodeStart );
+        sb.append ( ',' );
+        sb.append ( unicodeEnd );
+        sb.append ( "]: GC[" );
+        sb.append ( glyphStartIndex );
+        sb.append ( ',' );
+        sb.append ( glyphStartIndex + ( unicodeEnd - unicodeStart ) );
+        sb.append ( "] }" );
+        return sb.toString();
+    }
+
 }
index d3ef159d5028d16a378ae73e9195edf140b7ef4f..4ed5c524c99440c4765c5cdea48ddadd60af2d81 100644 (file)
@@ -29,17 +29,41 @@ import java.util.Map;
  */
 public class DefaultScriptProcessor extends ScriptProcessor {
 
+    /** features to use for substitutions */
+    private static final String[] gsubFeatures =                                                                        // CSOK: ConstantNameCheck
+    {
+        "ccmp",                                                 // glyph composition/decomposition
+        "liga",                                                 // common ligatures
+        "locl"                                                  // localized forms
+    };
+
+    /** features to use for positioning */
+    private static final String[] gposFeatures =                                                                        // CSOK: ConstantNameCheck
+    {
+        "kern"                                                  // kerning
+    };
+
     DefaultScriptProcessor ( String script ) {
         super ( script );
     }
 
     /** {@inheritDoc} */
-    public GlyphSequence substitute ( GlyphSequence gs, String script, String language, Map/*<LookupSpec,GlyphSubtable[]>*/ lookups ) {
-        return gs;
+    public String[] getSubstitutionFeatures() {
+        return gsubFeatures;
+    }
+
+    /** {@inheritDoc} */
+    public ScriptContextTester getSubstitutionContextTester() {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    public String[] getPositioningFeatures() {
+        return gposFeatures;
     }
 
     /** {@inheritDoc} */
-    public int[] position ( GlyphSequence cs, String script, String language, Map/*<LookupSpec,GlyphSubtable[]>*/ lookups ) {
+    public ScriptContextTester getPositioningContextTester() {
         return null;
     }
 
index 988a9b14b5a1939e109aff8f879c8b85a049c1e9..1f0eff1467f099d280d9f41b033457702022905e 100644 (file)
@@ -379,13 +379,18 @@ public class Font implements Substitutable, Positionable {
     }
 
     /** {@inheritDoc} */
-    public int[] performPositioning ( CharSequence cs, String script, String language ) {
+    public int[][] performPositioning ( CharSequence cs, String script, String language, int fontSize ) {
         if ( metric instanceof Positionable ) {
             Positionable p = (Positionable) metric;
-            return p.performPositioning ( cs, script, language );
+            return p.performPositioning ( cs, script, language, fontSize );
         } else {
             throw new UnsupportedOperationException();
         }
     }
 
+    /** {@inheritDoc} */
+    public int[][] performPositioning ( CharSequence cs, String script, String language ) {
+        return performPositioning ( cs, script, language, fontSize );
+    }
+
 }
index 7792b118d0372d7bbb90b9af79d46a5010dae080..4d887d167d75887e09a96c24e03eb8cf52e9834f 100644 (file)
@@ -82,6 +82,18 @@ public class FontManagerConfigurator {
             }
         }
 
+        // [GA] permit configuration control over base14 kerning; without this,
+        // there is no way for a user to enable base14 kerning other than by
+        // programmatic API;
+        if (cfg.getChild("base14-kerning", false) != null) {
+            try {
+                fontManager
+                    .setBase14KerningEnabled(cfg.getChild("base14-kerning").getValueAsBoolean());
+            } catch (ConfigurationException e) {
+                LogUtil.handleException(log, e, true);
+            }
+        }
+
         // global font configuration
         Configuration fontsCfg = cfg.getChild("fonts", false);
         if (fontsCfg != null) {
index 73eefaa5824a666e7aa82dac6fb63fa53d08145b..194d894091fcc16d03f96c05403eecd2b795db48 100644 (file)
@@ -38,8 +38,6 @@ import org.xml.sax.helpers.DefaultHandler;
 import org.apache.fop.apps.FOPException;
 import org.apache.fop.fonts.apps.TTFReader;
 
-// CSOFF: LineLengthCheck
-
 /**
  * Class for reading a metric.xml file and creating a font object.
  * Typical usage:
@@ -52,7 +50,7 @@ import org.apache.fop.fonts.apps.TTFReader;
  */
 public class FontReader extends DefaultHandler {
 
-    private Locator locator = null;
+    // private Locator locator = null; // not used at present
     private boolean isCID = false;
     private CustomFont returnFont = null;
     private MultiByteFont multiFont = null;
@@ -66,31 +64,6 @@ public class FontReader extends DefaultHandler {
 
     private List bfranges = null;
 
-    /* advanced typographic (script extras) support */
-    private boolean inScriptExtras = false;
-    private int seTable = -1;
-    private Map seLookups = null;
-    private String seScript = null;
-    private String seLanguage = null;
-    private String seFeature = null;
-    private String seUseLookup = null;
-    private List seUseLookups = null;
-    private String luID = null;
-    private int luType = -1;
-    private List ltSubtables = null;
-    private int luSequence = -1;
-    private int luFlags = 0;
-    private int lstSequence = -1;
-    private int lstFormat = -1;
-    private List lstCoverage = null;
-    private List lstGIDs = null;
-    private List lstRanges = null;
-    private List lstEntries = null;
-    private List lstLIGSets = null;
-    private List lstLIGs = null;
-    private int ligGID = -1;
-    /* end of script extras parse state */
-
     private void createFont(InputSource source) throws FOPException {
         XMLReader parser = null;
 
@@ -186,7 +159,7 @@ public class FontReader extends DefaultHandler {
      * {@inheritDoc}
      */
     public void setDocumentLocator(Locator locator) {
-        this.locator = locator;
+        // this.locator = locator; // not used at present
     }
 
     /**
@@ -194,9 +167,7 @@ public class FontReader extends DefaultHandler {
      */
     public void startElement(String uri, String localName, String qName,
                              Attributes attributes) throws SAXException {
-        if ( inScriptExtras ) {
-            startElementScriptExtras ( uri, localName, qName, attributes );
-        } else if (localName.equals("font-metrics")) {
+        if (localName.equals("font-metrics")) {
             if ("TYPE0".equals(attributes.getValue("type"))) {
                 multiFont = new MultiByteFont();
                 returnFont = multiFont;
@@ -246,8 +217,6 @@ public class FontReader extends DefaultHandler {
         } else if ("pair".equals(localName)) {
             currentKerning.put(new Integer(attributes.getValue("kpx2")),
                                new Integer(attributes.getValue("kern")));
-        } else if ("script-extras".equals(localName)) {
-            inScriptExtras = true;
         }
 
     }
@@ -267,9 +236,7 @@ public class FontReader extends DefaultHandler {
      */
     public void endElement(String uri, String localName, String qName) throws SAXException {
         String content = text.toString().trim();
-        if ( inScriptExtras ) {
-            endElementScriptExtras ( uri, localName, qName, content );
-        } else if ("font-name".equals(localName)) {
+        if ("font-name".equals(localName)) {
             returnFont.setFontName(content);
         } else if ("full-name".equals(localName)) {
             returnFont.setFullName(content);
@@ -347,375 +314,4 @@ public class FontReader extends DefaultHandler {
         text.append(ch, start, length);
     }
 
-    private void validateScriptTag ( String tag )
-        throws SAXException {
-    }
-
-    private void validateLanguageTag ( String tag, String script )
-        throws SAXException {
-    }
-
-    private void validateFeatureTag ( String tag, int tableType, String script, String language )
-        throws SAXException {
-    }
-
-    private int mapLookupType ( String type, int tableType ) {
-        int t = -1;
-        if ( tableType == GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION ) {
-            t = GlyphSubstitutionTable.getLookupTypeFromName ( type );
-        } else if ( tableType == GlyphTable.GLYPH_TABLE_TYPE_POSITIONING ) {
-            t = GlyphPositioningTable.getLookupTypeFromName ( type );
-        }
-        return t;
-    }
-
-    private void validateLookupType ( String type, int tableType )
-        throws SAXException {
-        if ( mapLookupType ( type, tableType ) == -1 ) {
-            throw new SAXParseException ( "invalid lookup type \'" + type + "\'", locator );
-        }
-    }
-
-    private void startElementScriptExtras ( String uri, String localName, String qName, Attributes attributes )
-        throws SAXException {
-        if ( "gsub".equals(localName) ) {
-            assert seLookups == null;
-            seLookups = new java.util.HashMap();
-            seTable = GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION;
-        } else if ( "gpos".equals(localName) ) {
-            assert seLookups == null;
-            seLookups = new java.util.HashMap();
-            seTable = GlyphTable.GLYPH_TABLE_TYPE_POSITIONING;
-        } else if ( "script".equals(localName) ) {
-            String tag = attributes.getValue("tag");
-            if ( tag != null ) {
-                assert seScript == null;
-                validateScriptTag ( tag );
-                seScript = tag;
-            } else {
-                throw new SAXParseException ( "missing tag attribute on <script/> element", locator );
-            }
-        } else if ( "lang".equals(localName) ) {
-            String tag = attributes.getValue("tag");
-            if ( tag != null ) {
-                assert seLanguage == null;
-                validateLanguageTag ( tag, seScript );
-                seLanguage = tag;
-            } else {
-                throw new SAXParseException ( "missing tag attribute on <lang/> element", locator );
-            }
-        } else if ( "feature".equals(localName) ) {
-            String tag = attributes.getValue("tag");
-            if ( tag != null ) {
-                validateFeatureTag ( tag, seTable, seScript, seLanguage );
-                assert seFeature == null;
-                seFeature = tag;
-            } else {
-                throw new SAXParseException ( "missing tag attribute on <feature/> element", locator );
-            }
-        } else if ( "use-lookup".equals(localName) ) {
-            String ref = attributes.getValue("ref");
-            if ( ref != null ) {
-                assert seUseLookup == null;
-                seUseLookup = ref;
-            } else {
-                throw new SAXParseException ( "missing ref attribute on <use-lookup/> element", locator );
-            }
-        } else if ( "lookup".equals(localName) ) {
-            String id = attributes.getValue("id");
-            if ( id != null ) {
-                assert luID == null;
-                luID = id; luSequence++; lstSequence = -1;
-            } else {
-                throw new SAXParseException ( "missing id attribute on <lookup/> element", locator );
-            }
-            String flags = attributes.getValue("flags");
-            if ( flags != null ) {
-                try {
-                    luFlags = Integer.parseInt ( flags );
-                } catch ( NumberFormatException e ) {
-                    throw new SAXParseException ( "invalid flags attribute on <lookup/> element, must be integer", locator );
-                }
-            }
-            String type = attributes.getValue("type");
-            if ( type != null ) {
-                validateLookupType ( type, seTable );
-                assert luType == -1;
-                luType = mapLookupType ( type, seTable );
-            } else {
-                throw new SAXParseException ( "missing type attribute on <lookup/> element", locator );
-            }
-        } else if ( "lst".equals(localName) ) {
-            String format = attributes.getValue("format");
-            if ( format != null ) {
-                try {
-                    lstSequence++;
-                    lstFormat = Integer.parseInt ( format );
-                } catch ( NumberFormatException e ) {
-                    throw new SAXParseException ( "invalid format attribute on <lst/> element, must be integer", locator );
-                }
-                assert lstCoverage == null;
-                assert lstEntries == null;
-            } else {
-                throw new SAXParseException ( "missing format attribute on <lst/> element", locator );
-            }
-        } else if ( "coverage".equals(localName) ) {
-            assert lstGIDs == null;
-            assert lstRanges == null;
-            lstGIDs = new java.util.ArrayList();
-            lstRanges = new java.util.ArrayList();
-        } else if ( "range".equals(localName) ) {
-            String gs = attributes.getValue("gs");
-            String ge = attributes.getValue("ge");
-            String ci = attributes.getValue("ci");
-            if ( ( gs != null ) && ( ge != null ) && ( ci != null ) ) {
-                try {
-                    int s = Integer.parseInt ( gs );
-                    int e = Integer.parseInt ( ge );
-                    int i = Integer.parseInt ( ci );
-                    lstRanges.add ( new GlyphCoverageTable.CoverageRange ( s, e, i ) );
-                } catch ( NumberFormatException e ) {
-                    throw new SAXParseException ( "invalid format attribute on <lst/> element, must be integer", locator );
-                } catch ( IllegalArgumentException e ) {
-                    throw new SAXParseException ( "bad gs, ge, or ci attribute on <range/> element, must be non-negative integers, with gs <= ge", locator );
-                }
-            } else {
-                throw new SAXParseException ( "missing gs, ge, or ci attribute on <range/> element", locator );
-            }
-        } else if ( "entries".equals(localName) ) {
-            initEntriesState ( seTable, luType, lstFormat );
-        } else if ( "ligs".equals(localName) ) {
-            assert lstLIGs == null;
-            lstLIGs = new java.util.ArrayList();
-        } else if ( "lig".equals(localName) ) {
-            if ( lstLIGs == null ) {
-                throw new SAXParseException ( "missing container <ligs/> element for <lig/> element", locator );
-            } else {
-                String gid = attributes.getValue("gid");
-                if ( gid != null ) {
-                    try {
-                        ligGID = Integer.parseInt ( gid );
-                    } catch ( NumberFormatException e ) {
-                        throw new SAXParseException ( "invalid gid attribute on <lig/> element, must be integer", locator );
-                    }
-                } else {
-                    throw new SAXParseException ( "missing gid attribute on <lig/> element", locator );
-                }
-            }
-        }
-    }
-
-    private void endElementScriptExtras ( String uri, String localName, String qName, String content )
-        throws SAXException {
-        if ( "script-extras".equals(localName) ) {
-            inScriptExtras = false;
-        } else if ( "gsub".equals(localName) ) {
-            if ( ( ltSubtables != null ) && ( ltSubtables.size() > 0 ) ) {
-                if ( multiFont.getGSUB() == null ) {
-                    multiFont.setGSUB ( new GlyphSubstitutionTable ( seLookups, ltSubtables ) );
-                }
-            }
-            ltSubtables = null; seTable = -1; seLookups = null;
-        } else if ( "gpos".equals(localName) ) {
-            if ( ( ltSubtables != null ) && ( ltSubtables.size() > 0 ) ) {
-                if ( multiFont.getGPOS() == null ) {
-                    multiFont.setGPOS ( new GlyphPositioningTable ( seLookups, ltSubtables ) );
-                }
-            }
-            ltSubtables = null; seTable = -1; seLookups = null;
-        } else if ( "script".equals(localName) ) {
-            assert seUseLookups == null;
-            assert seUseLookup == null;
-            assert seFeature == null;
-            assert seLanguage == null;
-            seScript = null;
-        } else if ( "lang".equals(localName) ) {
-            assert seUseLookups == null;
-            assert seUseLookup == null;
-            assert seFeature == null;
-            seLanguage = null;
-        } else if ( "feature".equals(localName) ) {
-            if ( ( seScript != null ) && ( seLanguage != null ) && ( seFeature != null ) ) {
-                if ( ( seUseLookups != null ) && ( seUseLookups.size() > 0 ) ) {
-                    seLookups.put ( new GlyphTable.LookupSpec ( seScript, seLanguage, seFeature ), seUseLookups );
-                }
-            }
-            seUseLookups = null; seFeature = null;
-        } else if ( "use-lookup".equals(localName) ) {
-            if ( seUseLookup != null ) {
-                if ( seUseLookups == null ) {
-                    seUseLookups = new java.util.ArrayList();
-                }
-                seUseLookups.add ( seUseLookup );
-            }
-            seUseLookup = null;
-        } else if ( "lookup".equals(localName) ) {
-            luType = -1;
-            luFlags = 0;
-        } else if ( "lst".equals(localName) ) {
-            assert lstCoverage != null;
-            assert lstEntries != null;
-            addLookupSubtable ( seTable, luType, luID, luSequence, luFlags, lstFormat, lstCoverage, lstEntries );
-            lstFormat = -1;
-            lstCoverage = null;
-            lstEntries = null;
-        } else if ( "coverage".equals(localName) ) {
-            assert lstGIDs != null;
-            assert lstRanges != null;
-            assert lstCoverage == null;
-            if ( lstGIDs.size() > 0 ) {
-                lstCoverage = lstGIDs;
-            } else if ( lstRanges.size() > 0 ) {
-                lstCoverage = lstRanges;
-            }
-            lstGIDs = null; lstRanges = null;
-        } else if ( "gid".equals(localName) ) {
-            if ( lstGIDs != null ) {
-                try {
-                    lstGIDs.add ( Integer.decode ( content ) );
-                } catch ( NumberFormatException e ) {
-                    throw new SAXParseException ( "invalid <gid/> element content, must be integer", locator );
-                }
-            }
-        } else if ( "entries".equals(localName) ) {
-            finishEntriesState ( seTable, luType, lstFormat );
-        } else if ( "ligs".equals(localName) ) {
-            assert lstLIGSets != null;
-            assert lstLIGs != null;
-            lstLIGSets.add ( new GlyphSubstitutionTable.LigatureSet ( lstLIGs ) );
-            lstLIGs = null;
-        } else if ( "lig".equals(localName) ) {
-            assert lstLIGs != null;
-            assert ligGID >= 0;
-            int[] ligComponents = parseLigatureComponents ( content );
-            if ( ligComponents != null ) {
-                lstLIGs.add ( new GlyphSubstitutionTable.Ligature ( ligGID, ligComponents ) );
-            }
-            ligGID = -1;
-        }
-    }
-
-    private void initEntriesState ( int tableType, int lookupType, int subtableFormat ) {
-        if ( tableType == GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION ) {
-            switch ( lookupType ) {
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_SINGLE:
-                assert lstGIDs == null;
-                lstGIDs = new java.util.ArrayList();
-                break;
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_LIGATURE:
-                assert lstLIGSets == null;
-                lstLIGSets = new java.util.ArrayList();
-                break;
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_MULTIPLE:
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_ALTERNATE:
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CONTEXT:
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CHAINING_CONTEXT:
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION:
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_REVERSE_CHAINING_CONTEXT_SINGLE:
-                throw new UnsupportedOperationException();
-            default:
-                break;
-            }
-        } else if ( tableType == GlyphTable.GLYPH_TABLE_TYPE_POSITIONING ) {
-            switch ( lookupType ) {
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_SINGLE:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_PAIR:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_CURSIVE:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_MARK_TO_BASE:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_MARK_TO_MARK:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_CONTEXT:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_CHAINED_CONTEXT:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING:
-                throw new UnsupportedOperationException();
-            default:
-                break;
-            }
-        }
-    }
-
-    private void finishEntriesState ( int tableType, int lookupType, int subtableFormat ) {
-        if ( tableType == GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION ) {
-            switch ( lookupType ) {
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_SINGLE:
-                assert lstGIDs != null;
-                lstEntries = lstGIDs; lstGIDs = null;
-                break;
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_LIGATURE:
-                assert lstLIGSets != null;
-                lstEntries = lstLIGSets; lstLIGSets = null;
-                break;
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_MULTIPLE:
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_ALTERNATE:
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CONTEXT:
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CHAINING_CONTEXT:
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION:
-            case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_REVERSE_CHAINING_CONTEXT_SINGLE:
-                throw new UnsupportedOperationException();
-            default:
-                break;
-            }
-        } else if ( tableType == GlyphTable.GLYPH_TABLE_TYPE_POSITIONING ) {
-            switch ( lookupType ) {
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_SINGLE:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_PAIR:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_CURSIVE:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_MARK_TO_BASE:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_MARK_TO_MARK:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_CONTEXT:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_CHAINED_CONTEXT:
-            case GlyphPositioningTable.GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING:
-                throw new UnsupportedOperationException();
-            default:
-                break;
-            }
-        }
-    }
-
-    private void addLookupSubtable                              // CSOK: ParameterNumber
-        ( int tableType, int lookupType, String lookupID, int lookupSequence, int lookupFlags, int subtableFormat, List coverage, List entries ) {
-        GlyphSubtable st = null;
-        if ( tableType == GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION ) {
-            st = GlyphSubstitutionTable.createSubtable ( lookupType, lookupID, lookupSequence, lookupFlags, subtableFormat, coverage, entries );
-        } else if ( tableType == GlyphTable.GLYPH_TABLE_TYPE_POSITIONING ) {
-            st = GlyphPositioningTable.createSubtable ( lookupType, lookupID, lookupSequence, lookupFlags, subtableFormat, coverage, entries );
-        }
-        if ( st != null ) {
-            if ( ltSubtables == null ) {
-                ltSubtables = new java.util.ArrayList();
-            }
-            ltSubtables.add ( st );
-        }
-    }
-
-    private int[] parseLigatureComponents ( String s )
-        throws SAXParseException {
-        String[] csa = s.split ( "\\s" );
-        if ( ( csa == null ) || ( csa.length == 0 ) ) {
-            throw new SAXParseException ( "invalid <lig/> element, must specify at least one component", locator );
-        } else {
-            int nc = csa.length;
-            int[] components = new int [ nc ];
-            for ( int i = 0, n = nc; i < n; i++ ) {
-                String cs = csa [ i ];
-                int c;
-                try {
-                    c = Integer.parseInt ( cs );
-                    if ( ( c < 0 ) || ( c > 65535 ) ) {
-                        throw new SAXParseException ( "invalid component value (" + c + ") in <lig/> element, out of range", locator );
-                    } else {
-                        components [ i ] = c;
-                    }
-                } catch ( NumberFormatException e ) {
-                    throw new SAXParseException ( "invalid component \"" + cs + "\" in <lig/> element, must be integer", locator );
-                }
-                
-            }
-            return components;
-        }
-    }
-
 }
-
-
diff --git a/src/java/org/apache/fop/fonts/GlyphClassMapping.java b/src/java/org/apache/fop/fonts/GlyphClassMapping.java
new file mode 100644 (file)
index 0000000..648ee27
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * 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.fonts;
+
+// CSOFF: LineLengthCheck
+
+/**
+ * The <code>GlyphClassMapping</code> interface provides glyph identifier to class
+ * index mapping support.
+ * @author Glenn Adams
+ */
+public interface GlyphClassMapping {
+
+    /**
+     * Obtain size of class table, i.e., ciMax + 1, where ciMax is the maximum
+     * class index.
+     * @param set for coverage set based class mappings, indicates set index, otherwise ignored
+     * @return size of class table
+     */
+    int getClassSize ( int set );
+
+    /**
+     * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of
+     * the class table.
+     * @param gid glyph identifier (code)
+     * @param set for coverage set based class mappings, indicates set index, otherwise ignored
+     * @return non-negative glyph class index or -1 if glyph identifiers is not mapped by table
+     */
+    int getClassIndex ( int gid, int set );
+
+}
diff --git a/src/java/org/apache/fop/fonts/GlyphClassTable.java b/src/java/org/apache/fop/fonts/GlyphClassTable.java
new file mode 100644 (file)
index 0000000..69dd811
--- /dev/null
@@ -0,0 +1,277 @@
+/*
+ * 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.fonts;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Iterator;
+
+// CSOFF: LineLengthCheck
+// CSOFF: NoWhitespaceAfterCheck
+
+/**
+ * Base class implementation of glyph class table.
+ * @author Glenn Adams
+ */
+public final class GlyphClassTable extends GlyphMappingTable implements GlyphClassMapping {
+
+    /** empty mapping table */
+    public static final int GLYPH_CLASS_TYPE_EMPTY = GLYPH_MAPPING_TYPE_EMPTY;
+
+    /** mapped mapping table */
+    public static final int GLYPH_CLASS_TYPE_MAPPED = GLYPH_MAPPING_TYPE_MAPPED;
+
+    /** range based mapping table */
+    public static final int GLYPH_CLASS_TYPE_RANGE = GLYPH_MAPPING_TYPE_RANGE;
+
+    /** empty mapping table */
+    public static final int GLYPH_CLASS_TYPE_COVERAGE_SET = 3;
+
+    private GlyphClassMapping cm;
+
+    private GlyphClassTable ( GlyphClassMapping cm ) {
+        assert cm != null;
+        assert cm instanceof GlyphMappingTable;
+        this.cm = cm;
+    }
+
+    /** {@inheritDoc} */
+    public int getType() {
+        return ( (GlyphMappingTable) cm ) .getType();
+    }
+
+    /** {@inheritDoc} */
+    public List getEntries() {
+        return ( (GlyphMappingTable) cm ) .getEntries();
+    }
+
+    /** {@inheritDoc} */
+    public int getClassSize ( int set ) {
+        return cm.getClassSize ( set );
+    }
+
+    /** {@inheritDoc} */
+    public int getClassIndex ( int gid, int set ) {
+        return cm.getClassIndex ( gid, set );
+    }
+
+    /**
+     * Create glyph class table.
+     * @param entries list of mapped or ranged class entries, or null or empty list
+     * @return a new covera table instance
+     */
+    public static GlyphClassTable createClassTable ( List entries ) {
+        GlyphClassMapping cm;
+        if ( ( entries == null ) || ( entries.size() == 0 ) ) {
+            cm = new EmptyClassTable ( entries );
+        } else if ( isMappedClass ( entries ) ) {
+            cm = new MappedClassTable ( entries );
+        } else if ( isRangeClass ( entries ) ) {
+            cm = new RangeClassTable ( entries );
+        } else if ( isCoverageSetClass ( entries ) ) {
+            cm = new CoverageSetClassTable ( entries );
+        } else {
+            cm = null;
+        }
+        assert cm != null : "unknown class type";
+        return new GlyphClassTable ( cm );
+    }
+
+    private static boolean isMappedClass ( List entries ) {
+        if ( ( entries == null ) || ( entries.size() == 0 ) ) {
+            return false;
+        } else {
+            for ( Iterator it = entries.iterator(); it.hasNext();) {
+                Object o = it.next();
+                if ( ! ( o instanceof Integer ) ) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    private static boolean isRangeClass ( List entries ) {
+        if ( ( entries == null ) || ( entries.size() == 0 ) ) {
+            return false;
+        } else {
+            for ( Iterator it = entries.iterator(); it.hasNext();) {
+                Object o = it.next();
+                if ( ! ( o instanceof MappingRange ) ) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    private static boolean isCoverageSetClass ( List entries ) {
+        if ( ( entries == null ) || ( entries.size() == 0 ) ) {
+            return false;
+        } else {
+            for ( Iterator it = entries.iterator(); it.hasNext();) {
+                Object o = it.next();
+                if ( ! ( o instanceof GlyphCoverageTable ) ) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    private static class EmptyClassTable extends GlyphMappingTable.EmptyMappingTable implements GlyphClassMapping {
+        public EmptyClassTable ( List entries ) {
+            super ( entries );
+        }
+        /** {@inheritDoc} */
+        public int getClassSize ( int set ) {
+            return 0;
+        }
+        /** {@inheritDoc} */
+        public int getClassIndex ( int gid, int set ) {
+            return -1;
+        }
+    }
+
+    private static class MappedClassTable extends GlyphMappingTable.MappedMappingTable implements GlyphClassMapping {
+        private int firstGlyph;
+        private int[] gca;
+        private int gcMax = -1;
+        public MappedClassTable ( List entries ) {
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            List entries = new java.util.ArrayList();
+            entries.add ( Integer.valueOf ( firstGlyph ) );
+            if ( gca != null ) {
+                for ( int i = 0, n = gca.length; i < n; i++ ) {
+                    entries.add ( Integer.valueOf ( gca [ i ] ) );
+                }
+            }
+            return entries;
+        }
+        /** {@inheritDoc} */
+        public int getMappingSize() {
+            return gcMax + 1;
+        }
+        /** {@inheritDoc} */
+        public int getMappedIndex ( int gid ) {
+            int i = gid - firstGlyph;
+            if ( ( i >= 0 ) && ( i < gca.length ) ) {
+                return gca [ i ];
+            } else {
+                return -1;
+            }
+        }
+        /** {@inheritDoc} */
+        public int getClassSize ( int set ) {
+            return getMappingSize();
+        }
+        /** {@inheritDoc} */
+        public int getClassIndex ( int gid, int set ) {
+            return getMappedIndex ( gid );
+        }
+        private void populate ( List entries ) {
+            // obtain entries iterator
+            Iterator it = entries.iterator();
+            // extract first glyph
+            int firstGlyph = 0;
+            if ( it.hasNext() ) {
+                Object o = it.next();
+                if ( o instanceof Integer ) {
+                    firstGlyph = ( (Integer) o ) . intValue();
+                } else {
+                    throw new IllegalArgumentException ( "illegal entry, first entry must be Integer denoting first glyph value, but is: " + o );
+                }
+            }
+            // extract glyph class array
+            int i = 0, n = entries.size() - 1, gcMax = -1;
+            int[] gca = new int [ n ];
+            while ( it.hasNext() ) {
+                Object o = it.next();
+                if ( o instanceof Integer ) {
+                    int gc = ( (Integer) o ) . intValue();
+                    gca [ i++ ] = gc;
+                    if ( gc > gcMax ) {
+                        gcMax = gc;
+                    }
+                } else {
+                    throw new IllegalArgumentException ( "illegal mapping entry, must be Integer: " + o );
+                }
+            }
+            assert i == n;
+            assert this.gca == null;
+            this.firstGlyph = firstGlyph;
+            this.gca = gca;
+            this.gcMax = gcMax;
+        }
+        /** {@inheritDoc} */
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            sb.append("{ firstGlyph = " + firstGlyph + ", classes = {");
+            for ( int i = 0, n = gca.length; i < n; i++ ) {
+                if ( i > 0 ) {
+                    sb.append(',');
+                }
+                sb.append ( Integer.toString ( gca [ i ] ) );
+            }
+            sb.append("} }");
+            return sb.toString();
+        }
+    }
+
+    private static class RangeClassTable extends GlyphMappingTable.RangeMappingTable implements GlyphClassMapping {
+        public RangeClassTable ( List entries ) {
+            super ( entries );
+        }
+        /** {@inheritDoc} */
+        public int getMappedIndex ( int gid, int s, int m ) {
+            return m;
+        }
+        /** {@inheritDoc} */
+        public int getClassSize ( int set ) {
+            return getMappingSize();
+        }
+        /** {@inheritDoc} */
+        public int getClassIndex ( int gid, int set ) {
+            return getMappedIndex ( gid );
+        }
+    }
+
+    private static class CoverageSetClassTable extends GlyphMappingTable.EmptyMappingTable implements GlyphClassMapping {
+        public CoverageSetClassTable ( List entries ) {
+            throw new UnsupportedOperationException ( "coverage set class table not yet supported" );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GLYPH_CLASS_TYPE_COVERAGE_SET;
+        }
+        /** {@inheritDoc} */
+        public int getClassSize ( int set ) {
+            return 0;
+        }
+        /** {@inheritDoc} */
+        public int getClassIndex ( int gid, int set ) {
+            return -1;
+        }
+    }
+
+}
index ab69894bffa4d4c6460df2099b8c7edfac7f74c2..f318b8145387d42d5735a3b6b45f50fa3d4a630b 100644 (file)
@@ -28,9 +28,9 @@ public interface GlyphContextTester {
     /**
      * Perform a test on a glyph sequence in a specific (originating) character context.
      * @param gs glyph sequence to test
-     * @param ca character association defining the context of test
+     * @param index index into glyph sequence to test
      * @return true if test is satisfied
      */
-    boolean test ( GlyphSequence gs, GlyphSequence.CharAssociation ca );
+    boolean test ( GlyphSequence gs, int index );
 
 }
diff --git a/src/java/org/apache/fop/fonts/GlyphCoverageMapping.java b/src/java/org/apache/fop/fonts/GlyphCoverageMapping.java
new file mode 100644 (file)
index 0000000..34d4cd3
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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.fonts;
+
+// CSOFF: LineLengthCheck
+
+/**
+ * The <code>GlyphCoverageMapping</code> interface provides glyph identifier to coverage
+ * index mapping support.
+ * @author Glenn Adams
+ */
+public interface GlyphCoverageMapping {
+
+    /**
+     * Obtain size of coverage table, i.e., ciMax + 1, where ciMax is the maximum
+     * coverage index.
+     * @return size of coverage table
+     */
+    int getCoverageSize();
+
+    /**
+     * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of
+     * the coverage table.
+     * @param gid glyph identifier (code)
+     * @return non-negative glyph coverage index or -1 if glyph identifiers is not mapped by table
+     */
+    int getCoverageIndex ( int gid );
+
+}
index 88c3db866c18750fce256446c87b276bc98640b4..5eb379ee88cbb98747f6bb12697062acfe0f354d 100644 (file)
@@ -23,70 +23,78 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Iterator;
 
-// CSOFF: NoWhitespaceAfterCheck
-// CSOFF: InnerAssignmentCheck
 // CSOFF: LineLengthCheck
+// CSOFF: InnerAssignmentCheck
+// CSOFF: NoWhitespaceAfterCheck
 
 /**
- * Abstract base class implementation of glyph coverage table.
+ * Base class implementation of glyph coverage table.
  * @author Glenn Adams
  */
-public abstract class GlyphCoverageTable {
+public final class GlyphCoverageTable extends GlyphMappingTable implements GlyphCoverageMapping {
 
-    /** empty coverage table */
-    public static final int GLYPH_COVERAGE_TYPE_EMPTY = 0;
+    /** empty mapping table */
+    public static final int GLYPH_COVERAGE_TYPE_EMPTY = GLYPH_MAPPING_TYPE_EMPTY;
 
-    /** mapped coverage table */
-    public static final int GLYPH_COVERAGE_TYPE_MAPPED = 1;
+    /** mapped mapping table */
+    public static final int GLYPH_COVERAGE_TYPE_MAPPED = GLYPH_MAPPING_TYPE_MAPPED;
 
-    /** range based coverage table */
-    public static final int GLYPH_COVERAGE_TYPE_RANGE = 2;
+    /** range based mapping table */
+    public static final int GLYPH_COVERAGE_TYPE_RANGE = GLYPH_MAPPING_TYPE_RANGE;
 
-    /**
-     * Obtain coverage type.
-     * @return coverage format type
-     */
-    public abstract int getType();
+    private GlyphCoverageMapping cm;
 
-    /**
-     * Obtain coverage entries.
-     * @return list of coverage entries
-     */
-    public abstract List getEntries();
+    private GlyphCoverageTable ( GlyphCoverageMapping cm ) {
+        assert cm != null;
+        assert cm instanceof GlyphMappingTable;
+        this.cm = cm;
+    }
 
-    /**
-     * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of
-     * the coverage table.
-     * @param gid glyph identifier (code)
-     * @return non-negative glyph coverage index or -1 if glyph identifiers is not mapped by table
-     */
-    public abstract int getCoverageIndex ( int gid );
+    /** {@inheritDoc} */
+    public int getType() {
+        return ( (GlyphMappingTable) cm ) .getType();
+    }
+
+    /** {@inheritDoc} */
+    public List getEntries() {
+        return ( (GlyphMappingTable) cm ) .getEntries();
+    }
+
+    /** {@inheritDoc} */
+    public int getCoverageSize() {
+        return cm.getCoverageSize();
+    }
+
+    /** {@inheritDoc} */
+    public int getCoverageIndex ( int gid ) {
+        return cm.getCoverageIndex ( gid );
+    }
 
     /**
      * Create glyph coverage table.
-     * @param coverage list of mapped or ranged coverage entries, or null or empty list
+     * @param entries list of mapped or ranged coverage entries, or null or empty list
      * @return a new covera table instance
      */
-    public static GlyphCoverageTable createCoverageTable ( List coverage ) {
-        GlyphCoverageTable ct;
-        if ( ( coverage == null ) || ( coverage.size() == 0 ) ) {
-            ct = new EmptyCoverageTable ( coverage );
-        } else if ( isMappedCoverage ( coverage ) ) {
-            ct = new MappedCoverageTable ( coverage );
-        } else if ( isRangeCoverage ( coverage ) ) {
-            ct = new RangeCoverageTable ( coverage );
+    public static GlyphCoverageTable createCoverageTable ( List entries ) {
+        GlyphCoverageMapping cm;
+        if ( ( entries == null ) || ( entries.size() == 0 ) ) {
+            cm = new EmptyCoverageTable ( entries );
+        } else if ( isMappedCoverage ( entries ) ) {
+            cm = new MappedCoverageTable ( entries );
+        } else if ( isRangeCoverage ( entries ) ) {
+            cm = new RangeCoverageTable ( entries );
         } else {
-            ct = null;
+            cm = null;
         }
-        assert ct != null : "unknown coverage type";
-        return ct;
+        assert cm != null : "unknown coverage type";
+        return new GlyphCoverageTable ( cm );
     }
 
-    private static boolean isMappedCoverage ( List coverage ) {
-        if ( ( coverage == null ) || ( coverage.size() == 0 ) ) {
+    private static boolean isMappedCoverage ( List entries ) {
+        if ( ( entries == null ) || ( entries.size() == 0 ) ) {
             return false;
         } else {
-            for ( Iterator it = coverage.iterator(); it.hasNext();) {
+            for ( Iterator it = entries.iterator(); it.hasNext();) {
                 Object o = it.next();
                 if ( ! ( o instanceof Integer ) ) {
                     return false;
@@ -96,13 +104,13 @@ public abstract class GlyphCoverageTable {
         }
     }
 
-    private static boolean isRangeCoverage ( List coverage ) {
-        if ( ( coverage == null ) || ( coverage.size() == 0 ) ) {
+    private static boolean isRangeCoverage ( List entries ) {
+        if ( ( entries == null ) || ( entries.size() == 0 ) ) {
             return false;
         } else {
-            for ( Iterator it = coverage.iterator(); it.hasNext();) {
+            for ( Iterator it = entries.iterator(); it.hasNext();) {
                 Object o = it.next();
-                if ( ! ( o instanceof CoverageRange ) ) {
+                if ( ! ( o instanceof MappingRange ) ) {
                     return false;
                 }
             }
@@ -110,28 +118,26 @@ public abstract class GlyphCoverageTable {
         }
     }
 
-    private static class EmptyCoverageTable extends GlyphCoverageTable {
-        public EmptyCoverageTable ( List coverage ) {
+    private static class EmptyCoverageTable extends GlyphMappingTable.EmptyMappingTable implements GlyphCoverageMapping {
+        public EmptyCoverageTable ( List entries ) {
+            super ( entries );
         }
-        public int getType() {
-            return GLYPH_COVERAGE_TYPE_EMPTY;
-        }
-        public List getEntries() {
-            return new java.util.ArrayList();
+        /** {@inheritDoc} */
+        public int getCoverageSize() {
+            return 0;
         }
+        /** {@inheritDoc} */
         public int getCoverageIndex ( int gid ) {
             return -1;
         }
     }
 
-    private static class MappedCoverageTable extends GlyphCoverageTable {
-        private int[] map = null;
-        public MappedCoverageTable ( List coverage ) {
-            populate ( coverage );
-        }
-        public int getType() {
-            return GLYPH_COVERAGE_TYPE_MAPPED;
+    private static class MappedCoverageTable extends GlyphMappingTable.MappedMappingTable implements GlyphCoverageMapping {
+        private int[] map;
+        public MappedCoverageTable ( List entries ) {
+            populate ( entries );
         }
+        /** {@inheritDoc} */
         public List getEntries() {
             List entries = new java.util.ArrayList();
             if ( map != null ) {
@@ -141,7 +147,11 @@ public abstract class GlyphCoverageTable {
             }
             return entries;
         }
-        public int getCoverageIndex ( int gid ) {
+        /** {@inheritDoc} */
+        public int getMappingSize() {
+            return ( map != null ) ? map.length : 0;
+        }
+        public int getMappedIndex ( int gid ) {
             int i;
             if ( ( i = Arrays.binarySearch ( map, gid ) ) >= 0 ) {
                 return i;
@@ -149,10 +159,18 @@ public abstract class GlyphCoverageTable {
                 return -1;
             }
         }
-        private void populate ( List coverage ) {
-            int i = 0, n = coverage.size(), gidMax = -1;
+        /** {@inheritDoc} */
+        public int getCoverageSize() {
+            return getMappingSize();
+        }
+        /** {@inheritDoc} */
+        public int getCoverageIndex ( int gid ) {
+            return getMappedIndex ( gid );
+        }
+        private void populate ( List entries ) {
+            int i = 0, n = entries.size(), gidMax = -1;
             int[] map = new int [ n ];
-            for ( Iterator it = coverage.iterator(); it.hasNext();) {
+            for ( Iterator it = entries.iterator(); it.hasNext();) {
                 Object o = it.next();
                 if ( o instanceof Integer ) {
                     int gid = ( (Integer) o ) . intValue();
@@ -173,6 +191,7 @@ public abstract class GlyphCoverageTable {
             assert this.map == null;
             this.map = map;
         }
+        /** {@inheritDoc} */
         public String toString() {
             StringBuffer sb = new StringBuffer();
             sb.append('{');
@@ -187,171 +206,22 @@ public abstract class GlyphCoverageTable {
         }
     }
 
-    private static class RangeCoverageTable extends GlyphCoverageTable {
-        private int[] sa = null;                                                // array of ranges starts
-        private int[] ea = null;                                                // array of range ends
-        private int[] ca = null;                                                // array of range coverage (start) indices
-        public RangeCoverageTable ( List coverage ) {
-            populate ( coverage );
+    private static class RangeCoverageTable extends GlyphMappingTable.RangeMappingTable implements GlyphCoverageMapping {
+        public RangeCoverageTable ( List entries ) {
+            super ( entries );
         }
-        public int getType() {
-            return GLYPH_COVERAGE_TYPE_RANGE;
+        /** {@inheritDoc} */
+        public int getMappedIndex ( int gid, int s, int m ) {
+            return m + gid - s;
         }
-        public List getEntries() {
-            List entries = new java.util.ArrayList();
-            if ( sa != null ) {
-                for ( int i = 0, n = sa.length; i < n; i++ ) {
-                    entries.add ( new CoverageRange ( sa [ i ], ea [ i ], ca [ i ] ) );
-                }
-            }
-            return entries;
+        /** {@inheritDoc} */
+        public int getCoverageSize() {
+            return getMappingSize();
         }
+        /** {@inheritDoc} */
         public int getCoverageIndex ( int gid ) {
-            int i, ci;
-            if ( ( i = Arrays.binarySearch ( sa, gid ) ) >= 0 ) {
-                ci = ca [ i ] + gid - sa [ i ];                         // matches start of (some) range
-            } else if ( ( i = - ( i + 1 ) ) == 0 ) {
-                ci = -1;                                                // precedes first range 
-            } else if ( gid > ea [ --i ] ) {
-                ci = -1;                                                // follows preceding (or last) range
-            } else {
-                ci = ca [ i ] + gid - sa [ i ];                         // intersects (some) range
-            }
-            return ci;
-        }
-        private void populate ( List coverage ) {
-            int i = 0, n = coverage.size(), gidMax = -1;
-            int[] sa = new int [ n ];
-            int[] ea = new int [ n ];
-            int[] ca = new int [ n ];
-            for ( Iterator it = coverage.iterator(); it.hasNext();) {
-                Object o = it.next();
-                if ( o instanceof CoverageRange ) {
-                    CoverageRange r = (CoverageRange) o;
-                    int gs = r.getStart();
-                    int ge = r.getEnd();
-                    int ci = r.getIndex();
-                    if ( ( gs < 0 ) || ( gs > 65535 ) ) {
-                        throw new IllegalArgumentException ( "illegal glyph range: [" + gs + "," + ge + "]: bad start index" );
-                    } else if ( ( ge < 0 ) || ( ge > 65535 ) ) {
-                        throw new IllegalArgumentException ( "illegal glyph range: [" + gs + "," + ge + "]: bad end index" );
-                    } else if ( gs > ge ) {
-                        throw new IllegalArgumentException ( "illegal glyph range: [" + gs + "," + ge + "]: start index exceeds end index" );
-                    } else if ( gs < gidMax ) {
-                        throw new IllegalArgumentException ( "out of order glyph range: [" + gs + "," + ge + "]" );
-                    } else if ( ci < 0 ) {
-                        throw new IllegalArgumentException ( "illegal coverage index: " + ci );
-                    } else {
-                        sa [ i ] = gs;
-                        ea [ i ] = gidMax = ge;
-                        ca [ i ] = ci;
-                        i++;
-                    }
-                } else {
-                    throw new IllegalArgumentException ( "illegal coverage entry, must be Integer: " + o );
-                }
-            }
-            assert i == n;
-            assert this.sa == null;
-            assert this.ea == null;
-            assert this.ca == null;
-            this.sa = sa;
-            this.ea = ea;
-            this.ca = ca;
-        }
-        public String toString() {
-            StringBuffer sb = new StringBuffer();
-            sb.append('{');
-            for ( int i = 0, n = sa.length; i < n; i++ ) {
-                if ( i > 0 ) {
-                    sb.append(',');
-                }
-                sb.append ( '[' );
-                sb.append ( Integer.toString ( sa [ i ] ) );
-                sb.append ( Integer.toString ( ea [ i ] ) );
-                sb.append ( "]:" );
-                sb.append ( Integer.toString ( ca [ i ] ) );
-            }
-            sb.append('}');
-            return sb.toString();
-        }
-    }
-
-    /**
-     * The <code>CoverageRange</code> class encapsulates a glyph [start,end] range and
-     * a coverage index.
-     */
-    public static class CoverageRange {
-
-        private final int gidStart;                     // first glyph in range (inclusive)
-        private final int gidEnd;                       // last glyph in range (inclusive)
-        private final int index;                        // coverage index;
-
-        /**
-         * Instantiate a coverage range.
-         */
-        public CoverageRange() {
-            this ( 0, 0, 0 );
+            return getMappedIndex ( gid );
         }
-
-        /**
-         * Instantiate a specific coverage range.
-         * @param gidStart start of range
-         * @param gidEnd end of range
-         * @param index coverage index
-         */
-        public CoverageRange ( int gidStart, int gidEnd, int index ) {
-            if ( ( gidStart < 0 ) || ( gidEnd < 0 ) || ( index < 0 ) ) {
-                throw new IllegalArgumentException();
-            } else if ( gidStart > gidEnd ) {
-                throw new IllegalArgumentException();
-            } else {
-                this.gidStart = gidStart;
-                this.gidEnd = gidEnd;
-                this.index = index;
-            }
-        }
-
-        /** @return start of range */
-        public int getStart() {
-            return gidStart;
-        }
-
-        /** @return end of range */
-        public int getEnd() {
-            return gidEnd;
-        }
-
-        /** @return coverage index */
-        public int getIndex() {
-            return index;
-        }
-
-        /** @return interval as a pair of integers */
-        public int[] getInterval() {
-            return new int[] { gidStart, gidEnd };
-        }
-
-        /**
-         * Obtain interval, filled into first two elements of specified array, or returning new array.
-         * @param interval an array of length two or greater or null
-         * @return interval as a pair of integers, filled into specified array
-         */
-        public int[] getInterval ( int[] interval ) {
-            if ( ( interval == null ) || ( interval.length != 2 ) ) {
-                throw new IllegalArgumentException();
-            } else {
-                interval[0] = gidStart;
-                interval[1] = gidEnd;
-            }
-            return interval;
-        }
-
-        /** @return length of interval */
-        public int getLength() {
-            return gidStart - gidEnd;
-        }
-
     }
 
 }
diff --git a/src/java/org/apache/fop/fonts/GlyphDefinition.java b/src/java/org/apache/fop/fonts/GlyphDefinition.java
new file mode 100644 (file)
index 0000000..e7cd4ad
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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.fonts;
+
+// CSOFF: LineLengthCheck
+
+/**
+ * The <code>GlyphDefinition</code> interface is a marker interface implemented by a glyph definition
+ * subtable.
+ * @author Glenn Adams
+ */
+public interface GlyphDefinition {
+
+    /**
+     * Determine if some definition is available for a specific glyph.
+     * @param gi a glyph index
+     * @return true if some (unspecified) definition is available for the specified glyph
+     */
+    boolean hasDefinition ( int gi );
+
+}
diff --git a/src/java/org/apache/fop/fonts/GlyphDefinitionSubtable.java b/src/java/org/apache/fop/fonts/GlyphDefinitionSubtable.java
new file mode 100644 (file)
index 0000000..74c6a42
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * 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.fonts;
+
+// CSOFF: LineLengthCheck
+// CSOFF: InnerAssignmentCheck
+
+/**
+ * The <code>GlyphDefinitionSubtable</code> implements an abstract base of a glyph definition subtable,
+ * providing a default implementation of the <code>GlyphDefinition</code> interface.
+ * @author Glenn Adams
+ */
+public abstract class GlyphDefinitionSubtable extends GlyphSubtable implements GlyphDefinition {
+
+    /**
+     * Instantiate a <code>GlyphDefinitionSubtable</code>.
+     * @param id subtable identifier
+     * @param sequence subtable sequence
+     * @param flags subtable flags
+     * @param format subtable format
+     * @param mapping subtable coverage table
+     */
+    protected GlyphDefinitionSubtable ( String id, int sequence, int flags, int format, GlyphMappingTable mapping ) {
+        super ( id, sequence, flags, format, mapping );
+    }
+
+    /** {@inheritDoc} */
+    public int getTableType() {
+        return GlyphTable.GLYPH_TABLE_TYPE_DEFINITION;
+    }
+
+    /** {@inheritDoc} */
+    public String getTypeName() {
+        return GlyphDefinitionTable.getLookupTypeName ( getType() );
+    }
+
+    /** {@inheritDoc} */
+    public boolean usesReverseScan() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public boolean hasDefinition ( int gi ) {
+        GlyphCoverageMapping cvm;
+        if ( ( cvm = getCoverage() ) != null ) {
+            if ( cvm.getCoverageIndex ( gi ) >= 0 ) {
+                return true;
+            }
+        }
+        GlyphClassMapping clm;
+        if ( ( clm = getClasses() ) != null ) {
+            if ( clm.getClassIndex ( gi, 0 ) >= 0 ) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/src/java/org/apache/fop/fonts/GlyphDefinitionTable.java b/src/java/org/apache/fop/fonts/GlyphDefinitionTable.java
new file mode 100644 (file)
index 0000000..fe56575
--- /dev/null
@@ -0,0 +1,347 @@
+/*
+ * 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.fonts;
+
+import java.nio.CharBuffer;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+// CSOFF: InnerAssignmentCheck
+// CSOFF: LineLengthCheck
+
+/**
+ * The <code>GlyphDefinitionTable</code> class is a glyph table that implements
+ * glyph definition functionality according to the OpenType GDEF table.
+ * @author Glenn Adams
+ */
+public class GlyphDefinitionTable extends GlyphTable {
+
+    /** logging instance */
+    private static final Log log = LogFactory.getLog(GlyphDefinitionTable.class);                                       // CSOK: ConstantNameCheck
+
+    /** glyph class subtable type */
+    public static final int GDEF_LOOKUP_TYPE_GLYPH_CLASS = 1;
+    /** attachment point subtable type */
+    public static final int GDEF_LOOKUP_TYPE_ATTACHMENT_POINT = 2;
+    /** ligature caret subtable type */
+    public static final int GDEF_LOOKUP_TYPE_LIGATURE_CARET = 3;
+    /** mark attachment subtable type */
+    public static final int GDEF_LOOKUP_TYPE_MARK_ATTACHMENT = 4;
+
+    /** pre-defined glyph class - base glyph */
+    public static final int GLYPH_CLASS_BASE = 1;
+    /** pre-defined glyph class - ligature glyph */
+    public static final int GLYPH_CLASS_LIGATURE = 2;
+    /** pre-defined glyph class - mark glyph */
+    public static final int GLYPH_CLASS_MARK = 3;
+    /** pre-defined glyph class - component glyph */
+    public static final int GLYPH_CLASS_COMPONENT = 4;
+
+    /** singleton glyph class table */
+    private GlyphClassSubtable gct;
+    /** singleton attachment point table */
+    // private AttachmentPointSubtable apt;
+    /** singleton ligature caret table */
+    // private LigatureCaretSubtable lct;
+    /** singleton mark attachment table */
+    // private MarkAttachmentSubtable mat;
+
+    /**
+     * Instantiate a <code>GlyphDefinitionTable</code> object using the specified subtables.
+     * @param subtables a list of identified subtables
+     */
+    public GlyphDefinitionTable ( List subtables ) {
+        super ( null, new HashMap(0) );
+        if ( ( subtables == null ) || ( subtables.size() == 0 ) ) {
+            throw new IllegalArgumentException ( "subtables must be non-empty" );
+        } else {
+            for ( Iterator it = subtables.iterator(); it.hasNext();) {
+                Object o = it.next();
+                if ( o instanceof GlyphDefinitionSubtable ) {
+                    addSubtable ( (GlyphSubtable) o );
+                } else {
+                    throw new IllegalArgumentException ( "subtable must be a glyph definition subtable" );
+                }
+            }
+            freezeSubtables();
+        }
+    }
+
+    /** {@inheritDoc} */
+    protected void addSubtable ( GlyphSubtable subtable ) {
+        if ( subtable instanceof GlyphClassSubtable ) {
+            this.gct = (GlyphClassSubtable) subtable;
+        } else if ( subtable instanceof AttachmentPointSubtable ) {
+            // TODO - not yet used
+            // this.apt = (AttachmentPointSubtable) subtable;
+        } else if ( subtable instanceof LigatureCaretSubtable ) {
+            // TODO - not yet used
+            // this.lct = (LigatureCaretSubtable) subtable;
+        } else if ( subtable instanceof MarkAttachmentSubtable ) {
+            // TODO - not yet used
+            // this.mat = (MarkAttachmentSubtable) subtable;
+        } else {
+            throw new UnsupportedOperationException ( "unsupported glyph definition subtable type: " + subtable );
+        }
+    }
+
+    /**
+     * Determine if glyph belongs to pre-defined glyph class.
+     * @param gid a glyph identifier (index)
+     * @param gc a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT).
+     * @return true if glyph belongs to specified glyph class
+     */
+    public boolean isGlyphClass ( int gid, int gc ) {
+        if ( gct != null ) {
+            return gct.isGlyphClass ( gid, gc );
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Map a lookup type name to its constant (integer) value.
+     * @param name lookup type name
+     * @return lookup type
+     */
+    public static int getLookupTypeFromName ( String name ) {
+        int t;
+        String s = name.toLowerCase();
+        if ( "glyphclass".equals ( s ) ) {
+            t = GDEF_LOOKUP_TYPE_GLYPH_CLASS;
+        } else if ( "attachmentpoint".equals ( s ) ) {
+            t = GDEF_LOOKUP_TYPE_ATTACHMENT_POINT;
+        } else if ( "ligaturecaret".equals ( s ) ) {
+            t = GDEF_LOOKUP_TYPE_LIGATURE_CARET;
+        } else if ( "markattachment".equals ( s ) ) {
+            t = GDEF_LOOKUP_TYPE_MARK_ATTACHMENT;
+        } else {
+            t = -1;
+        }
+        return t;
+    }
+
+    /**
+     * Map a lookup type constant (integer) value to its name.
+     * @param type lookup type
+     * @return lookup type name
+     */
+    public static String getLookupTypeName ( int type ) {
+        String tn = null;
+        switch ( type ) {
+        case GDEF_LOOKUP_TYPE_GLYPH_CLASS:
+            tn = "glyphclass";
+            break;
+        case GDEF_LOOKUP_TYPE_ATTACHMENT_POINT:
+            tn = "attachmentpoint";
+            break;
+        case GDEF_LOOKUP_TYPE_LIGATURE_CARET:
+            tn = "ligaturecaret";
+            break;
+        case GDEF_LOOKUP_TYPE_MARK_ATTACHMENT:
+            tn = "markattachment";
+            break;
+        default:
+            tn = "unknown";
+            break;
+        }
+        return tn;
+    }
+
+    /**
+     * Create a definition subtable according to the specified arguments.
+     * @param type subtable type
+     * @param id subtable identifier
+     * @param sequence subtable sequence
+     * @param flags subtable flags (must be zero)
+     * @param format subtable format
+     * @param mapping subtable mapping table
+     * @param entries subtable entries
+     * @return a glyph subtable instance
+     */
+    public static GlyphSubtable createSubtable ( int type, String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+        GlyphSubtable st = null;
+        switch ( type ) {
+        case GDEF_LOOKUP_TYPE_GLYPH_CLASS:
+            st = GlyphClassSubtable.create ( id, sequence, flags, format, mapping, entries );
+            break;
+        case GDEF_LOOKUP_TYPE_ATTACHMENT_POINT:
+            st = AttachmentPointSubtable.create ( id, sequence, flags, format, mapping, entries );
+            break;
+        case GDEF_LOOKUP_TYPE_LIGATURE_CARET:
+            st = LigatureCaretSubtable.create ( id, sequence, flags, format, mapping, entries );
+            break;
+        case GDEF_LOOKUP_TYPE_MARK_ATTACHMENT:
+            st = MarkAttachmentSubtable.create ( id, sequence, flags, format, mapping, entries );
+            break;
+        default:
+            break;
+        }
+        return st;
+    }
+
+    private abstract static class GlyphClassSubtable extends GlyphDefinitionSubtable {
+        GlyphClassSubtable ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+            super ( id, sequence, flags, format, mapping );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GDEF_LOOKUP_TYPE_GLYPH_CLASS;
+        }
+        /**
+         * Determine if glyph belongs to pre-defined glyph class.
+         * @param gid a glyph identifier (index)
+         * @param gc a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT).
+         * @return true if glyph belongs to specified glyph class
+         */
+        public abstract boolean isGlyphClass ( int gid, int gc );
+        static GlyphDefinitionSubtable create ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+            if ( format == 1 ) {
+                return new GlyphClassSubtableFormat1 ( id, sequence, flags, format, mapping, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class GlyphClassSubtableFormat1 extends GlyphClassSubtable {
+        GlyphClassSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+            super ( id, sequence, flags, format, mapping, entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            return null;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof GlyphClassSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean isGlyphClass ( int gid, int gc ) {
+            GlyphClassMapping cm = getClasses();
+            if ( cm != null ) {
+                return cm.getClassIndex ( gid, 0 ) == gc;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    private abstract static class AttachmentPointSubtable extends GlyphDefinitionSubtable {
+        AttachmentPointSubtable ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+            super ( id, sequence, flags, format, mapping );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GDEF_LOOKUP_TYPE_ATTACHMENT_POINT;
+        }
+        static GlyphDefinitionSubtable create ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+            if ( format == 1 ) {
+                return new AttachmentPointSubtableFormat1 ( id, sequence, flags, format, mapping, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class AttachmentPointSubtableFormat1 extends AttachmentPointSubtable {
+        AttachmentPointSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+            super ( id, sequence, flags, format, mapping, entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            return null;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof AttachmentPointSubtable;
+        }
+    }
+
+    private abstract static class LigatureCaretSubtable extends GlyphDefinitionSubtable {
+        LigatureCaretSubtable ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+            super ( id, sequence, flags, format, mapping );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GDEF_LOOKUP_TYPE_LIGATURE_CARET;
+        }
+        static GlyphDefinitionSubtable create ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+            if ( format == 1 ) {
+                return new LigatureCaretSubtableFormat1 ( id, sequence, flags, format, mapping, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class LigatureCaretSubtableFormat1 extends LigatureCaretSubtable {
+        LigatureCaretSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+            super ( id, sequence, flags, format, mapping, entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            return null;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof LigatureCaretSubtable;
+        }
+    }
+
+    private abstract static class MarkAttachmentSubtable extends GlyphDefinitionSubtable {
+        MarkAttachmentSubtable ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+            super ( id, sequence, flags, format, mapping );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GDEF_LOOKUP_TYPE_MARK_ATTACHMENT;
+        }
+        static GlyphDefinitionSubtable create ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+            if ( format == 1 ) {
+                return new MarkAttachmentSubtableFormat1 ( id, sequence, flags, format, mapping, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class MarkAttachmentSubtableFormat1 extends MarkAttachmentSubtable {
+        MarkAttachmentSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) {
+            super ( id, sequence, flags, format, mapping, entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            return null;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof MarkAttachmentSubtable;
+        }
+    }
+
+}
diff --git a/src/java/org/apache/fop/fonts/GlyphMappingTable.java b/src/java/org/apache/fop/fonts/GlyphMappingTable.java
new file mode 100644 (file)
index 0000000..55dbbf4
--- /dev/null
@@ -0,0 +1,322 @@
+/*
+ * 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.fonts;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Iterator;
+
+// CSOFF: NoWhitespaceAfterCheck
+// CSOFF: InnerAssignmentCheck
+// CSOFF: LineLengthCheck
+
+/**
+ * Base class implementation of glyph mapping table. This base
+ * class maps glyph indices to arbitrary integers (mappping indices), and
+ * is used to implement both glyph coverage and glyph class maps.
+ * @author Glenn Adams
+ */
+public class GlyphMappingTable {
+
+    /** empty mapping table */
+    public static final int GLYPH_MAPPING_TYPE_EMPTY = 0;
+
+    /** mapped mapping table */
+    public static final int GLYPH_MAPPING_TYPE_MAPPED = 1;
+
+    /** range based mapping table */
+    public static final int GLYPH_MAPPING_TYPE_RANGE = 2;
+
+    /**
+     * Obtain mapping type.
+     * @return mapping format type
+     */
+    public int getType() {
+        return -1;
+    }
+
+    /**
+     * Obtain mapping entries.
+     * @return list of mapping entries
+     */
+    public List getEntries() {
+        return null;
+    }
+
+    /**
+     * Obtain size of mapping table, i.e., ciMax + 1, where ciMax is the maximum
+     * mapping index.
+     * @return size of mapping table
+     */
+    public int getMappingSize() {
+        return 0;
+    }
+
+    /**
+     * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of
+     * the mapping table.
+     * @param gid glyph identifier (code)
+     * @return non-negative glyph mapping index or -1 if glyph identifiers is not mapped by table
+     */
+    public int getMappedIndex ( int gid ) {
+        return -1;
+    }
+
+    /** empty mapping table base class */
+    protected static class EmptyMappingTable extends GlyphMappingTable {
+        /**
+         * Construct empty mapping table.
+         */
+        public EmptyMappingTable() {
+            this ( (List) null );
+        }
+        /**
+         * Construct empty mapping table with entries (ignored).
+         * @param entries list of entries (ignored)
+         */
+        public EmptyMappingTable ( List entries ) {
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GLYPH_MAPPING_TYPE_EMPTY;
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            return new java.util.ArrayList();
+        }
+        /** {@inheritDoc} */
+        public int getMappingSize() {
+            return 0;
+        }
+        /** {@inheritDoc} */
+        public int getMappedIndex ( int gid ) {
+            return -1;
+        }
+    }
+
+    /** mapped mapping table base class */
+    protected static class MappedMappingTable extends GlyphMappingTable {
+        /**
+         * Construct mapped mapping table.
+         */
+        public MappedMappingTable() {
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GLYPH_MAPPING_TYPE_MAPPED;
+        }
+    }
+
+    /** range mapping table base class */
+    protected abstract static class RangeMappingTable extends GlyphMappingTable {
+        private int[] sa = null;                                                // array of range (inclusive) starts
+        private int[] ea = null;                                                // array of range (inclusive) ends
+        private int[] ma = null;                                                // array of range mapped values
+        private int miMax = -1;
+        /**
+         * Construct range mapping table.
+         * @param entries of mapping ranges
+         */
+        public RangeMappingTable ( List entries ) {
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GLYPH_MAPPING_TYPE_RANGE;
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            List entries = new java.util.ArrayList();
+            if ( sa != null ) {
+                for ( int i = 0, n = sa.length; i < n; i++ ) {
+                    entries.add ( new MappingRange ( sa [ i ], ea [ i ], ma [ i ] ) );
+                }
+            }
+            return entries;
+        }
+        /** {@inheritDoc} */
+        public int getMappingSize() {
+            return miMax + 1;
+        }
+        /** {@inheritDoc} */
+        public int getMappedIndex ( int gid ) {
+            int i, mi;
+            if ( ( i = Arrays.binarySearch ( sa, gid ) ) >= 0 ) {
+                mi = getMappedIndex ( gid, sa [ i ], ma [ i ] );                // matches start of (some) range
+            } else if ( ( i = - ( i + 1 ) ) == 0 ) {
+                mi = -1;                                                        // precedes first range 
+            } else if ( gid > ea [ --i ] ) {
+                mi = -1;                                                        // follows preceding (or last) range
+            } else {
+                mi = getMappedIndex ( gid, sa [ i ], ma [ i ] );                // intersects (some) range
+            }
+            return mi;
+        }
+        /**
+         * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of
+         * the mapping table.
+         * @param gid glyph identifier (code)
+         * @param s start of range
+         * @param m mapping value
+         * @return non-negative glyph mapping index or -1 if glyph identifiers is not mapped by table
+         */
+        public abstract int getMappedIndex ( int gid, int s, int m );
+        private void populate ( List entries ) {
+            int i = 0, n = entries.size(), gidMax = -1, miMax = -1;
+            int[] sa = new int [ n ];
+            int[] ea = new int [ n ];
+            int[] ma = new int [ n ];
+            for ( Iterator it = entries.iterator(); it.hasNext();) {
+                Object o = it.next();
+                if ( o instanceof MappingRange ) {
+                    MappingRange r = (MappingRange) o;
+                    int gs = r.getStart();
+                    int ge = r.getEnd();
+                    int mi = r.getIndex();
+                    if ( ( gs < 0 ) || ( gs > 65535 ) ) {
+                        throw new IllegalArgumentException ( "illegal glyph range: [" + gs + "," + ge + "]: bad start index" );
+                    } else if ( ( ge < 0 ) || ( ge > 65535 ) ) {
+                        throw new IllegalArgumentException ( "illegal glyph range: [" + gs + "," + ge + "]: bad end index" );
+                    } else if ( gs > ge ) {
+                        throw new IllegalArgumentException ( "illegal glyph range: [" + gs + "," + ge + "]: start index exceeds end index" );
+                    } else if ( gs < gidMax ) {
+                        throw new IllegalArgumentException ( "out of order glyph range: [" + gs + "," + ge + "]" );
+                    } else if ( mi < 0 ) {
+                        throw new IllegalArgumentException ( "illegal mapping index: " + mi );
+                    } else {
+                        int miLast;
+                        sa [ i ] = gs;
+                        ea [ i ] = gidMax = ge;
+                        ma [ i ] = mi;
+                        if ( ( miLast = mi + ( ge - gs ) ) > miMax ) {
+                            miMax = miLast;
+                        }
+                        i++;
+                    }
+                } else {
+                    throw new IllegalArgumentException ( "illegal mapping entry, must be Integer: " + o );
+                }
+            }
+            assert i == n;
+            assert this.sa == null;
+            assert this.ea == null;
+            assert this.ma == null;
+            this.sa = sa;
+            this.ea = ea;
+            this.ma = ma;
+            this.miMax = miMax;
+        }
+        /** {@inheritDoc} */
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            sb.append('{');
+            for ( int i = 0, n = sa.length; i < n; i++ ) {
+                if ( i > 0 ) {
+                    sb.append(',');
+                }
+                sb.append ( '[' );
+                sb.append ( Integer.toString ( sa [ i ] ) );
+                sb.append ( Integer.toString ( ea [ i ] ) );
+                sb.append ( "]:" );
+                sb.append ( Integer.toString ( ma [ i ] ) );
+            }
+            sb.append('}');
+            return sb.toString();
+        }
+    }
+
+    /**
+     * The <code>MappingRange</code> class encapsulates a glyph [start,end] range and
+     * a mapping index.
+     */
+    public static class MappingRange {
+
+        private final int gidStart;                     // first glyph in range (inclusive)
+        private final int gidEnd;                       // last glyph in range (inclusive)
+        private final int index;                        // mapping index;
+
+        /**
+         * Instantiate a mapping range.
+         */
+        public MappingRange() {
+            this ( 0, 0, 0 );
+        }
+
+        /**
+         * Instantiate a specific mapping range.
+         * @param gidStart start of range
+         * @param gidEnd end of range
+         * @param index mapping index
+         */
+        public MappingRange ( int gidStart, int gidEnd, int index ) {
+            if ( ( gidStart < 0 ) || ( gidEnd < 0 ) || ( index < 0 ) ) {
+                throw new IllegalArgumentException();
+            } else if ( gidStart > gidEnd ) {
+                throw new IllegalArgumentException();
+            } else {
+                this.gidStart = gidStart;
+                this.gidEnd = gidEnd;
+                this.index = index;
+            }
+        }
+
+        /** @return start of range */
+        public int getStart() {
+            return gidStart;
+        }
+
+        /** @return end of range */
+        public int getEnd() {
+            return gidEnd;
+        }
+
+        /** @return mapping index */
+        public int getIndex() {
+            return index;
+        }
+
+        /** @return interval as a pair of integers */
+        public int[] getInterval() {
+            return new int[] { gidStart, gidEnd };
+        }
+
+        /**
+         * Obtain interval, filled into first two elements of specified array, or returning new array.
+         * @param interval an array of length two or greater or null
+         * @return interval as a pair of integers, filled into specified array
+         */
+        public int[] getInterval ( int[] interval ) {
+            if ( ( interval == null ) || ( interval.length != 2 ) ) {
+                throw new IllegalArgumentException();
+            } else {
+                interval[0] = gidStart;
+                interval[1] = gidEnd;
+            }
+            return interval;
+        }
+
+        /** @return length of interval */
+        public int getLength() {
+            return gidStart - gidEnd;
+        }
+
+    }
+
+}
index 1896b21e0211dcabf7e8654b0e5bd9eb5adb61ab..9f00de6664b5e8067769b2f0163707bfc1d2fbd5 100644 (file)
@@ -22,7 +22,7 @@ package org.apache.fop.fonts;
 // CSOFF: LineLengthCheck
 
 /**
- * The <code>GlyphPositioning</code> interface is implemented by a font related object
+ * The <code>GlyphPositioning</code> interface is implemented by a glyph positioning subtable
  * that supports the determination of glyph positioning information based on script and
  * language of the corresponding character content.
  * @author Glenn Adams
@@ -30,13 +30,14 @@ package org.apache.fop.fonts;
 public interface GlyphPositioning {
 
     /**
-     * Perform glyph positioning.
-     * @param gs sequence to map to output glyph sequence
-     * @param script the script associated with the characters corresponding to the glyph sequence
-     * @param language the language associated with the characters corresponding to the glyph sequence
-     * @return array (sequence) of pairs of position [DX,DY] offsets, one pair for each element of
-     * glyph sequence, or null if no non-zero offset applies
+     * Perform glyph positioning at the current index, mutating the positioning state object as required.
+     * Only the context associated with the current index is processed.
+     * @param ps glyph positioning state object
+     * @return true if the glyph subtable applies, meaning that the current context matches the
+     * associated input context glyph coverage table; note that returning true does not mean any position
+     * adjustment occurred; it only means that no further glyph subtables for the current lookup table
+     * should be applied.
      */
-    int[] position ( GlyphSequence gs, String script, String language );
+    boolean position ( GlyphPositioningState ps );
 
 }
diff --git a/src/java/org/apache/fop/fonts/GlyphPositioningState.java b/src/java/org/apache/fop/fonts/GlyphPositioningState.java
new file mode 100644 (file)
index 0000000..b94216b
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * 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.fonts;
+
+// CSOFF: LineLengthCheck
+// CSOFF: ParameterNumberCheck
+
+/**
+ * The <code>GlyphPositioningState</code> implements an state object used during glyph positioning
+ * processing.
+ * @author Glenn Adams
+ */
+
+public class GlyphPositioningState extends GlyphProcessingState {
+
+    /** font size */
+    private int fontSize;
+    /** default advancements */
+    private int[] widths;
+    /** current adjustments */
+    private int[][] adjustments;
+    /** if true, then some adjustment was applied */
+    private boolean adjusted;
+
+    /**
+     * Construct glyph positioning state.
+     * @param gs input glyph sequence
+     * @param script script identifier
+     * @param language language identifier
+     * @param feature feature identifier
+     * @param fontSize font size (in micropoints)
+     * @param widths array of design advancements (in glyph index order)
+     * @param adjustments positioning adjustments to which positioning is applied
+     * @param sct script context tester (or null)
+     */
+    public GlyphPositioningState ( GlyphSequence gs, String script, String language, String feature, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct ) {
+        super ( gs, script, language, feature, sct );
+        this.fontSize = fontSize;
+        this.widths = widths;
+        this.adjustments = adjustments;
+    }
+
+    /**
+     * Construct glyph positioning state using an existing state object using shallow copy
+     * except as follows: input glyph sequence is copied deep except for its characters array.
+     * @param ps existing positioning state to copy from
+     */
+    public GlyphPositioningState ( GlyphPositioningState ps ) {
+        super ( ps );
+        this.fontSize = ps.fontSize;
+        this.widths = ps.widths;
+        this.adjustments = ps.adjustments;
+    }
+
+    /**
+     * Obtain design advancement (width) of glyph at specified index.
+     * @param gi glyph index
+     * @return design advancement, or zero if glyph index is not present
+     */
+    public int getWidth ( int gi ) {
+        if ( ( widths != null ) && ( gi < widths.length ) ) {
+            return widths [ gi ];
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Perform adjustments at current position index.
+     * @param v value containing adjustments
+     * @return true if a non-zero adjustment was made
+     */
+    public boolean adjust ( GlyphPositioningTable.Value v ) {
+        return adjust ( v, 0 );
+    }
+
+    /**
+     * Perform adjustments at specified offset from current position index.
+     * @param v value containing adjustments
+     * @param offset from current position index
+     * @return true if a non-zero adjustment was made
+     */
+    public boolean adjust ( GlyphPositioningTable.Value v, int offset ) {
+        assert v != null;
+        if ( ( index + offset ) < indexLast ) {
+            return v.adjust ( adjustments [ index + offset ], fontSize );
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    /**
+     * Obtain current adjustments at current position index.
+     * @return array of adjustments (int[4]) at current position
+     */
+    public int[] getAdjustment() {
+        return getAdjustment ( 0 );
+    }
+
+    /**
+     * Obtain current adjustments at specified offset from current position index.
+     * @param offset from current position index
+     * @return array of adjustments (int[4]) at specified offset
+     * @throws IndexOutOfBoundsException if offset is invalid
+     */
+    public int[] getAdjustment ( int offset ) throws IndexOutOfBoundsException {
+        if ( ( index + offset ) < indexLast ) {
+            return adjustments [ index + offset ];
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    /**
+     * Apply positioning subtable to current state at current position (only),
+     * resulting in the consumption of zero or more input glyphs.
+     * @param st the glyph positioning subtable to apply
+     * @return true if subtable applied, or false if it did not (e.g., its
+     * input coverage table did not match current input context)
+     */
+    public boolean apply ( GlyphPositioningSubtable st ) {
+        assert st != null;
+        updateSubtableState ( st );
+        boolean applied = st.position ( this );
+        resetSubtableState();
+        return applied;
+    }
+
+    /**
+     * Apply a sequence of matched rule lookups to the <code>nig</code> input glyphs
+     * starting at the current position. If lookups are non-null and non-empty, then
+     * all input glyphs specified by <code>nig</code> are consumed irregardless of
+     * whether any specified lookup applied.
+     * @param lookups array of matched lookups (or null)
+     * @param nig number of glyphs in input sequence, starting at current position, to which
+     * the lookups are to apply, and to be consumed once the application has finished
+     * @return true if lookups are non-null and non-empty; otherwise, false
+     */
+    public boolean apply ( GlyphTable.RuleLookup[] lookups, int nig ) {
+        if ( ( lookups != null ) && ( lookups.length > 0 ) ) {
+            // apply each rule lookup to extracted input glyph array
+            for ( int i = 0, n = lookups.length; i < n; i++ ) {
+                GlyphTable.RuleLookup l = lookups [ i ];
+                if ( l != null ) {
+                    GlyphTable.LookupTable lt = l.getLookup();
+                    if ( lt != null ) {
+                        // perform positioning on a copy of previous state
+                        GlyphPositioningState ps = new GlyphPositioningState ( this );
+                        // apply lookup table positioning
+                        if ( lt.position ( ps, l.getSequenceIndex() ) ) {
+                            setAdjusted ( true );
+                        }
+                    }
+                }
+            }
+            consume ( nig );
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Apply default application semantices; namely, consume one input glyph.
+     */
+    public void applyDefault() {
+        super.applyDefault();
+    }
+
+    /**
+     * Set adjusted state, used to record effect of non-zero adjustment.
+     * @param adjusted true if to set adjusted state, otherwise false to
+     * clear adjusted state
+     */
+    public void setAdjusted ( boolean adjusted ) {
+        this.adjusted = adjusted;
+    }
+
+    /**
+     * Get adjusted state.
+     * @return adjusted true if some non-zero adjustment occurred and
+     * was recorded by {@link #setAdjusted}; otherwise, false.
+     */
+    public boolean getAdjusted() {
+        return adjusted;
+    }
+
+}
index c8656a3a787969979adf8c4745dea75e9948647e..b1cdfa9f341a12cb9bb3ede1a17868eb7a255ce2 100644 (file)
 
 package org.apache.fop.fonts;
 
+import java.util.List;
+
 // CSOFF: LineLengthCheck
+// CSOFF: NoWhitespaceAfterCheck
+// CSOFF: ParameterNumberCheck
 
 /**
  * The <code>GlyphPositioningSubtable</code> implements an abstract base of a glyph subtable,
@@ -51,12 +55,59 @@ public abstract class GlyphPositioningSubtable extends GlyphSubtable implements
     }
 
     /** {@inheritDoc} */
-    public int[] position ( GlyphSequence gs, String script, String language ) {
-        if ( gs == null ) {
-            throw new IllegalArgumentException ( "invalid glyph sequence: must not be null" );
-        } else {
-            return null;
+    public boolean isCompatible ( GlyphSubtable subtable ) {
+        return subtable instanceof GlyphPositioningSubtable;
+    }
+
+    /** {@inheritDoc} */
+    public boolean usesReverseScan() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public boolean position ( GlyphPositioningState ps ) {
+        return false;
+    }
+
+    /**
+     * Apply positioning using specified state and subtable array. For each position in input sequence,
+     * apply subtables in order until some subtable applies or none remain. If no subtable applied or no
+     * input was consumed for a given position, then apply default action (no adjustments and advance).
+     * If <code>sequenceIndex</code> is non-negative, then apply subtables only when current position
+     * matches <code>sequenceIndex</code> in relation to the starting position. Furthermore, upon
+     * successful application at <code>sequenceIndex</code>, then discontinue processing the remaining
+     * @param ps positioning state
+     * @param sta array of subtables to apply
+     * @param sequenceIndex if non negative, then apply subtables only at specified sequence index
+     * @return true if a non-zero adjustment occurred
+     */
+    static final boolean position ( GlyphPositioningState ps, GlyphPositioningSubtable[] sta, int sequenceIndex ) {
+        int sequenceStart = ps.getPosition();
+        boolean appliedOneShot = false;
+        while ( ps.hasNext() ) {
+            boolean applied = false;
+            if ( ! appliedOneShot && ps.maybeApplicable() ) {
+                for ( int i = 0, n = sta.length; ! applied && ( i < n ); i++ ) {
+                    if ( sequenceIndex < 0 ) {
+                        applied = ps.apply ( sta [ i ] );
+                    } else if ( ps.getPosition() == ( sequenceStart + sequenceIndex ) ) {
+                        applied = ps.apply ( sta [ i ] );
+                        if ( applied ) {
+                            appliedOneShot = true;
+                        }
+                    }
+                }
+            }
+            if ( ! applied || ! ps.didConsume() ) {
+                ps.applyDefault();
+            }
+            ps.next();
         }
+        return ps.getAdjusted();
+    }
+
+    static final boolean position ( GlyphSequence gs, String script, String language, String feature, int fontSize, GlyphPositioningSubtable[] sta, int[] widths, int[][] adjustments, ScriptContextTester sct ) {
+        return position ( new GlyphPositioningState ( gs, script, language, feature, fontSize, widths, adjustments, sct ), sta, -1 );
     }
 
 }
index 85b311c97351c90ab7f85000a724d3cbdff070cb..d752bca0191d63a8e5611fa56ca2c68db537e38a 100644 (file)
 
 package org.apache.fop.fonts;
 
+import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
 // CSOFF: LineLengthCheck
+// CSOFF: InnerAssignmentCheck
+// CSOFF: NoWhitespaceAfterCheck
+// CSOFF: ParameterNumberCheck
 
 /**
  * The <code>GlyphPositioningTable</code> class is a glyph table that implements
  * <code>GlyphPositioning</code> functionality.
  * @author Glenn Adams
  */
-public class GlyphPositioningTable extends GlyphTable implements GlyphPositioning {
+public class GlyphPositioningTable extends GlyphTable {
+
+    /** logging instance */
+    private static final Log log = LogFactory.getLog(GlyphPositioningTable.class);                                      // CSOK: ConstantNameCheck
 
     /** single positioning subtable type */
     public static final int GPOS_LOOKUP_TYPE_SINGLE = 1;
@@ -44,21 +55,22 @@ public class GlyphPositioningTable extends GlyphTable implements GlyphPositionin
     public static final int GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE = 5;
     /** mark to mark positioning subtable type */
     public static final int GPOS_LOOKUP_TYPE_MARK_TO_MARK = 6;
-    /** context positioning subtable type */
-    public static final int GPOS_LOOKUP_TYPE_CONTEXT = 7;
-    /** chained context positioning subtable type */
-    public static final int GPOS_LOOKUP_TYPE_CHAINED_CONTEXT = 8;
+    /** contextual positioning subtable type */
+    public static final int GPOS_LOOKUP_TYPE_CONTEXTUAL = 7;
+    /** chained contextual positioning subtable type */
+    public static final int GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL = 8;
     /** extension positioning subtable type */
     public static final int GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING = 9;
 
     /**
      * Instantiate a <code>GlyphPositioningTable</code> object using the specified lookups
      * and subtables.
+     * @param gdef glyph definition table that applies
      * @param lookups a map of lookup specifications to subtable identifier strings
      * @param subtables a list of identified subtables
      */
-    public GlyphPositioningTable ( Map lookups, List subtables ) {
-        super ( lookups );
+    public GlyphPositioningTable ( GlyphDefinitionTable gdef, Map lookups, List subtables ) {
+        super ( gdef, lookups );
         if ( ( subtables == null ) || ( subtables.size() == 0 ) ) {
             throw new IllegalArgumentException ( "subtables must be non-empty" );
         } else {
@@ -70,6 +82,7 @@ public class GlyphPositioningTable extends GlyphTable implements GlyphPositionin
                     throw new IllegalArgumentException ( "subtable must be a glyph positioning subtable" );
                 }
             }
+            freezeSubtables();
         }
     }
 
@@ -93,10 +106,10 @@ public class GlyphPositioningTable extends GlyphTable implements GlyphPositionin
             t = GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE;
         } else if ( "marktomark".equals ( s ) ) {
             t = GPOS_LOOKUP_TYPE_MARK_TO_MARK;
-        } else if ( "context".equals ( s ) ) {
-            t = GPOS_LOOKUP_TYPE_CONTEXT;
-        } else if ( "chainedcontext".equals ( s ) ) {
-            t = GPOS_LOOKUP_TYPE_CHAINED_CONTEXT;
+        } else if ( "contextual".equals ( s ) ) {
+            t = GPOS_LOOKUP_TYPE_CONTEXTUAL;
+        } else if ( "chainedcontextual".equals ( s ) ) {
+            t = GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL;
         } else if ( "extensionpositioning".equals ( s ) ) {
             t = GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING;
         } else {
@@ -131,11 +144,11 @@ public class GlyphPositioningTable extends GlyphTable implements GlyphPositionin
         case GPOS_LOOKUP_TYPE_MARK_TO_MARK:
             tn = "marktomark";
             break;
-        case GPOS_LOOKUP_TYPE_CONTEXT:
-            tn = "context";
+        case GPOS_LOOKUP_TYPE_CONTEXTUAL:
+            tn = "contextual";
             break;
-        case GPOS_LOOKUP_TYPE_CHAINED_CONTEXT:
-            tn = "chainedcontext";
+        case GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL:
+            tn = "chainedcontextual";
             break;
         case GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING:
             tn = "extensionpositioning";
@@ -158,13 +171,2092 @@ public class GlyphPositioningTable extends GlyphTable implements GlyphPositionin
      * @param entries subtable entries
      * @return a glyph subtable instance
      */
+    public static GlyphSubtable createSubtable ( int type, String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+        GlyphSubtable st = null;
+        switch ( type ) {
+        case GPOS_LOOKUP_TYPE_SINGLE:
+            st = SingleSubtable.create ( id, sequence, flags, format, coverage, entries );
+            break;
+        case GPOS_LOOKUP_TYPE_PAIR:
+            st = PairSubtable.create ( id, sequence, flags, format, coverage, entries );
+            break;
+        case GPOS_LOOKUP_TYPE_CURSIVE:
+            st = CursiveSubtable.create ( id, sequence, flags, format, coverage, entries );
+            break;
+        case GPOS_LOOKUP_TYPE_MARK_TO_BASE:
+            st = MarkToBaseSubtable.create ( id, sequence, flags, format, coverage, entries );
+            break;
+        case GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE:
+            st = MarkToLigatureSubtable.create ( id, sequence, flags, format, coverage, entries );
+            break;
+        case GPOS_LOOKUP_TYPE_MARK_TO_MARK:
+            st = MarkToMarkSubtable.create ( id, sequence, flags, format, coverage, entries );
+            break;
+        case GPOS_LOOKUP_TYPE_CONTEXTUAL:
+            st = ContextualSubtable.create ( id, sequence, flags, format, coverage, entries );
+            break;
+        case GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL:
+            st = ChainedContextualSubtable.create ( id, sequence, flags, format, coverage, entries );
+            break;
+        default:
+            break;
+        }
+        return st;
+    }
+
+    /**
+     * Create a positioning subtable according to the specified arguments.
+     * @param type subtable type
+     * @param id subtable identifier
+     * @param sequence subtable sequence
+     * @param flags subtable flags
+     * @param format subtable format
+     * @param coverage list of coverage table entries
+     * @param entries subtable entries
+     * @return a glyph subtable instance
+     */
     public static GlyphSubtable createSubtable ( int type, String id, int sequence, int flags, int format, List coverage, List entries ) {
-        return null;
+        return createSubtable ( type, id, sequence, flags, format, GlyphCoverageTable.createCoverageTable ( coverage ), entries );
+    }
+
+    /**
+     * Perform positioning processing using all matching lookups.
+     * @param gs an input glyph sequence
+     * @param script a script identifier
+     * @param language a language identifier
+     * @param fontSize size in device units
+     * @param widths array of default advancements for each glyph
+     * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
+     * with one 4-tuple for each element of glyph sequence
+     * @return true if some adjustment is not zero; otherwise, false
+     */
+    public boolean position ( GlyphSequence gs, String script, String language, int fontSize, int[] widths, int[][] adjustments ) {
+        Map/*<LookupSpec,List<LookupTable>>*/ lookups = matchLookups ( script, language, "*" );
+        if ( ( lookups != null ) && ( lookups.size() > 0 ) ) {
+            ScriptProcessor sp = ScriptProcessor.getInstance ( script );
+            return sp.position ( this, gs, script, language, fontSize, lookups, widths, adjustments );
+        } else {
+            return false;
+        }
+    }
+
+    private abstract static class SingleSubtable extends GlyphPositioningSubtable {
+        SingleSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GPOS_LOOKUP_TYPE_SINGLE;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof SingleSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean position ( GlyphPositioningState ps ) {
+            int gi = ps.getGlyph(), ci;
+            if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) {
+                return false;
+            } else {
+                Value v = getValue ( ci, gi );
+                if ( v != null ) {
+                    if ( ps.adjust(v) ) {
+                        ps.setAdjusted ( true );
+                    }
+                    ps.consume(1);
+                }
+                return true;
+            }
+        }
+        /**
+         * Obtain positioning value for coverage index.
+         * @param ci coverage index
+         * @param gi input glyph index
+         * @return positioning value or null if none applies
+         */
+        public abstract Value getValue ( int ci, int gi );
+        static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new SingleSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else if ( format == 2 ) {
+                return new SingleSubtableFormat2 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class SingleSubtableFormat1 extends SingleSubtable {
+        private Value value;
+        private int ciMax;
+        SingleSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( value != null ) {
+                List entries = new ArrayList ( 1 );
+                entries.add ( value );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public Value getValue ( int ci, int gi ) {
+            if ( ( value != null ) && ( ci <= ciMax ) ) {
+                return value;
+            } else {
+                return null;
+            }
+        }
+        private void populate ( List entries ) {
+            if ( ( entries == null ) || ( entries.size() != 1 ) ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null and contain exactly one entry" );
+            } else {
+                Value v;
+                Object o = entries.get(0);
+                if ( o instanceof Value ) {
+                    v = (Value) o;
+                } else {
+                    throw new IllegalArgumentException ( "illegal entries entry, must be Value, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                }
+                assert this.value == null;
+                this.value = v;
+                this.ciMax = getCoverageSize() - 1;
+            }
+        }
+    }
+
+    private static class SingleSubtableFormat2 extends SingleSubtable {
+        private Value[] values;
+        SingleSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( values != null ) {
+                List entries = new ArrayList ( values.length );
+                for ( int i = 0, n = values.length; i < n; i++ ) {
+                    entries.add ( values[i] );
+                }
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public Value getValue ( int ci, int gi ) {
+            if ( ( values != null ) && ( ci < values.length ) ) {
+                return values [ ci ];
+            } else {
+                return null;
+            }
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 1 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof Value[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, single entry must be a Value[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    Value[] va = (Value[]) o;
+                    if ( va.length != getCoverageSize() ) {
+                        throw new IllegalArgumentException ( "illegal values array, " + entries.size() + " values present, but requires " + getCoverageSize() + " values" );
+                    } else {
+                        assert this.values == null;
+                        this.values = va;
+                    }
+                }
+            }
+        }
+    }
+
+    private abstract static class PairSubtable extends GlyphPositioningSubtable {
+        PairSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GPOS_LOOKUP_TYPE_PAIR;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof PairSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean position ( GlyphPositioningState ps ) {
+            int gi = ps.getGlyph(0), ci;
+            if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) {
+                return false;
+            } else {
+                int[] counts = ps.getGlyphsAvailable ( 0 );
+                int nga = counts[0];
+                if ( nga > 1 ) {
+                    int[] iga = ps.getGlyphs ( 0, 2, null, counts );
+                    if ( ( iga != null ) && ( iga.length == 2 ) ) {
+                        PairValues pv = getPairValues ( ci, iga[0], iga[1] );
+                        if ( pv != null ) {
+                            Value v1 = pv.getValue1();
+                            if ( v1 != null ) {
+                                if ( ps.adjust(v1, 0) ) {
+                                    ps.setAdjusted ( true );
+                                }
+                            }
+                            Value v2 = pv.getValue2();
+                            if ( v2 != null ) {
+                                if ( ps.adjust(v2, 1) ) {
+                                    ps.setAdjusted ( true );
+                                }
+                            }
+                            ps.consume ( counts[0] + counts[1] );
+                        }
+                    }
+                }
+                return true;
+            }
+        }
+        /**
+         * Obtain associated pair values.
+         * @param ci coverage index
+         * @param gi1 first input glyph index
+         * @param gi2 second input glyph index
+         * @return pair values or null if none applies
+         */
+        public abstract PairValues getPairValues ( int ci, int gi1, int gi2 );
+        static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new PairSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else if ( format == 2 ) {
+                return new PairSubtableFormat2 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class PairSubtableFormat1 extends PairSubtable {
+        private PairValues[][] pvm;                     // pair values matrix
+        PairSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( pvm != null ) {
+                List entries = new ArrayList ( 1 );
+                entries.add ( pvm );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public PairValues getPairValues ( int ci, int gi1, int gi2 ) {
+            if ( ( pvm != null ) && ( ci < pvm.length ) ) {
+                PairValues[] pvt = pvm [ ci ];
+                for ( int i = 0, n = pvt.length; i < n; i++ ) {
+                    PairValues pv = pvt [ i ];
+                    if ( pv != null ) {
+                        int g = pv.getGlyph();
+                        if ( g < gi2 ) {
+                            continue;
+                        } else if ( g == gi2 ) {
+                            return pv;
+                        } else {
+                            break;
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 1 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof PairValues[][] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first (and only) entry must be a PairValues[][], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    pvm = (PairValues[][]) o;
+                }
+            }
+        }
+    }
+
+    private static class PairSubtableFormat2 extends PairSubtable {
+        private GlyphClassTable cdt1;                   // class def table 1
+        private GlyphClassTable cdt2;                   // class def table 2
+        private int nc1;                                // class 1 count
+        private int nc2;                                // class 2 count
+        private PairValues[][] pvm;                     // pair values matrix
+        PairSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( pvm != null ) {
+                List entries = new ArrayList ( 5 );
+                entries.add ( cdt1 );
+                entries.add ( cdt2 );
+                entries.add ( Integer.valueOf ( nc1 ) );
+                entries.add ( Integer.valueOf ( nc2 ) );
+                entries.add ( pvm );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public PairValues getPairValues ( int ci, int gi1, int gi2 ) {
+            if ( pvm != null ) {
+                int c1 = cdt1.getClassIndex ( gi1, 0 );
+                if ( ( c1 < nc1 ) && ( c1 < pvm.length ) ) {
+                    PairValues[] pvt = pvm [ c1 ];
+                    if ( pvt != null ) {
+                        int c2 = cdt2.getClassIndex ( gi2, 0 );
+                        if ( ( c2 < nc2 ) && ( c2 < pvt.length ) ) {
+                            return pvt [ c2 ];
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 5 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 5 entries" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphClassTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an GlyphClassTable, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    cdt1 = (GlyphClassTable) o;
+                }
+                if ( ( ( o = entries.get(1) ) == null ) || ! ( o instanceof GlyphClassTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, second entry must be an GlyphClassTable, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    cdt2 = (GlyphClassTable) o;
+                }
+                if ( ( ( o = entries.get(2) ) == null ) || ! ( o instanceof Integer ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, third entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    nc1 = ((Integer)(o)).intValue();
+                }
+                if ( ( ( o = entries.get(3) ) == null ) || ! ( o instanceof Integer ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, fourth entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    nc2 = ((Integer)(o)).intValue();
+                }
+                if ( ( ( o = entries.get(4) ) == null ) || ! ( o instanceof PairValues[][] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, fifth entry must be a PairValues[][], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    pvm = (PairValues[][]) o;
+                }
+            }
+        }
+    }
+
+    private abstract static class CursiveSubtable extends GlyphPositioningSubtable {
+        CursiveSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GPOS_LOOKUP_TYPE_CURSIVE;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof CursiveSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean position ( GlyphPositioningState ps ) {
+            int gi = ps.getGlyph(0), ci;
+            if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) {
+                return false;
+            } else {
+                int[] counts = ps.getGlyphsAvailable ( 0 );
+                int nga = counts[0];
+                if ( nga > 1 ) {
+                    int[] iga = ps.getGlyphs ( 0, 2, null, counts );
+                    if ( ( iga != null ) && ( iga.length == 2 ) ) {
+                        // int gi1 = gi;
+                        int ci1 = ci;
+                        int gi2 = iga [ 1 ];
+                        int ci2 = getCoverageIndex ( gi2 );
+                        Anchor[] aa = getExitEntryAnchors ( ci1, ci2 );
+                        if ( aa != null ) {
+                            Anchor exa = aa [ 0 ];
+                            Anchor ena = aa [ 1 ];
+                            // int exw = ps.getWidth ( gi1 );
+                            int enw = ps.getWidth ( gi2 );
+                            if ( ( exa != null ) && ( ena != null ) ) {
+                                Value v = ena.getAlignmentAdjustment ( exa );
+                                v.adjust ( - enw, 0, 0, 0 );
+                                if ( ps.adjust ( v ) ) {
+                                    ps.setAdjusted ( true );
+                                }
+                            }
+                            // consume only first glyph of exit/entry glyph pair
+                            ps.consume ( 1 );
+                        }
+                    }
+                }
+                return true;
+            }
+        }
+        /**
+         * Obtain exit anchor for first glyph with coverage index <code>ci1</code> and entry anchor for second
+         * glyph with coverage index <code>ci2</code>.
+         * @param ci1 coverage index of first glyph (may be negative)
+         * @param ci2 coverage index of second glyph (may be negative)
+         * @return array of two anchors or null if either coverage index is negative or corresponding anchor is
+         * missing, where the first entry is the exit anchor of the first glyph and the second entry is the
+         * entry anchor of the second glyph
+         */
+        public abstract Anchor[] getExitEntryAnchors ( int ci1, int ci2 );
+        static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new CursiveSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class CursiveSubtableFormat1 extends CursiveSubtable {
+        private Anchor[] aa;                            // anchor array, where even entries are entry anchors, and odd entries are exit anchors
+        CursiveSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( aa != null ) {
+                List entries = new ArrayList ( 1 );
+                entries.add ( aa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public Anchor[] getExitEntryAnchors ( int ci1, int ci2 ) {
+            if ( ( ci1 >= 0 ) && ( ci2 >= 0 ) ) {
+                int ai1 = ( ci1 * 2 ) + 1; // ci1 denotes glyph with exit anchor
+                int ai2 = ( ci2 * 2 ) + 0; // ci2 denotes glyph with entry anchor
+                if ( ( aa != null ) && ( ai1 < aa.length ) && ( ai2 < aa.length ) ) {
+                    Anchor exa = aa [ ai1 ];
+                    Anchor ena = aa [ ai2 ];
+                    if ( ( exa != null ) && ( ena != null ) ) {
+                        return new Anchor[] { exa, ena };
+                    }
+                }
+            }
+            return null;
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 1 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof Anchor[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first (and only) entry must be a Anchor[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else if ( ( ( (Anchor[]) o ) . length % 2 ) != 0 ) {
+                    throw new IllegalArgumentException ( "illegal entries, Anchor[] array must have an even number of entries, but has: " + ( (Anchor[]) o ) . length );
+                } else {
+                    aa = (Anchor[]) o;
+                }
+            }
+        }
+    }
+
+    private abstract static class MarkToBaseSubtable extends GlyphPositioningSubtable {
+        MarkToBaseSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GPOS_LOOKUP_TYPE_MARK_TO_BASE;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof MarkToBaseSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean position ( GlyphPositioningState ps ) {
+            int giMark = ps.getGlyph(), ciMark;
+            if ( ( ciMark = getCoverageIndex ( giMark ) ) < 0 ) {
+                return false;
+            } else {
+                MarkAnchor ma = getMarkAnchor ( ciMark, giMark );
+                if ( ma != null ) {
+                    for ( int i = 0, n = ps.getPosition(); i < n; i++ ) {
+                        int gi = ps.getGlyph ( - ( i + 1 ) );
+                        if ( ps.isMark ( gi ) ) {
+                            continue;
+                        } else {
+                            Anchor a = getBaseAnchor ( gi, ma.getMarkClass() );
+                            if ( a != null ) {
+                                Value v = a.getAlignmentAdjustment ( ma );
+                                // start experimental fix for END OF AYAH in Lateef/Scheherazade
+                                int[] aa = ps.getAdjustment();
+                                if ( aa[2] == 0 ) {
+                                    v.adjust ( 0, 0, - ps.getWidth ( giMark ), 0 );
+                                }
+                                // end experimental fix for END OF AYAH in Lateef/Scheherazade
+                                if ( ps.adjust ( v ) ) {
+                                    ps.setAdjusted ( true );
+                                }
+                            }
+                            ps.consume(1);
+                            break;
+                        }
+                    }
+                }
+                return true;
+            }
+        }
+        /**
+         * Obtain mark anchor associated with mark coverage index.
+         * @param ciMark coverage index
+         * @param giMark input glyph index of mark glyph
+         * @return mark anchor or null if none applies
+         */
+        public abstract MarkAnchor getMarkAnchor ( int ciMark, int giMark );
+        /**
+         * Obtain anchor associated with base glyph index and mark class.
+         * @param giBase input glyph index of base glyph
+         * @param markClass class number of mark glyph
+         * @return anchor or null if none applies
+         */
+        public abstract Anchor getBaseAnchor ( int giBase, int markClass );
+        static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new MarkToBaseSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class MarkToBaseSubtableFormat1 extends MarkToBaseSubtable {
+        private GlyphCoverageTable bct;                 // base coverage table
+        private int nmc;                                // mark class count
+        private MarkAnchor[] maa;                       // mark anchor array, ordered by mark coverage index
+        private Anchor[][] bam;                         // base anchor matrix, ordered by base coverage index, then by mark class
+        MarkToBaseSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( ( bct != null ) && ( maa != null ) && ( nmc > 0 ) && ( bam != null ) ) {
+                List entries = new ArrayList ( 4 );
+                entries.add ( bct );
+                entries.add ( Integer.valueOf ( nmc ) );
+                entries.add ( maa );
+                entries.add ( bam );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public MarkAnchor getMarkAnchor ( int ciMark, int giMark ) {
+            if ( ( maa != null ) && ( ciMark < maa.length ) ) {
+                return maa [ ciMark ];
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public Anchor getBaseAnchor ( int giBase, int markClass ) {
+            int ciBase;
+            if ( ( bct != null ) && ( ( ciBase = bct.getCoverageIndex ( giBase ) ) >= 0 ) ) {
+                if ( ( bam != null ) && ( ciBase < bam.length ) ) {
+                    Anchor[] ba = bam [ ciBase ];
+                    if ( ( ba != null ) && ( markClass < ba.length ) ) {
+                        return ba [ markClass ];
+                    }
+                }
+            }
+            return null;
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 4 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 4 entries" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphCoverageTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an GlyphCoverageTable, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    bct = (GlyphCoverageTable) o;
+                }
+                if ( ( ( o = entries.get(1) ) == null ) || ! ( o instanceof Integer ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, second entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    nmc = ((Integer)(o)).intValue();
+                }
+                if ( ( ( o = entries.get(2) ) == null ) || ! ( o instanceof MarkAnchor[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, third entry must be a MarkAnchor[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    maa = (MarkAnchor[]) o;
+                }
+                if ( ( ( o = entries.get(3) ) == null ) || ! ( o instanceof Anchor[][] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, fourth entry must be a Anchor[][], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    bam = (Anchor[][]) o;
+                }
+            }
+        }
+    }
+
+    private abstract static class MarkToLigatureSubtable extends GlyphPositioningSubtable {
+        MarkToLigatureSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof MarkToLigatureSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean position ( GlyphPositioningState ps ) {
+            int giMark = ps.getGlyph(), ciMark;
+            if ( ( ciMark = getCoverageIndex ( giMark ) ) < 0 ) {
+                return false;
+            } else {
+                MarkAnchor ma = getMarkAnchor ( ciMark, giMark );
+                int mxc = getMaxComponentCount();
+                if ( ma != null ) {
+                    for ( int i = 0, n = ps.getPosition(); i < n; i++ ) {
+                        int gi = ps.getGlyph ( - ( i + 1 ) );
+                        if ( ps.isMark ( gi ) ) {
+                            continue;
+                        } else {
+                            Anchor a = getLigatureAnchor ( gi, mxc, i, ma.getMarkClass() );
+                            if ( a != null ) {
+                                if ( ps.adjust ( a.getAlignmentAdjustment ( ma ) ) ) {
+                                    ps.setAdjusted ( true );
+                                }
+                            }
+                            ps.consume(1);
+                            break;
+                        }
+                    }
+                }
+                return true;
+            }
+        }
+        /**
+         * Obtain mark anchor associated with mark coverage index.
+         * @param ciMark coverage index
+         * @param giMark input glyph index of mark glyph
+         * @return mark anchor or null if none applies
+         */
+        public abstract MarkAnchor getMarkAnchor ( int ciMark, int giMark );
+        /**
+         * Obtain maximum component count.
+         * @return maximum component count (>=0)
+         */
+        public abstract int getMaxComponentCount();
+        /**
+         * Obtain anchor associated with ligature glyph index and mark class.
+         * @param giLig input glyph index of ligature glyph
+         * @param maxComponents maximum component count
+         * @param component component number (0...maxComponents-1)
+         * @param markClass class number of mark glyph
+         * @return anchor or null if none applies
+         */
+        public abstract Anchor getLigatureAnchor ( int giLig, int maxComponents, int component, int markClass );
+        static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new MarkToLigatureSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class MarkToLigatureSubtableFormat1 extends MarkToLigatureSubtable {
+        private GlyphCoverageTable lct;                 // ligature coverage table
+        private int nmc;                                // mark class count
+        private int mxc;                                // maximum ligature component count
+        private MarkAnchor[] maa;                       // mark anchor array, ordered by mark coverage index
+        private Anchor[][][] lam;                       // ligature anchor matrix, ordered by ligature coverage index, then ligature component, then mark class
+        MarkToLigatureSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( lam != null ) {
+                List entries = new ArrayList ( 5 );
+                entries.add ( lct );
+                entries.add ( Integer.valueOf ( nmc ) );
+                entries.add ( Integer.valueOf ( mxc ) );
+                entries.add ( maa );
+                entries.add ( lam );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public MarkAnchor getMarkAnchor ( int ciMark, int giMark ) {
+            if ( ( maa != null ) && ( ciMark < maa.length ) ) {
+                return maa [ ciMark ];
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public int getMaxComponentCount() {
+            return mxc;
+        }
+        /** {@inheritDoc} */
+        public Anchor getLigatureAnchor ( int giLig, int maxComponents, int component, int markClass ) {
+            int ciLig;
+            if ( ( lct != null ) && ( ( ciLig = lct.getCoverageIndex ( giLig ) ) >= 0 ) ) {
+                if ( ( lam != null ) && ( ciLig < lam.length ) ) {
+                    Anchor[][] lcm = lam [ ciLig ];
+                    if ( component < maxComponents ) {
+                        Anchor[] la = lcm [ component ];
+                        if ( ( la != null ) && ( markClass < la.length ) ) {
+                            return la [ markClass ];
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 5 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 5 entries" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphCoverageTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an GlyphCoverageTable, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    lct = (GlyphCoverageTable) o;
+                }
+                if ( ( ( o = entries.get(1) ) == null ) || ! ( o instanceof Integer ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, second entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    nmc = ((Integer)(o)).intValue();
+                }
+                if ( ( ( o = entries.get(2) ) == null ) || ! ( o instanceof Integer ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, third entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    mxc = ((Integer)(o)).intValue();
+                }
+                if ( ( ( o = entries.get(3) ) == null ) || ! ( o instanceof MarkAnchor[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, fourth entry must be a MarkAnchor[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    maa = (MarkAnchor[]) o;
+                }
+                if ( ( ( o = entries.get(4) ) == null ) || ! ( o instanceof Anchor[][][] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, fifth entry must be a Anchor[][][], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    lam = (Anchor[][][]) o;
+                }
+            }
+        }
+    }
+
+    private abstract static class MarkToMarkSubtable extends GlyphPositioningSubtable {
+        MarkToMarkSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GPOS_LOOKUP_TYPE_MARK_TO_MARK;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof MarkToMarkSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean position ( GlyphPositioningState ps ) {
+            int giMark1 = ps.getGlyph(), ciMark1;
+            if ( ( ciMark1 = getCoverageIndex ( giMark1 ) ) < 0 ) {
+                return false;
+            } else {
+                MarkAnchor ma = getMark1Anchor ( ciMark1, giMark1 );
+                if ( ma != null ) {
+                    if ( ps.hasPrev() ) {
+                        Anchor a = getMark2Anchor ( ps.getGlyph(-1), ma.getMarkClass() );
+                        if ( a != null ) {
+                            if ( ps.adjust ( a.getAlignmentAdjustment ( ma ) ) ) {
+                                ps.setAdjusted ( true );
+                            }
+                        }
+                        ps.consume(1);
+                    }
+                }
+                return true;
+            }
+        }
+        /**
+         * Obtain mark 1 anchor associated with mark 1 coverage index.
+         * @param ciMark1 mark 1 coverage index
+         * @param giMark1 input glyph index of mark 1 glyph
+         * @return mark 1 anchor or null if none applies
+         */
+        public abstract MarkAnchor getMark1Anchor ( int ciMark1, int giMark1 );
+        /**
+         * Obtain anchor associated with mark 2 glyph index and mark 1 class.
+         * @param giMark2 input glyph index of mark 2 glyph
+         * @param markClass class number of mark 1 glyph
+         * @return anchor or null if none applies
+         */
+        public abstract Anchor getMark2Anchor ( int giBase, int markClass );
+        static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new MarkToMarkSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class MarkToMarkSubtableFormat1 extends MarkToMarkSubtable {
+        private GlyphCoverageTable mct2;                // mark 2 coverage table
+        private int nmc;                                // mark class count
+        private MarkAnchor[] maa;                       // mark1 anchor array, ordered by mark1 coverage index
+        private Anchor[][] mam;                         // mark2 anchor matrix, ordered by mark2 coverage index, then by mark1 class
+        MarkToMarkSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( ( mct2 != null ) && ( maa != null ) && ( nmc > 0 ) && ( mam != null ) ) {
+                List entries = new ArrayList ( 4 );
+                entries.add ( mct2 );
+                entries.add ( Integer.valueOf ( nmc ) );
+                entries.add ( maa );
+                entries.add ( mam );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public MarkAnchor getMark1Anchor ( int ciMark1, int giMark1 ) {
+            if ( ( maa != null ) && ( ciMark1 < maa.length ) ) {
+                return maa [ ciMark1 ];
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public Anchor getMark2Anchor ( int giMark2, int markClass ) {
+            int ciMark2;
+            if ( ( mct2 != null ) && ( ( ciMark2 = mct2.getCoverageIndex ( giMark2 ) ) >= 0 ) ) {
+                if ( ( mam != null ) && ( ciMark2 < mam.length ) ) {
+                    Anchor[] ma = mam [ ciMark2 ];
+                    if ( ( ma != null ) && ( markClass < ma.length ) ) {
+                        return ma [ markClass ];
+                    }
+                }
+            }
+            return null;
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 4 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 4 entries" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphCoverageTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an GlyphCoverageTable, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    mct2 = (GlyphCoverageTable) o;
+                }
+                if ( ( ( o = entries.get(1) ) == null ) || ! ( o instanceof Integer ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, second entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    nmc = ((Integer)(o)).intValue();
+                }
+                if ( ( ( o = entries.get(2) ) == null ) || ! ( o instanceof MarkAnchor[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, third entry must be a MarkAnchor[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    maa = (MarkAnchor[]) o;
+                }
+                if ( ( ( o = entries.get(3) ) == null ) || ! ( o instanceof Anchor[][] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, fourth entry must be a Anchor[][], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    mam = (Anchor[][]) o;
+                }
+            }
+        }
+    }
+
+    private abstract static class ContextualSubtable extends GlyphPositioningSubtable {
+        ContextualSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GPOS_LOOKUP_TYPE_CONTEXTUAL;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof ContextualSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean position ( GlyphPositioningState ps ) {
+            int gi = ps.getGlyph(), ci;
+            if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) {
+                return false;
+            } else {
+                int[] rv = new int[1];
+                RuleLookup[] la = getLookups ( ci, gi, ps, rv );
+                if ( la != null ) {
+                    ps.apply ( la, rv[0] );
+                }
+                return true;
+            }
+        }
+        /**
+         * Obtain rule lookups set associated current input glyph context.
+         * @param ci coverage index of glyph at current position
+         * @param gi glyph index of glyph at current position
+         * @param ps glyph positioning state
+         * @param rv array of ints used to receive multiple return values, must be of length 1 or greater,
+         * where the first entry is used to return the input sequence length of the matched rule
+         * @return array of rule lookups or null if none applies
+         */
+        public abstract RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv );
+        static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new ContextualSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else if ( format == 2 ) {
+                return new ContextualSubtableFormat2 ( id, sequence, flags, format, coverage, entries );
+            } else if ( format == 3 ) {
+                return new ContextualSubtableFormat3 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class ContextualSubtableFormat1 extends ContextualSubtable {
+        private RuleSet[] rsa;                          // rule set array, ordered by glyph coverage index
+        ContextualSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( rsa != null ) {
+                List entries = new ArrayList ( 1 );
+                entries.add ( rsa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            GlyphTable.resolveLookupReferences ( rsa, lookupTables );
+        }
+        /** {@inheritDoc} */
+        public RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv  ) {
+            assert ps != null;
+            assert ( rv != null ) && ( rv.length > 0 );
+            assert rsa != null;
+            if ( rsa.length > 0 ) {
+                RuleSet rs = rsa [ 0 ];
+                if ( rs != null ) {
+                    Rule[] ra = rs.getRules();
+                    for ( int i = 0, n = ra.length; i < n; i++ ) {
+                        Rule r = ra [ i ];
+                        if ( ( r != null ) && ( r instanceof ChainedGlyphSequenceRule ) ) {
+                            ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r;
+                            int[] iga = cr.getGlyphs ( gi );
+                            if ( matches ( ps, iga, 0, rv ) ) {
+                                return r.getLookups();
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        static boolean matches ( GlyphPositioningState ps, int[] glyphs, int offset, int[] rv ) {
+            if ( ( glyphs == null ) || ( glyphs.length == 0 ) ) {
+                return true;                            // match null or empty glyph sequence
+            } else {
+                boolean reverse = offset < 0;
+                GlyphTester ignores = ps.getIgnoreDefault();
+                int[] counts = ps.getGlyphsAvailable ( offset, reverse, ignores );
+                int nga = counts[0];
+                int ngm = glyphs.length;
+                if ( nga < ngm ) {
+                    return false;                       // insufficient glyphs available to match
+                } else {
+                    int[] ga = ps.getGlyphs ( offset, ngm, reverse, ignores, null, counts );
+                    for ( int k = 0; k < ngm; k++ ) {
+                        if ( ga [ k ] != glyphs [ k ] ) {
+                            return false;               // match fails at ga [ k ]
+                        }
+                    }
+                    if ( rv != null ) {
+                        rv[0] = counts[0] + counts[1];
+                    }
+                    return true;                        // all glyphs match
+                }
+            }
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 1 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    rsa = (RuleSet[]) o;
+                }
+            }
+        }
+    }
+
+    private static class ContextualSubtableFormat2 extends ContextualSubtable {
+        private GlyphClassTable cdt;                    // class def table
+        private int ngc;                                // class set count
+        private RuleSet[] rsa;                          // rule set array, ordered by class number [0...ngc - 1]
+        ContextualSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( rsa != null ) {
+                List entries = new ArrayList ( 3 );
+                entries.add ( cdt );
+                entries.add ( Integer.valueOf ( ngc ) );
+                entries.add ( rsa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            GlyphTable.resolveLookupReferences ( rsa, lookupTables );
+        }
+        /** {@inheritDoc} */
+        public RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv  ) {
+            assert ps != null;
+            assert ( rv != null ) && ( rv.length > 0 );
+            assert rsa != null;
+            if ( rsa.length > 0 ) {
+                RuleSet rs = rsa [ 0 ];
+                if ( rs != null ) {
+                    Rule[] ra = rs.getRules();
+                    for ( int i = 0, n = ra.length; i < n; i++ ) {
+                        Rule r = ra [ i ];
+                        if ( ( r != null ) && ( r instanceof ChainedClassSequenceRule ) ) {
+                            ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r;
+                            int[] ca = cr.getClasses ( cdt.getClassIndex ( gi, ps.getClassMatchSet ( gi ) ) );
+                            if ( matches ( ps, cdt, ca, 0, rv ) ) {
+                                return r.getLookups();
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        static boolean matches ( GlyphPositioningState ps, GlyphClassTable cdt, int[] classes, int offset, int[] rv ) {
+            if ( ( cdt == null ) || ( classes == null ) || ( classes.length == 0 ) ) {
+                return true;                            // match null class definitions, null or empty class sequence
+            } else {
+                boolean reverse = offset < 0;
+                GlyphTester ignores = ps.getIgnoreDefault();
+                int[] counts = ps.getGlyphsAvailable ( offset, reverse, ignores );
+                int nga = counts[0];
+                int ngm = classes.length;
+                if ( nga < ngm ) {
+                    return false;                       // insufficient glyphs available to match
+                } else {
+                    int[] ga = ps.getGlyphs ( offset, ngm, reverse, ignores, null, counts );
+                    for ( int k = 0; k < ngm; k++ ) {
+                        int gi = ga [ k ];
+                        int ms = ps.getClassMatchSet ( gi );
+                        int gc = cdt.getClassIndex ( gi, ms );
+                        if ( ( gc < 0 ) || ( gc >= cdt.getClassSize ( ms ) ) ) {
+                            return false;               // none or invalid class fails mat ch
+                        } else if ( gc != classes [ k ] ) {
+                            return false;               // match fails at ga [ k ]
+                        }
+                    }
+                    if ( rv != null ) {
+                        rv[0] = counts[0] + counts[1];
+                    }
+                    return true;                        // all glyphs match
+                }
+            }
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 3 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 3 entries" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphClassTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an GlyphClassTable, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    cdt = (GlyphClassTable) o;
+                }
+                if ( ( ( o = entries.get(1) ) == null ) || ! ( o instanceof Integer ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, second entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    ngc = ((Integer)(o)).intValue();
+                }
+                if ( ( ( o = entries.get(2) ) == null ) || ! ( o instanceof RuleSet[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, third entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    rsa = (RuleSet[]) o;
+                    if ( rsa.length != ngc ) {
+                        throw new IllegalArgumentException ( "illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes" );
+                    }
+                }
+            }
+        }
+    }
+
+    private static class ContextualSubtableFormat3 extends ContextualSubtable {
+        private RuleSet[] rsa;                          // rule set array, containing a single rule set
+        ContextualSubtableFormat3 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( rsa != null ) {
+                List entries = new ArrayList ( 1 );
+                entries.add ( rsa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            GlyphTable.resolveLookupReferences ( rsa, lookupTables );
+        }
+        /** {@inheritDoc} */
+        public RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv  ) {
+            assert ps != null;
+            assert ( rv != null ) && ( rv.length > 0 );
+            assert rsa != null;
+            if ( rsa.length > 0 ) {
+                RuleSet rs = rsa [ 0 ];
+                if ( rs != null ) {
+                    Rule[] ra = rs.getRules();
+                    for ( int i = 0, n = ra.length; i < n; i++ ) {
+                        Rule r = ra [ i ];
+                        if ( ( r != null ) && ( r instanceof ChainedCoverageSequenceRule ) ) {
+                            ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r;
+                            GlyphCoverageTable[] gca = cr.getCoverages();
+                            if ( matches ( ps, gca, 0, rv ) ) {
+                                return r.getLookups();
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        static boolean matches ( GlyphPositioningState ps, GlyphCoverageTable[] gca, int offset, int[] rv ) {
+            if ( ( gca == null ) || ( gca.length == 0 ) ) {
+                return true;                            // match null or empty coverage array
+            } else {
+                boolean reverse = offset < 0;
+                GlyphTester ignores = ps.getIgnoreDefault();
+                int[] counts = ps.getGlyphsAvailable ( offset, reverse, ignores );
+                int nga = counts[0];
+                int ngm = gca.length;
+                if ( nga < ngm ) {
+                    return false;                       // insufficient glyphs available to match
+                } else {
+                    int[] ga = ps.getGlyphs ( offset, ngm, reverse, ignores, null, counts );
+                    for ( int k = 0; k < ngm; k++ ) {
+                        GlyphCoverageTable ct = gca [ k ];
+                        if ( ct != null ) {
+                            if ( ct.getCoverageIndex ( ga [ k ] ) < 0 ) {
+                                return false;           // match fails at ga [ k ]
+                            }
+                        }
+                    }
+                    if ( rv != null ) {
+                        rv[0] = counts[0] + counts[1];
+                    }
+                    return true;                        // all glyphs match
+                }
+            }
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 1 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    rsa = (RuleSet[]) o;
+                }
+            }
+        }
+    }
+
+    private abstract static class ChainedContextualSubtable extends GlyphPositioningSubtable {
+        ChainedContextualSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof ChainedContextualSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean position ( GlyphPositioningState ps ) {
+            int gi = ps.getGlyph(), ci;
+            if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) {
+                return false;
+            } else {
+                int[] rv = new int[1];
+                RuleLookup[] la = getLookups ( ci, gi, ps, rv );
+                if ( la != null ) {
+                    ps.apply ( la, rv[0] );
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+        }
+        /**
+         * Obtain rule lookups set associated current input glyph context.
+         * @param ci coverage index of glyph at current position
+         * @param gi glyph index of glyph at current position
+         * @param ps glyph positioning state
+         * @param rv array of ints used to receive multiple return values, must be of length 1 or greater,
+         * where the first entry is used to return the input sequence length of the matched rule
+         * @return array of rule lookups or null if none applies
+         */
+        public abstract RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv );
+        static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new ChainedContextualSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else if ( format == 2 ) {
+                return new ChainedContextualSubtableFormat2 ( id, sequence, flags, format, coverage, entries );
+            } else if ( format == 3 ) {
+                return new ChainedContextualSubtableFormat3 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
     }
 
-    /** {@inheritDoc} */
-    public int[] position ( GlyphSequence gs, String script, String language ) {
-        return null;
+    private static class ChainedContextualSubtableFormat1 extends ChainedContextualSubtable {
+        private RuleSet[] rsa;                          // rule set array, ordered by glyph coverage index
+        ChainedContextualSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( rsa != null ) {
+                List entries = new ArrayList ( 1 );
+                entries.add ( rsa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            GlyphTable.resolveLookupReferences ( rsa, lookupTables );
+        }
+        /** {@inheritDoc} */
+        public RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv  ) {
+            assert ps != null;
+            assert ( rv != null ) && ( rv.length > 0 );
+            assert rsa != null;
+            if ( rsa.length > 0 ) {
+                RuleSet rs = rsa [ 0 ];
+                if ( rs != null ) {
+                    Rule[] ra = rs.getRules();
+                    for ( int i = 0, n = ra.length; i < n; i++ ) {
+                        Rule r = ra [ i ];
+                        if ( ( r != null ) && ( r instanceof ChainedGlyphSequenceRule ) ) {
+                            ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r;
+                            int[] iga = cr.getGlyphs ( gi );
+                            if ( matches ( ps, iga, 0, rv ) ) {
+                                int[] bga = cr.getBacktrackGlyphs();
+                                if ( matches ( ps, bga, -1, null ) ) {
+                                    int[] lga = cr.getLookaheadGlyphs();
+                                    if ( matches ( ps, lga, rv[0], null ) ) {
+                                        return r.getLookups();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        private boolean matches ( GlyphPositioningState ps, int[] glyphs, int offset, int[] rv ) {
+            return ContextualSubtableFormat1.matches ( ps, glyphs, offset, rv );
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 1 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    rsa = (RuleSet[]) o;
+                }
+            }
+        }
+    }
+
+    private static class ChainedContextualSubtableFormat2 extends ChainedContextualSubtable {
+        private GlyphClassTable icdt;                   // input class def table
+        private GlyphClassTable bcdt;                   // backtrack class def table
+        private GlyphClassTable lcdt;                   // lookahead class def table
+        private int ngc;                                // class set count
+        private RuleSet[] rsa;                          // rule set array, ordered by class number [0...ngc - 1]
+        ChainedContextualSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( rsa != null ) {
+                List entries = new ArrayList ( 5 );
+                entries.add ( icdt );
+                entries.add ( bcdt );
+                entries.add ( lcdt );
+                entries.add ( Integer.valueOf ( ngc ) );
+                entries.add ( rsa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            GlyphTable.resolveLookupReferences ( rsa, lookupTables );
+        }
+        /** {@inheritDoc} */
+        public RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv  ) {
+            assert ps != null;
+            assert ( rv != null ) && ( rv.length > 0 );
+            assert rsa != null;
+            if ( rsa.length > 0 ) {
+                RuleSet rs = rsa [ 0 ];
+                if ( rs != null ) {
+                    Rule[] ra = rs.getRules();
+                    for ( int i = 0, n = ra.length; i < n; i++ ) {
+                        Rule r = ra [ i ];
+                        if ( ( r != null ) && ( r instanceof ChainedClassSequenceRule ) ) {
+                            ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r;
+                            int[] ica = cr.getClasses ( icdt.getClassIndex ( gi, ps.getClassMatchSet ( gi ) ) );
+                            if ( matches ( ps, icdt, ica, 0, rv ) ) {
+                                int[] bca = cr.getBacktrackClasses();
+                                if ( matches ( ps, bcdt, bca, -1, null ) ) {
+                                    int[] lca = cr.getLookaheadClasses();
+                                    if ( matches ( ps, lcdt, lca, rv[0], null ) ) {
+                                        return r.getLookups();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        private boolean matches ( GlyphPositioningState ps, GlyphClassTable cdt, int[] classes, int offset, int[] rv ) {
+            return ContextualSubtableFormat2.matches ( ps, cdt, classes, offset, rv );
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 5 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 5 entries" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphClassTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an GlyphClassTable, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    icdt = (GlyphClassTable) o;
+                }
+                if ( ( ( o = entries.get(1) ) != null ) && ! ( o instanceof GlyphClassTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, second entry must be an GlyphClassTable, but is: " + o.getClass() );
+                } else {
+                    bcdt = (GlyphClassTable) o;
+                }
+                if ( ( ( o = entries.get(2) ) != null ) && ! ( o instanceof GlyphClassTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, third entry must be an GlyphClassTable, but is: " + o.getClass() );
+                } else {
+                    lcdt = (GlyphClassTable) o;
+                }
+                if ( ( ( o = entries.get(3) ) == null ) || ! ( o instanceof Integer ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, fourth entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    ngc = ((Integer)(o)).intValue();
+                }
+                if ( ( ( o = entries.get(4) ) == null ) || ! ( o instanceof RuleSet[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, fifth entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    rsa = (RuleSet[]) o;
+                    if ( rsa.length != ngc ) {
+                        throw new IllegalArgumentException ( "illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes" );
+                    }
+                }
+            }
+        }
+    }
+
+    private static class ChainedContextualSubtableFormat3 extends ChainedContextualSubtable {
+        private RuleSet[] rsa;                          // rule set array, containing a single rule set
+        ChainedContextualSubtableFormat3 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( rsa != null ) {
+                List entries = new ArrayList ( 1 );
+                entries.add ( rsa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            GlyphTable.resolveLookupReferences ( rsa, lookupTables );
+        }
+        /** {@inheritDoc} */
+        public RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv  ) {
+            assert ps != null;
+            assert ( rv != null ) && ( rv.length > 0 );
+            assert rsa != null;
+            if ( rsa.length > 0 ) {
+                RuleSet rs = rsa [ 0 ];
+                if ( rs != null ) {
+                    Rule[] ra = rs.getRules();
+                    for ( int i = 0, n = ra.length; i < n; i++ ) {
+                        Rule r = ra [ i ];
+                        if ( ( r != null ) && ( r instanceof ChainedCoverageSequenceRule ) ) {
+                            ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r;
+                            GlyphCoverageTable[] igca = cr.getCoverages();
+                            if ( matches ( ps, igca, 0, rv ) ) {
+                                GlyphCoverageTable[] bgca = cr.getBacktrackCoverages();
+                                if ( matches ( ps, bgca, -1, null ) ) {
+                                    GlyphCoverageTable[] lgca = cr.getLookaheadCoverages();
+                                    if ( matches ( ps, lgca, rv[0], null ) ) {
+                                        return r.getLookups();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        private boolean matches ( GlyphPositioningState ps, GlyphCoverageTable[] gca, int offset, int[] rv ) {
+            return ContextualSubtableFormat3.matches ( ps, gca, offset, rv );
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 1 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    rsa = (RuleSet[]) o;
+                }
+            }
+        }
+    }
+
+    /**
+     * The <code>DeviceTable</code> class implements a positioning device table record, comprising
+     * adjustments to be made to scaled design units according to the scaled size.
+     */
+    public static class DeviceTable {
+
+        private final int startSize;
+        private final int endSize;
+        private final int[] deltas;
+
+        /**
+         * Instantiate a DeviceTable.
+         * @param startSize the 
+         * @param endSize the ending (scaled) size
+         * @param deltas adjustments for each scaled size
+         */
+        public DeviceTable ( int startSize, int endSize, int[] deltas ) {
+            assert startSize >= 0;
+            assert startSize <= endSize;
+            assert deltas != null;
+            assert deltas.length == ( endSize - startSize ) + 1;
+            this.startSize = startSize;
+            this.endSize = endSize;
+            this.deltas = deltas;
+        }
+
+        /** @return the start size */
+        public int getStartSize() {
+            return startSize;
+        }
+
+        /** @return the end size */
+        public int getEndSize() {
+            return endSize;
+        }
+
+        /** @return the deltas */
+        public int[] getDeltas() {
+            return deltas;
+        }
+
+        /**
+         * Find device adjustment.
+         * @param fontSize the font size to search for
+         * @return an adjustment if font size matches an entry
+         * @asf.todo at present, assumes that 1 device unit equals one point
+         */
+        public int findAdjustment ( int fontSize ) {
+            int fs = fontSize / 1000;
+            if ( fs < startSize ) {
+                return 0;
+            } else if ( fs <= endSize ) {
+                return deltas [ fs - startSize ] * 1000;
+            } else {
+                return 0;
+            }
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            return "{ start = " + startSize + ", end = " + endSize + ", deltas = " + Arrays.toString ( deltas ) + "}";
+        }
+
+    }
+
+    /**
+     * The <code>Value</code> class implements a positioning value record, comprising placement
+     * and advancement information in X and Y axes, and optionally including device data used to
+     * perform device (grid-fitted) specific fine grain adjustments.
+     */
+    public static class Value {
+
+        /** X_PLACEMENT value format flag */
+        public static final int X_PLACEMENT             = 0x0001;
+        /** Y_PLACEMENT value format flag */
+        public static final int Y_PLACEMENT             = 0x0002;
+        /** X_ADVANCE value format flag */
+        public static final int X_ADVANCE               = 0x0004;
+        /** Y_ADVANCE value format flag */
+        public static final int Y_ADVANCE               = 0x0008;
+        /** X_PLACEMENT_DEVICE value format flag */
+        public static final int X_PLACEMENT_DEVICE      = 0x0010;
+        /** Y_PLACEMENT_DEVICE value format flag */
+        public static final int Y_PLACEMENT_DEVICE      = 0x0020;
+        /** X_ADVANCE_DEVICE value format flag */
+        public static final int X_ADVANCE_DEVICE        = 0x0040;
+        /** Y_ADVANCE_DEVICE value format flag */
+        public static final int Y_ADVANCE_DEVICE        = 0x0080;
+
+        /** X_PLACEMENT value index (within adjustments arrays) */
+        public static final int IDX_X_PLACEMENT         = 0;
+        /** Y_PLACEMENT value index (within adjustments arrays) */
+        public static final int IDX_Y_PLACEMENT         = 1;
+        /** X_ADVANCE value index (within adjustments arrays) */
+        public static final int IDX_X_ADVANCE           = 2;
+        /** Y_ADVANCE value index (within adjustments arrays) */
+        public static final int IDX_Y_ADVANCE           = 3;
+
+        private int xPlacement;                         // x placement
+        private int yPlacement;                         // y placement
+        private int xAdvance;                           // x advance
+        private int yAdvance;                           // y advance
+        private final DeviceTable xPlaDevice;           // x placement device table
+        private final DeviceTable yPlaDevice;           // y placement device table
+        private final DeviceTable xAdvDevice;           // x advance device table
+        private final DeviceTable yAdvDevice;           // x advance device table
+
+        /**
+         * Instantiate a Value.
+         * @param xPlacement the x placement or zero
+         * @param yPlacement the y placement or zero
+         * @param xAdvance the x advance or zero
+         * @param yAdvance the y advance or zero
+         * @param xPlaDevice the x placement device table or null
+         * @param yPlaDevice the y placement device table or null
+         * @param xAdvDevice the x advance device table or null
+         * @param yAdvDevice the y advance device table or null
+         */
+        public Value ( int xPlacement, int yPlacement, int xAdvance, int yAdvance, DeviceTable xPlaDevice, DeviceTable yPlaDevice, DeviceTable xAdvDevice, DeviceTable yAdvDevice ) {
+            this.xPlacement = xPlacement;
+            this.yPlacement = yPlacement;
+            this.xAdvance = xAdvance;
+            this.yAdvance = yAdvance;
+            this.xPlaDevice = xPlaDevice;
+            this.yPlaDevice = yPlaDevice;
+            this.xAdvDevice = xAdvDevice;
+            this.yAdvDevice = yAdvDevice;
+        }
+
+        /** @return the x placement */
+        public int getXPlacement() {
+            return xPlacement;
+        }
+
+        /** @return the y placement */
+        public int getYPlacement() {
+            return yPlacement;
+        }
+
+        /** @return the x advance */
+        public int getXAdvance() {
+            return xAdvance;
+        }
+
+        /** @return the y advance */
+        public int getYAdvance() {
+            return yAdvance;
+        }
+
+        /** @return the x placement device table */
+        public DeviceTable getXPlaDevice() {
+            return xPlaDevice;
+        }
+
+        /** @return the y placement device table */
+        public DeviceTable getYPlaDevice() {
+            return yPlaDevice;
+        }
+
+        /** @return the x advance device table */
+        public DeviceTable getXAdvDevice() {
+            return xAdvDevice;
+        }
+
+        /** @return the y advance device table */
+        public DeviceTable getYAdvDevice() {
+            return yAdvDevice;
+        }
+
+        /**
+         * Apply value to specific adjustments to without use of device table adjustments.
+         * @param xPlacement the x placement or zero
+         * @param yPlacement the y placement or zero
+         * @param xAdvance the x advance or zero
+         * @param yAdvance the y advance or zero
+         */
+        public void adjust ( int xPlacement, int yPlacement, int xAdvance, int yAdvance ) {
+            this.xPlacement += xPlacement;
+            this.yPlacement += yPlacement;
+            this.xAdvance += xAdvance;
+            this.yAdvance += yAdvance;
+        }
+
+        /**
+         * Apply value to adjustments using font size for device table adjustments.
+         * @param adjustments array of four integers containing X,Y placement and X,Y advance adjustments
+         * @param fontSize font size for device table adjustments
+         * @return true if some adjustment was made
+         */
+        public boolean adjust ( int[] adjustments, int fontSize ) {
+            boolean adjust = false;
+            int dv;
+            if ( ( dv = xPlacement ) != 0 ) {
+                adjustments [ IDX_X_PLACEMENT ] += dv;
+                adjust = true;
+            }
+            if ( ( dv = yPlacement ) != 0 ) {
+                adjustments [ IDX_Y_PLACEMENT ] += dv;
+                adjust = true;
+            }
+            if ( ( dv = xAdvance ) != 0 ) {
+                adjustments [ IDX_X_ADVANCE ] += dv;
+                adjust = true;
+            }
+            if ( ( dv = yAdvance ) != 0 ) {
+                adjustments [ IDX_Y_ADVANCE ] += dv;
+                adjust = true;
+            }
+            if ( fontSize != 0 ) {
+                DeviceTable dt;
+                if ( ( dt = xPlaDevice ) != null ) {
+                    if ( ( dv = dt.findAdjustment ( fontSize ) ) != 0 ) {
+                        adjustments [ IDX_X_PLACEMENT ] += dv;
+                        adjust = true;
+                    }
+                }
+                if ( ( dt = yPlaDevice ) != null ) {
+                    if ( ( dv = dt.findAdjustment ( fontSize ) ) != 0 ) {
+                        adjustments [ IDX_Y_PLACEMENT ] += dv;
+                        adjust = true;
+                    }
+                }
+                if ( ( dt = xAdvDevice ) != null ) {
+                    if ( ( dv = dt.findAdjustment ( fontSize ) ) != 0 ) {
+                        adjustments [ IDX_X_ADVANCE ] += dv;
+                        adjust = true;
+                    }
+                }
+                if ( ( dt = yAdvDevice ) != null ) {
+                    if ( ( dv = dt.findAdjustment ( fontSize ) ) != 0 ) {
+                        adjustments [ IDX_Y_ADVANCE ] += dv;
+                        adjust = true;
+                    }
+                }
+            }
+            return adjust;
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            boolean first = true;
+            sb.append ( "{ " );
+            if ( xPlacement != 0 ) {
+                if ( ! first ) {
+                    sb.append ( ", " );
+                } else {
+                    first = false;
+                }
+                sb.append ( "xPlacement = " + xPlacement );
+            }
+            if ( yPlacement != 0 ) {
+                if ( ! first ) {
+                    sb.append ( ", " );
+                } else {
+                    first = false;
+                }
+                sb.append ( "yPlacement = " + yPlacement );
+            }
+            if ( xAdvance != 0 ) {
+                if ( ! first ) {
+                    sb.append ( ", " );
+                } else {
+                    first = false;
+                }
+                sb.append ( "xAdvance = " + xAdvance );
+            }
+            if ( yAdvance != 0 ) {
+                if ( ! first ) {
+                    sb.append ( ", " );
+                } else {
+                    first = false;
+                }
+                sb.append ( "yAdvance = " + yAdvance );
+            }
+            if ( xPlaDevice != null ) {
+                if ( ! first ) {
+                    sb.append ( ", " );
+                } else {
+                    first = false;
+                }
+                sb.append ( "xPlaDevice = " + xPlaDevice );
+            }
+            if ( yPlaDevice != null ) {
+                if ( ! first ) {
+                    sb.append ( ", " );
+                } else {
+                    first = false;
+                }
+                sb.append ( "xPlaDevice = " + yPlaDevice );
+            }
+            if ( xAdvDevice != null ) {
+                if ( ! first ) {
+                    sb.append ( ", " );
+                } else {
+                    first = false;
+                }
+                sb.append ( "xAdvDevice = " + xAdvDevice );
+            }
+            if ( yAdvDevice != null ) {
+                if ( ! first ) {
+                    sb.append ( ", " );
+                } else {
+                    first = false;
+                }
+                sb.append ( "xAdvDevice = " + yAdvDevice );
+            }
+            sb.append(" }");
+            return sb.toString();
+        }
+
+    }
+
+    /**
+     * The <code>PairValues</code> class implements a pair value record, comprising a glyph id (or zero)
+     * and two optional positioning values.
+     */
+    public static class PairValues {
+
+        private final int glyph;                        // glyph id (or 0)
+        private final Value value1;                     // value for first glyph in pair (or null)
+        private final Value value2;                     // value for second glyph in pair (or null)
+
+        /**
+         * Instantiate a PairValues.
+         * @param glyph the glyph id (or zero)
+         * @param value1 the value of the first glyph in pair (or null)
+         * @param value2 the value of the second glyph in pair (or null)
+         */
+        public PairValues ( int glyph, Value value1, Value value2 ) {
+            assert glyph >= 0;
+            this.glyph = glyph;
+            this.value1 = value1;
+            this.value2 = value2;
+        }
+
+        /** @return the glyph id */
+        public int getGlyph() {
+            return glyph;
+        }
+
+        /** @return the first value */
+        public Value getValue1() {
+            return value1;
+        }
+
+        /** @return the second value */
+        public Value getValue2() {
+            return value2;
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            boolean first = true;
+            sb.append ( "{ " );
+            if ( glyph != 0 ) {
+                if ( ! first ) {
+                    sb.append ( ", " );
+                } else {
+                    first = false;
+                }
+                sb.append ( "glyph = " + glyph );
+            }
+            if ( value1 != null ) {
+                if ( ! first ) {
+                    sb.append ( ", " );
+                } else {
+                    first = false;
+                }
+                sb.append ( "value1 = " + value1 );
+            }
+            if ( value2 != null ) {
+                if ( ! first ) {
+                    sb.append ( ", " );
+                } else {
+                    first = false;
+                }
+                sb.append ( "value2 = " + value2 );
+            }
+            sb.append(" }");
+            return sb.toString();
+        }
+
+    }
+
+    /**
+     * The <code>Anchor</code> class implements a anchor record, comprising an X,Y coordinate pair,
+     * an optional anchor point index (or -1), and optional X or Y device tables (or null if absent).
+     */
+    public static class Anchor {
+
+        private final int x;                            // xCoordinate (in design units)
+        private final int y;                            // yCoordinate (in design units)
+        private final int anchorPoint;                  // anchor point index (or -1)
+        private final DeviceTable xDevice;              // x device table
+        private final DeviceTable yDevice;              // y device table
+
+        /**
+         * Instantiate an Anchor (format 1).
+         * @param x the x coordinate
+         * @param y the y coordinate
+         */
+        public Anchor ( int x, int y ) {
+            this ( x, y, -1, null, null );
+        }
+
+        /**
+         * Instantiate an Anchor (format 2).
+         * @param x the x coordinate
+         * @param y the y coordinate
+         * @param anchorPoint anchor index (or -1)
+         */
+        public Anchor ( int x, int y, int anchorPoint ) {
+            this ( x, y, anchorPoint, null, null );
+        }
+
+        /**
+         * Instantiate an Anchor (format 3).
+         * @param x the x coordinate
+         * @param y the y coordinate
+         * @param xDevice the x device table (or null if not present)
+         * @param yDevice the y device table (or null if not present)
+         */
+        public Anchor ( int x, int y, DeviceTable xDevice, DeviceTable yDevice ) {
+            this ( x, y, -1, xDevice, yDevice );
+        }
+
+        /**
+         * Instantiate an Anchor based on an existing anchor.
+         * @param a the existing anchor
+         */
+        protected Anchor ( Anchor a ) {
+            this ( a.x, a.y, a.anchorPoint, a.xDevice, a.yDevice );
+        }
+
+        private Anchor ( int x, int  y, int anchorPoint, DeviceTable xDevice, DeviceTable yDevice ) {
+            assert ( anchorPoint >= 0 ) || ( anchorPoint == -1 );
+            this.x = x;
+            this.y = y;
+            this.anchorPoint = anchorPoint;
+            this.xDevice = xDevice;
+            this.yDevice = yDevice;
+        }
+
+        /** @return the x coordinate */
+        public int getX() {
+            return x;
+        }
+
+        /** @return the y coordinate */
+        public int getY() {
+            return y;
+        }
+
+        /** @return the anchor point index (or -1 if not specified) */
+        public int getAnchorPoint() {
+            return anchorPoint;
+        }
+
+        /** @return the x device table (or null if not specified) */
+        public DeviceTable getXDevice() {
+            return xDevice;
+        }
+
+        /** @return the y device table (or null if not specified) */
+        public DeviceTable getYDevice() {
+            return yDevice;
+        }
+
+        /**
+         * Obtain adjustment value required to align the specified anchor
+         * with this anchor.
+         * @param a the anchor to align
+         * @return the adjustment value needed to effect alignment
+         */
+        public Value getAlignmentAdjustment ( Anchor a ) {
+            assert a != null;
+            // TODO - handle anchor point
+            // TODO - handle device tables
+            return new Value ( x - a.x, y - a.y, 0, 0, null, null, null, null );
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            sb.append ( "{ [" + x + "," + y + "]" );
+            if ( anchorPoint != -1 ) {
+                sb.append ( ", anchorPoint = " + anchorPoint );
+            }
+            if ( xDevice != null ) {
+                sb.append ( ", xDevice = " + xDevice );
+            }
+            if ( yDevice != null ) {
+                sb.append ( ", yDevice = " + yDevice );
+            }
+            sb.append(" }");
+            return sb.toString();
+        }
+
+    }
+
+    /**
+     * The <code>MarkAnchor</code> class is a subclass of the <code>Anchor</code> class, adding a mark
+     * class designation.
+     */
+    public static class MarkAnchor extends Anchor {
+
+        private final int markClass;                            // mark class
+
+        /**
+         * Instantiate a MarkAnchor
+         * @param markClass the mark class
+         * @param a the underlying anchor (whose fields are copied)
+         */
+        public MarkAnchor ( int markClass, Anchor a ) {
+            super ( a );
+            this.markClass = markClass;
+        }
+
+        /** @return the mark class */
+        public int getMarkClass() {
+            return markClass;
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            return "{ markClass = " + markClass + ", anchor = " + super.toString() + " }";
+        }
+
     }
 
 }
diff --git a/src/java/org/apache/fop/fonts/GlyphProcessingState.java b/src/java/org/apache/fop/fonts/GlyphProcessingState.java
new file mode 100644 (file)
index 0000000..d0b79ce
--- /dev/null
@@ -0,0 +1,1089 @@
+/*
+ * 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.fonts;
+
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+// CSOFF: LineLengthCheck
+// CSOFF: NoWhitespaceAfterCheck
+
+/**
+ * The <code>GlyphProcessingState</code> implements a common, base state object used during glyph substitution
+ * and positioning processing.
+ * @author Glenn Adams
+ */
+
+public class GlyphProcessingState {
+
+    /** governing glyph definition table */
+    protected GlyphDefinitionTable gdef;
+    /** governing script */
+    protected String script;
+    /** governing language */
+    protected String language;
+    /** governing feature */
+    protected String feature;
+    /** current input glyph sequence */
+    protected GlyphSequence igs;
+    /** current index in input sequence */
+    protected int index;
+    /** last (maximum) index of input sequence (exclusive) */
+    protected int indexLast;
+    /** consumed, updated after each successful subtable application */
+    protected int consumed;
+    /** lookup flags */
+    protected int flags;
+    /** class match set */
+    protected int classMatchSet;
+    /** script specific context tester or null */
+    protected ScriptContextTester sct;
+    /** glyph context tester or null */
+    protected GlyphContextTester gct;
+    /** ignore base glyph tester */
+    protected GlyphTester ignoreBase;
+    /** ignore ligature glyph tester */
+    protected GlyphTester ignoreLigature;
+    /** ignore mark glyph tester */
+    protected GlyphTester ignoreMark;
+    /** default ignore glyph tester */
+    protected GlyphTester ignoreDefault;
+
+    /**
+     * Construct glyph processing state.
+     * @param gs input glyph sequence
+     * @param script script identifier
+     * @param language language identifier
+     * @param feature feature identifier
+     * @param sct script context tester (or null)
+     */
+    protected GlyphProcessingState ( GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct ) {
+        this.script = script;
+        this.language = language;
+        this.feature = feature;
+        this.igs = gs;
+        this.indexLast = gs.getGlyphCount();
+        this.sct = sct;
+        this.gct = ( sct != null ) ? sct.getTester ( feature ) : null;
+        this.ignoreBase = new GlyphTester() { public boolean test(int gi) { return isBase(gi); } };
+        this.ignoreLigature = new GlyphTester() { public boolean test(int gi) { return isLigature(gi); } };
+        this.ignoreMark = new GlyphTester() { public boolean test(int gi) { return isMark(gi); } };
+    }
+
+    /**
+     * Construct glyph processing state using an existing state object using shallow copy
+     * except as follows: input glyph sequence is copied deep except for its characters array.
+     * @param s existing processing state to copy from
+     */
+    protected GlyphProcessingState ( GlyphProcessingState s ) {
+        this ( new GlyphSequence ( s.igs ), s.script, s.language, s.feature, s.sct );
+        setPosition ( s.index );
+    }
+
+    /**
+     * Set governing glyph definition table.
+     * @param gdef glyph definition table (or null, to unset)
+     */
+    public void setGDEF ( GlyphDefinitionTable gdef ) {
+        if ( this.gdef == null ) {
+            this.gdef = gdef;
+        } else if ( gdef == null ) {
+            this.gdef = null;
+        }
+    }
+
+    /**
+     * Obtain governing glyph definition table.
+     * @return glyph definition table (or null, to not set)
+     */
+    public GlyphDefinitionTable getGDEF() {
+        return gdef;
+    }
+
+    /**
+     * Set governing lookup flags
+     * @param flags lookup flags (or zero, to unset)
+     */
+    public void setFlags ( int flags ) {
+        if ( this.flags == 0 ) {
+            this.flags = flags;
+        } else if ( flags == 0 ) {
+            this.flags = 0;
+        }
+    }
+
+    /**
+     * Obtain governing lookup  flags.
+     * @return lookup flags (zero may indicate unset or no flags)
+     */
+    public int getFlags() {
+        return flags;
+    }
+
+    /**
+     * Obtain governing class match set.
+     * @param gi glyph index that may be used to determine which match set applies
+     * @return class match set (zero may indicate unset or no set)
+     */
+    public int getClassMatchSet ( int gi ) {
+        return 0;
+    }
+
+    /**
+     * Set default ignore tester.
+     * @param ignoreDefault glyph tester (or null, to unset)
+     */
+    public void setIgnoreDefault ( GlyphTester ignoreDefault ) {
+        if ( this.ignoreDefault == null ) {
+            this.ignoreDefault = ignoreDefault;
+        } else if ( ignoreDefault == null ) {
+            this.ignoreDefault = null;
+        }
+    }
+
+    /**
+     * Obtain governing default ignores tester.
+     * @return default ignores tester
+     */
+    public GlyphTester getIgnoreDefault() {
+        return ignoreDefault;
+    }
+
+    /**
+     * Update glyph subtable specific state. Each time a
+     * different glyph subtable is to be applied, it is used
+     * to update this state prior to application, after which
+     * this state is to be reset.
+     * @param st glyph subtable to use for update
+     */
+    public void updateSubtableState ( GlyphSubtable st ) {
+        setGDEF ( st.getGDEF() );
+        setFlags ( st.getFlags() );
+        setIgnoreDefault ( getIgnoreTester ( flags ) );
+    }
+
+    /**
+     * Reset glyph subtable specific state.
+     */
+    public void resetSubtableState() {
+        setGDEF ( null );
+        setFlags ( 0 );
+        setIgnoreDefault ( null );
+    }
+
+    /**
+     * Obtain current position index in input glyph sequence.
+     * @return current index
+     */
+    public int getPosition() {
+        return index;
+    }
+
+    /**
+     * Set (seek to) position index in input glyph sequence.
+     * @param index to seek to
+     * @throws IndexOutOfBoundsException if index is less than zero
+     * or exceeds last valid position
+     */
+    public void setPosition ( int index ) throws IndexOutOfBoundsException {
+        if ( ( index >= 0 ) && ( index <= indexLast ) ) {
+            this.index =  index;
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    /**
+     * Obtain last valid position index in input glyph sequence.
+     * @return current last index
+     */
+    public int getLastPosition() {
+        return indexLast;
+    }
+
+    /**
+     * Determine if at least one glyph remains in
+     * input sequence.
+     * @return true if one or more glyph remains
+     */
+    public boolean hasNext() {
+        return hasNext ( 1 );
+    }
+
+    /**
+     * Determine if at least <code>count</code> glyphs remain in
+     * input sequence.
+     * @param count of glyphs to test
+     * @return true if at least <code>count</code> glyphs are available
+     */
+    public boolean hasNext ( int count ) {
+        return ( index + count ) <= indexLast;
+    }
+
+    /**
+     * Update the current position index based upon previously consumed
+     * glyphs, i.e., add the consuemd count to the current position index.
+     * If no glyphs were previously consumed, then forces exactly one
+     * glyph to be consumed.
+     * @return the new (updated) position index
+     */
+    public int next() {
+        if ( index < indexLast ) {
+            // force consumption of at least one input glyph
+            if ( consumed == 0 ) {
+                consumed = 1;
+            }
+            index += consumed; consumed = 0;
+            if ( index > indexLast ) {
+                index = indexLast;
+            }
+        }
+        return index;
+    }
+
+    /**
+     * Determine if at least one backtrack (previous) glyph is present
+     * in input sequence.
+     * @return true if one or more glyph remains
+     */
+    public boolean hasPrev() {
+        return hasPrev ( 1 );
+    }
+
+    /**
+     * Determine if at least <code>count</code> backtrack (previous) glyphs
+     * are present in input sequence.
+     * @param count of glyphs to test
+     * @return true if at least <code>count</code> glyphs are available
+     */
+    public boolean hasPrev ( int count ) {
+        return ( index - count ) >= 0;
+    }
+
+    /**
+     * Update the current position index based upon previously consumed
+     * glyphs, i.e., subtract the consuemd count from the current position index.
+     * If no glyphs were previously consumed, then forces exactly one
+     * glyph to be consumed. This method is used to traverse an input
+     * glyph sequence in reverse order.
+     * @return the new (updated) position index
+     */
+    public int prev() {
+        if ( index > 0 ) {
+            // force consumption of at least one input glyph
+            if ( consumed == 0 ) {
+                consumed = 1;
+            }
+            index -= consumed; consumed = 0;
+            if ( index < 0 ) {
+                index = 0;
+            }
+        }
+        return index;
+    }
+
+    /**
+     * Record the consumption of <code>count</code> glyphs such that
+     * this consumption never exceeds the number of glyphs in the input glyph
+     * sequence.
+     * @param count of glyphs to consume
+     * @return newly adjusted consumption count
+     * @throws IndexOutOfBoundsException if count would cause consumption
+     * to exceed count of glyphs in input glyph sequence
+     */
+    public int consume ( int count ) throws IndexOutOfBoundsException {
+        if ( ( consumed + count ) <= indexLast ) {
+            consumed += count;
+            return consumed;
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    /**
+     * Determine if any consumption has occurred.
+     * @return true if consumption count is greater than zero
+     */
+    public boolean didConsume() {
+        return consumed > 0;
+    }
+
+    /**
+     * Obtain reference to input glyph sequence, which must not be modified.
+     * @return input glyph sequence
+     */
+    public GlyphSequence getInput() {
+        return igs;
+    }
+
+    /**
+     * Obtain glyph at specified offset from current position.
+     * @param offset from current position
+     * @return glyph at specified offset from current position
+     * @throws IndexOutOfBoundsException if no glyph available at offset
+     */
+    public int getGlyph ( int offset ) throws IndexOutOfBoundsException {
+        int i = index + offset;
+        if ( ( i >= 0 ) && ( i < indexLast ) ) {
+            return igs.getGlyph ( i );
+        } else {
+            throw new IndexOutOfBoundsException ( "attempting index at " + i );
+        }
+    }
+
+    /**
+     * Obtain glyph at current position.
+     * @return glyph at current position
+     * @throws IndexOutOfBoundsException if no glyph available
+     */
+    public int getGlyph() throws IndexOutOfBoundsException {
+        return getGlyph ( 0 );
+    }
+
+    /**
+     * Set (replace) glyph at specified offset from current position.
+     * @param offset from current position
+     * @param glyph to set at specified offset from current position
+     * @throws IndexOutOfBoundsException if specified offset is not valid position
+     */
+    public void setGlyph ( int offset, int glyph ) throws IndexOutOfBoundsException {
+        int i = index + offset;
+        if ( ( i >= 0 ) && ( i < indexLast ) ) {
+            igs.setGlyph ( i, glyph );
+        } else {
+            throw new IndexOutOfBoundsException ( "attempting index at " + i );
+        }
+    }
+
+    /**
+     * Obtain character association of glyph at specified offset from current position.
+     * @param offset from current position
+     * @return character association of glyph at current position
+     * @throws IndexOutOfBoundsException if offset results in an invalid index into input glyph sequence
+     */
+    public GlyphSequence.CharAssociation getAssociation ( int offset ) throws IndexOutOfBoundsException {
+        int i = index + offset;
+        if ( ( i >= 0 ) && ( i < indexLast ) ) {
+            return igs.getAssociation ( i );
+        } else {
+            throw new IndexOutOfBoundsException ( "attempting index at " + i );
+        }
+    }
+
+    /**
+     * Obtain character association of glyph at current position.
+     * @return character association of glyph at current position
+     * @throws IndexOutOfBoundsException if no glyph available
+     */
+    public GlyphSequence.CharAssociation getAssociation() throws IndexOutOfBoundsException {
+        return getAssociation ( 0 );
+    }
+
+    /**
+     * Obtain <code>count</code> glyphs starting at specified offset from current position. If
+     * <code>reverseOrder</code> is true, then glyphs are returned in reverse order starting at specified offset
+     * and going in reverse towards beginning of input glyph sequence.
+     * @param offset from current position
+     * @param count number of glyphs to obtain
+     * @param reverseOrder true if to obtain in reverse order
+     * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored)
+     * @param glyphs array to use to fetch glyphs
+     * @param counts int[2] array to receive fetched glyph counts, where counts[0] will
+     * receive the number of glyphs obtained, and counts[1] will receive the number of glyphs
+     * ignored
+     * @return array of glyphs
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public int[] getGlyphs ( int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, int[] glyphs, int[] counts ) throws IndexOutOfBoundsException {
+        if ( count < 0 ) {
+            count = getGlyphsAvailable ( offset, reverseOrder, ignoreTester ) [ 0 ];
+        }
+        int start = index + offset;
+        if ( start < 0 ) {
+            throw new IndexOutOfBoundsException ( "will attempt index at " + start );
+        } else if ( ! reverseOrder && ( ( start + count ) > indexLast ) ) {
+            throw new IndexOutOfBoundsException ( "will attempt index at " + ( start + count ) );
+        } else if ( reverseOrder && ( ( start + 1 ) < count ) ) {
+            throw new IndexOutOfBoundsException ( "will attempt index at " + ( start - count ) );
+        }
+        if ( glyphs == null ) {
+            glyphs = new int [ count ];
+        } else if ( glyphs.length != count ) {
+            throw new IllegalArgumentException ( "glyphs array is non-null, but its length (" + glyphs.length + "), is not equal to count (" + count + ")" );
+        }
+        if ( ! reverseOrder ) {
+            return getGlyphsForward ( start, count, ignoreTester, glyphs, counts );
+        } else {
+            return getGlyphsReverse ( start, count, ignoreTester, glyphs, counts );
+        }
+    }
+
+    private int[] getGlyphsForward ( int start, int count, GlyphTester ignoreTester, int[] glyphs, int[] counts ) throws IndexOutOfBoundsException {
+        int counted = 0;
+        int ignored = 0;
+        for ( int i = start, n = indexLast, k = 0; i < n; i++ ) {
+            int gi = getGlyph ( i - index );
+            if ( gi == 65535 ) {
+                ignored++;
+            } else {
+                if ( ( ignoreTester == null ) || ! ignoreTester.test ( gi ) ) {
+                    if ( k < count ) {
+                        glyphs [ k++ ] = gi; counted++;
+                    } else {
+                        break;
+                    }
+                } else {
+                    ignored++;
+                }
+            }
+        }
+        if ( ( counts != null ) && ( counts.length > 1 ) ) {
+            counts[0] = counted;
+            counts[1] = ignored;
+        }
+        return glyphs;
+    }
+
+    private int[] getGlyphsReverse ( int start, int count, GlyphTester ignoreTester, int[] glyphs, int[] counts ) throws IndexOutOfBoundsException {
+        int counted = 0;
+        int ignored = 0;
+        for ( int i = start, k = 0; i >= 0; i-- ) {
+            int gi = getGlyph ( i - index );
+            if ( gi == 65535 ) {
+                ignored++;
+            } else {
+                if ( ( ignoreTester == null ) || ! ignoreTester.test ( gi ) ) {
+                    if ( k < count ) {
+                        glyphs [ k++ ] = gi; counted++;
+                    } else {
+                        break;
+                    }
+                } else {
+                    ignored++;
+                }
+            }
+        }
+        if ( ( counts != null ) && ( counts.length > 1 ) ) {
+            counts[0] = counted;
+            counts[1] = ignored;
+        }
+        return glyphs;
+    }
+
+    /**
+     * Obtain <code>count</code> glyphs starting at specified offset from current position. If
+     * offset is negative, then glyphs are returned in reverse order starting at specified offset
+     * and going in reverse towards beginning of input glyph sequence.
+     * @param offset from current position
+     * @param count number of glyphs to obtain
+     * @param glyphs array to use to fetch glyphs
+     * @param counts int[2] array to receive fetched glyph counts, where counts[0] will
+     * receive the number of glyphs obtained, and counts[1] will receive the number of glyphs
+     * ignored
+     * @return array of glyphs
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public int[] getGlyphs ( int offset, int count, int[] glyphs, int[] counts ) throws IndexOutOfBoundsException {
+        return getGlyphs ( offset, count, offset < 0, ignoreDefault, glyphs, counts );
+    }
+
+    /**
+     * Obtain all glyphs starting from current position to end of input glyph sequence.
+     * @return array of available glyphs
+     * @throws IndexOutOfBoundsException if no glyph available
+     */
+    public int[] getGlyphs() throws IndexOutOfBoundsException {
+        return getGlyphs ( 0, indexLast - index, false, null, null, null );
+    }
+
+    /**
+     * Obtain <code>count</code> ignored glyphs starting at specified offset from current position. If
+     * <code>reverseOrder</code> is true, then glyphs are returned in reverse order starting at specified offset
+     * and going in reverse towards beginning of input glyph sequence.
+     * @param offset from current position
+     * @param count number of glyphs to obtain
+     * @param reverseOrder true if to obtain in reverse order
+     * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored)
+     * @param glyphs array to use to fetch glyphs
+     * @param counts int[2] array to receive fetched glyph counts, where counts[0] will
+     * receive the number of glyphs obtained, and counts[1] will receive the number of glyphs
+     * ignored
+     * @return array of glyphs
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public int[] getIgnoredGlyphs ( int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, int[] glyphs, int[] counts ) throws IndexOutOfBoundsException {
+        return getGlyphs ( offset, count, reverseOrder, new NotGlyphTester ( ignoreTester ), glyphs, counts );
+    }
+
+    /**
+     * Obtain <code>count</code> ignored glyphs starting at specified offset from current position. If <code>offset</code> is
+     * negative, then fetch in reverse order.
+     * @param offset from current position
+     * @param count number of glyphs to obtain
+     * @return array of glyphs
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public int[] getIgnoredGlyphs ( int offset, int count ) throws IndexOutOfBoundsException {
+        return getIgnoredGlyphs ( offset, count, offset < 0, ignoreDefault, null, null );
+    }
+
+    /**
+     * Determine number of glyphs available starting at specified offset from current position. If
+     * <code>reverseOrder</code> is true, then search backwards in input glyph sequence.
+     * @param offset from current position
+     * @param reverseOrder true if to obtain in reverse order
+     * @param ignoreTester glyph tester to use to determine which glyphs to count (or null, in which case none are ignored)
+     * @return an int[2] array where counts[0] is the number of glyphs available, and counts[1] is the number of glyphs ignored
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public int[] getGlyphsAvailable ( int offset, boolean reverseOrder, GlyphTester ignoreTester ) throws IndexOutOfBoundsException {
+        int start = index + offset;
+        if ( ( start < 0 ) || ( start > indexLast ) ) {
+            return new int[] { 0, 0 };
+        } else if ( ! reverseOrder ) {
+            return getGlyphsAvailableForward ( start, ignoreTester );
+        } else {
+            return getGlyphsAvailableReverse ( start, ignoreTester );
+        }
+    }
+
+    private int[] getGlyphsAvailableForward ( int start, GlyphTester ignoreTester ) throws IndexOutOfBoundsException {
+        int counted = 0;
+        int ignored = 0;
+        if ( ignoreTester == null ) {
+            counted = indexLast - start;
+        } else {
+            for ( int i = start, n = indexLast; i < n; i++ ) {
+                int gi = getGlyph ( i - index );
+                if ( gi == 65535 ) {
+                    ignored++;
+                } else {
+                    if ( ignoreTester.test ( gi ) ) {
+                        ignored++;
+                    } else {
+                        counted++;
+                    }
+                }
+            }
+        }
+        return new int[] { counted, ignored };
+    }
+
+    private int[] getGlyphsAvailableReverse ( int start, GlyphTester ignoreTester ) throws IndexOutOfBoundsException {
+        int counted = 0;
+        int ignored = 0;
+        if ( ignoreTester == null ) {
+            counted = start + 1;
+        } else {
+            for ( int i = start; i >= 0; i-- ) {
+                int gi = getGlyph ( i - index );
+                if ( gi == 65535 ) {
+                    ignored++;
+                } else {
+                    if ( ignoreTester.test ( gi ) ) {
+                        ignored++;
+                    } else {
+                        counted++;
+                    }
+                }
+            }
+        }
+        return new int[] { counted, ignored };
+    }
+
+    /**
+     * Determine number of glyphs available starting at specified offset from current position. If
+     * <code>reverseOrder</code> is true, then search backwards in input glyph sequence. Uses the
+     * default ignores tester.
+     * @param offset from current position
+     * @param reverseOrder true if to obtain in reverse order
+     * @return an int[2] array where counts[0] is the number of glyphs available, and counts[1] is the number of glyphs ignored
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public int[] getGlyphsAvailable ( int offset, boolean reverseOrder ) throws IndexOutOfBoundsException {
+        return getGlyphsAvailable ( offset, reverseOrder, ignoreDefault );
+    }
+
+    /**
+     * Determine number of glyphs available starting at specified offset from current position. If
+     * offset is negative, then search backwards in input glyph sequence. Uses the
+     * default ignores tester.
+     * @param offset from current position
+     * @return an int[2] array where counts[0] is the number of glyphs available, and counts[1] is the number of glyphs ignored
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public int[] getGlyphsAvailable ( int offset ) throws IndexOutOfBoundsException {
+        return getGlyphsAvailable ( offset, offset < 0 );
+    }
+
+    /**
+     * Obtain <code>count</code> character associations of glyphs starting at specified offset from current position. If
+     * <code>reverseOrder</code> is true, then associations are returned in reverse order starting at specified offset
+     * and going in reverse towards beginning of input glyph sequence.
+     * @param offset from current position
+     * @param count number of associations to obtain
+     * @param reverseOrder true if to obtain in reverse order
+     * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored)
+     * @param associations array to use to fetch associations
+     * @param counts int[2] array to receive fetched association counts, where counts[0] will
+     * receive the number of associations obtained, and counts[1] will receive the number of glyphs whose
+     * associations were ignored
+     * @return array of associations
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public GlyphSequence.CharAssociation[] getAssociations ( int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, GlyphSequence.CharAssociation[] associations, int[] counts )
+        throws IndexOutOfBoundsException {
+        if ( count < 0 ) {
+            count = getGlyphsAvailable ( offset, reverseOrder, ignoreTester ) [ 0 ];
+        }
+        int start = index + offset;
+        if ( start < 0 ) {
+            throw new IndexOutOfBoundsException ( "will attempt index at " + start );
+        } else if ( ! reverseOrder && ( ( start + count ) > indexLast ) ) {
+            throw new IndexOutOfBoundsException ( "will attempt index at " + ( start + count ) );
+        } else if ( reverseOrder && ( ( start + 1 ) < count ) ) {
+            throw new IndexOutOfBoundsException ( "will attempt index at " + ( start - count ) );
+        }
+        if ( associations == null ) {
+            associations = new GlyphSequence.CharAssociation [ count ];
+        } else if ( associations.length != count ) {
+            throw new IllegalArgumentException ( "associations array is non-null, but its length (" + associations.length + "), is not equal to count (" + count + ")" );
+        }
+        if ( ! reverseOrder ) {
+            return getAssociationsForward ( start, count, ignoreTester, associations, counts );
+        } else {
+            return getAssociationsReverse ( start, count, ignoreTester, associations, counts );
+        }
+    }
+
+    private GlyphSequence.CharAssociation[] getAssociationsForward ( int start, int count, GlyphTester ignoreTester, GlyphSequence.CharAssociation[] associations, int[] counts )
+        throws IndexOutOfBoundsException {
+        int counted = 0;
+        int ignored = 0;
+        for ( int i = start, n = indexLast, k = 0; i < n; i++ ) {
+            int gi = getGlyph ( i - index );
+            if ( gi == 65535 ) {
+                ignored++;
+            } else {
+                if ( ( ignoreTester == null ) || ! ignoreTester.test ( gi ) ) {
+                    if ( k < count ) {
+                        associations [ k++ ] = getAssociation ( i - index ); counted++;
+                    } else {
+                        break;
+                    }
+                } else {
+                    ignored++;
+                }
+            }
+        }
+        if ( ( counts != null ) && ( counts.length > 1 ) ) {
+            counts[0] = counted;
+            counts[1] = ignored;
+        }
+        return associations;
+    }
+
+    private GlyphSequence.CharAssociation[] getAssociationsReverse ( int start, int count, GlyphTester ignoreTester, GlyphSequence.CharAssociation[] associations, int[] counts )
+        throws IndexOutOfBoundsException {
+        int counted = 0;
+        int ignored = 0;
+        for ( int i = start, k = 0; i >= 0; i-- ) {
+            int gi = getGlyph ( i - index );
+            if ( gi == 65535 ) {
+                ignored++;
+            } else {
+                if ( ( ignoreTester == null ) || ! ignoreTester.test ( gi ) ) {
+                    if ( k < count ) {
+                        associations [ k++ ] = getAssociation ( i - index ); counted++;
+                    } else {
+                        break;
+                    }
+                } else {
+                    ignored++;
+                }
+            }
+        }
+        if ( ( counts != null ) && ( counts.length > 1 ) ) {
+            counts[0] = counted;
+            counts[1] = ignored;
+        }
+        return associations;
+    }
+
+    /**
+     * Obtain <code>count</code> character associations of glyphs starting at specified offset from current position. If
+     * offset is negative, then search backwards in input glyph sequence. Uses the
+     * default ignores tester.
+     * @param offset from current position
+     * @param count number of associations to obtain
+     * @return array of associations
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public GlyphSequence.CharAssociation[] getAssociations ( int offset, int count ) throws IndexOutOfBoundsException {
+        return getAssociations ( offset, count, offset < 0, ignoreDefault, null, null );
+    }
+
+    /**
+     * Obtain <code>count</code> character associations of ignored glyphs starting at specified offset from current position. If
+     * <code>reverseOrder</code> is true, then glyphs are returned in reverse order starting at specified offset
+     * and going in reverse towards beginning of input glyph sequence.
+     * @param offset from current position
+     * @param count number of character associations to obtain
+     * @param reverseOrder true if to obtain in reverse order
+     * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored)
+     * @param associations array to use to fetch associations
+     * @param counts int[2] array to receive fetched association counts, where counts[0] will
+     * receive the number of associations obtained, and counts[1] will receive the number of glyphs whose
+     * associations were ignored
+     * @return array of associations
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public GlyphSequence.CharAssociation[] getIgnoredAssociations ( int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, GlyphSequence.CharAssociation[] associations, int[] counts )
+        throws IndexOutOfBoundsException {
+        return getAssociations ( offset, count, reverseOrder, new NotGlyphTester ( ignoreTester ), associations, counts );
+    }
+
+    /**
+     * Obtain <code>count</code> character associations of ignored glyphs starting at specified offset from current position. If
+     * offset is negative, then search backwards in input glyph sequence. Uses the
+     * default ignores tester.
+     * @param offset from current position
+     * @param count number of character associations to obtain
+     * @return array of associations
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public GlyphSequence.CharAssociation[] getIgnoredAssociations ( int offset, int count ) throws IndexOutOfBoundsException {
+        return getIgnoredAssociations ( offset, count, offset < 0, ignoreDefault, null, null );
+    }
+
+    /**
+     * Replace subsequence of input glyph sequence starting at specified offset from current position and of
+     * length <code>count</code> glyphs with a subsequence of the sequence <code>gs</code> starting from the specified
+     * offset <code>gsOffset</code> of length <code>gsCount</code> glyphs.
+     * @param offset from current position
+     * @param count number of glyphs to replace, which, if negative means all glyphs from offset to end of input sequence
+     * @param gs glyph sequence from which to obtain replacement glyphs
+     * @param gsOffset offset of first glyph in replacement sequence
+     * @param gsCount count of glyphs in replacement sequence starting at <code>gsOffset</code>
+     * @return true if replacement occurred, or false if replacement would result in no change to input glyph sequence
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public boolean replaceInput ( int offset, int count, GlyphSequence gs, int gsOffset, int gsCount ) throws IndexOutOfBoundsException {
+        int nig = ( igs != null ) ? igs.getGlyphCount() : 0;
+        int position = getPosition() + offset;
+        if ( position < 0 ) {
+            position = 0;
+        } else if ( position > nig ) {
+            position = nig;
+        }
+        if ( ( count < 0 ) || ( ( position + count ) > nig ) ) {
+            count = nig - position;
+        }
+        int nrg = ( gs != null ) ? gs.getGlyphCount() : 0;
+        if ( gsOffset < 0 ) {
+            gsOffset = 0;
+        } else if ( gsOffset > nrg ) {
+            gsOffset = nrg;
+        }
+        if ( ( gsCount < 0 ) || ( ( gsOffset + gsCount ) > nrg ) ) {
+            gsCount = nrg - gsOffset;
+        }
+        int ng = nig + gsCount - count;
+        IntBuffer gb = IntBuffer.allocate ( ng );
+        List al = new ArrayList ( ng );
+        for ( int i = 0, n = position; i < n; i++ ) {
+            gb.put ( igs.getGlyph ( i ) );
+            al.add ( igs.getAssociation ( i ) );
+        }
+        for ( int i = gsOffset, n = gsOffset + gsCount; i < n; i++ ) {
+            gb.put ( gs.getGlyph ( i ) );
+            al.add ( gs.getAssociation ( i ) );
+        }
+        for ( int i = position + count, n = nig; i < n; i++ ) {
+            gb.put ( igs.getGlyph ( i ) );
+            al.add ( igs.getAssociation ( i ) );
+        }
+        gb.flip();
+        if ( igs.compareGlyphs ( gb ) != 0 ) {
+            this.igs = new GlyphSequence ( igs.getCharacters(), gb, al );
+            this.indexLast = gb.limit();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Replace subsequence of input glyph sequence starting at specified offset from current position and of
+     * length <code>count</code> glyphs with all glyphs in the replacement sequence <code>gs</code>.
+     * @param offset from current position
+     * @param count number of glyphs to replace, which, if negative means all glyphs from offset to end of input sequence
+     * @param gs glyph sequence from which to obtain replacement glyphs
+     * @return true if replacement occurred, or false if replacement would result in no change to input glyph sequence
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public boolean replaceInput ( int offset, int count, GlyphSequence gs ) throws IndexOutOfBoundsException {
+        return replaceInput ( offset, count, gs, 0, gs.getGlyphCount() );
+    }
+
+    /**
+     * Erase glyphs in input glyph sequence starting at specified offset from current position, where each glyph
+     * in the specified <code>glyphs</code> array is matched, one at a time, and when a (forward searching) match is found
+     * in the input glyph sequence, the matching glyph is replaced with the glyph index 65535.
+     * @param offset from current position
+     * @param glyphs array of glyphs to erase
+     * @return the number of glyphs erased, which may be less than the number of specified glyphs
+     * @throws IndexOutOfBoundsException if offset or count results in an
+     * invalid index into input glyph sequence
+     */
+    public int erase ( int offset, int[] glyphs ) throws IndexOutOfBoundsException {
+        int start = index + offset;
+        if ( ( start < 0 ) || ( start > indexLast ) ) {
+            throw new IndexOutOfBoundsException ( "will attempt index at " + start );
+        } else {
+            int erased = 0;
+            for ( int i = start - index, n = indexLast - start; i < n; i++ ) {
+                int gi = getGlyph ( i );
+                if ( gi == glyphs [ erased ] ) {
+                    setGlyph ( i, 65535 );
+                    erased++;
+                }
+            }
+            return erased;
+        }
+    }
+
+    /**
+     * Determine if is possible that the current input sequence satisfies a script specific
+     * context testing predicate. If no predicate applies, then application is always possible.
+     * @return true if no script specific context tester applies or if a specified tester returns
+     * true for the current input sequence context
+     */
+    public boolean maybeApplicable() {
+        if ( gct == null ) {
+            return true;
+        } else {
+            return gct.test ( igs, index );
+        }
+    }
+
+    /**
+     * Apply default application semantices; namely, consume one glyph.
+     */
+    public void applyDefault() {
+        consumed += 1;
+    }
+
+    /**
+     * Determine if specified glyph is a base glyph according to the governing
+     * glyph definition table.
+     * @param gi glyph index to test
+     * @return true if glyph definition table records glyph as a base glyph; otherwise, false
+     */
+    public boolean isBase ( int gi ) {
+        if ( gdef != null ) {
+            return gdef.isGlyphClass ( gi, GlyphDefinitionTable.GLYPH_CLASS_BASE );
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Determine if specified glyph is a ligature glyph according to the governing
+     * glyph definition table.
+     * @param gi glyph index to test
+     * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false
+     */
+    public boolean isLigature ( int gi ) {
+        if ( gdef != null ) {
+            return gdef.isGlyphClass ( gi, GlyphDefinitionTable.GLYPH_CLASS_LIGATURE );
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Determine if specified glyph is a mark glyph according to the governing
+     * glyph definition table.
+     * @param gi glyph index to test
+     * @return true if glyph definition table records glyph as a mark glyph; otherwise, false
+     */
+    public boolean isMark ( int gi ) {
+        if ( gdef != null ) {
+            return gdef.isGlyphClass ( gi, GlyphDefinitionTable.GLYPH_CLASS_MARK );
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Obtain an ignored glyph tester that corresponds to the specified lookup flags.
+     * @param flags lookup flags
+     * @return a glyph tester
+     */
+    public GlyphTester getIgnoreTester ( int flags ) {
+        if ( ( flags & GlyphSubtable.LF_IGNORE_BASE ) != 0 ) {
+            if ( ( flags & (GlyphSubtable.LF_IGNORE_LIGATURE | GlyphSubtable.LF_IGNORE_MARK) ) == 0 ) {
+                return ignoreBase;
+            } else {
+                return getCombinedIgnoreTester ( flags );
+            }
+        }
+        if ( ( flags & GlyphSubtable.LF_IGNORE_LIGATURE ) != 0 ) {
+            if ( ( flags & (GlyphSubtable.LF_IGNORE_BASE | GlyphSubtable.LF_IGNORE_MARK) ) == 0 ) {
+                return ignoreLigature;
+            } else {
+                return getCombinedIgnoreTester ( flags );
+            }
+        }
+        if ( ( flags & GlyphSubtable.LF_IGNORE_MARK ) != 0 ) {
+            if ( ( flags & (GlyphSubtable.LF_IGNORE_BASE | GlyphSubtable.LF_IGNORE_LIGATURE) ) == 0 ) {
+                return ignoreMark;
+            } else {
+                return getCombinedIgnoreTester ( flags );
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Obtain an ignored glyph tester that corresponds to the specified multiple (combined) lookup flags.
+     * @param flags lookup flags
+     * @return a glyph tester
+     */
+    public GlyphTester getCombinedIgnoreTester ( int flags ) {
+        GlyphTester[] gta = new GlyphTester [ 3 ];
+        int ngt = 0;
+        if ( ( flags & GlyphSubtable.LF_IGNORE_BASE ) != 0 ) {
+            gta [ ngt++ ] = ignoreBase;
+        }
+        if ( ( flags & GlyphSubtable.LF_IGNORE_LIGATURE ) != 0 ) {
+            gta [ ngt++ ] = ignoreLigature;
+        }
+        if ( ( flags & GlyphSubtable.LF_IGNORE_MARK ) != 0 ) {
+            gta [ ngt++ ] = ignoreMark;
+        }
+        return getCombinedOrTester ( gta, ngt );
+    }
+
+    /**
+     * Obtain an combined OR glyph tester.
+     * @param gta an array of glyph testers
+     * @param ngt number of glyph testers present in specified array
+     * @return a combined OR glyph tester
+     */
+    public GlyphTester getCombinedOrTester ( GlyphTester[] gta, int ngt ) {
+        if ( ngt > 0 ) {
+            return new CombinedOrGlyphTester ( gta, ngt );
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Obtain an combined AND glyph tester.
+     * @param gta an array of glyph testers
+     * @param ngt number of glyph testers present in specified array
+     * @return a combined AND glyph tester
+     */
+    public GlyphTester getCombinedAndTester ( GlyphTester[] gta, int ngt ) {
+        if ( ngt > 0 ) {
+            return new CombinedAndGlyphTester ( gta, ngt );
+        } else {
+            return null;
+        }
+    }
+
+    /** combined OR glyph tester */
+    private static class CombinedOrGlyphTester implements GlyphTester {
+        private GlyphTester[] gta;
+        private int ngt;
+        CombinedOrGlyphTester ( GlyphTester[] gta, int ngt ) {
+            this.gta = gta;
+            this.ngt = ngt;
+        }
+        /** {@inheritDoc} */
+        public boolean test ( int gi ) {
+            for ( int i = 0, n = ngt; i < n; i++ ) {
+                GlyphTester gt = gta [ i ];
+                if ( gt != null ) {
+                    if ( gt.test ( gi ) ) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+    }
+
+    /** combined AND glyph tester */
+    private static class CombinedAndGlyphTester implements GlyphTester {
+        private GlyphTester[] gta;
+        private int ngt;
+        CombinedAndGlyphTester ( GlyphTester[] gta, int ngt ) {
+            this.gta = gta;
+            this.ngt = ngt;
+        }
+        /** {@inheritDoc} */
+        public boolean test ( int gi ) {
+            for ( int i = 0, n = ngt; i < n; i++ ) {
+                GlyphTester gt = gta [ i ];
+                if ( gt != null ) {
+                    if ( ! gt.test ( gi ) ) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+    }
+
+    /** NOT glyph tester */
+    private static class NotGlyphTester implements GlyphTester {
+        private GlyphTester gt;
+        NotGlyphTester ( GlyphTester gt ) {
+            this.gt = gt;
+        }
+        /** {@inheritDoc} */
+        public boolean test ( int gi ) {
+            if ( gt != null ) {
+                if ( gt.test ( gi ) ) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+}
index e8430f263ec7f1b27acca20741c4e0dee070ebe8..16f2efb13bd79bc99b5f9a705aea30dcae7004d1 100644 (file)
 
 package org.apache.fop.fonts;
 
-import java.nio.CharBuffer;
+import java.nio.IntBuffer;
 
-import java.util.Arrays;
-import java.util.Iterator;
+import java.util.ArrayList;
 import java.util.List;
-import java.util.ListIterator;
 
-// CSOFF: NoWhitespaceAfterCheck
+import org.apache.fop.util.CharUtilities;
+
+// CSOFF: InnerAssignmentCheck
 // CSOFF: LineLengthCheck
+// CSOFF: NoWhitespaceAfterCheck
 
 /**
  * A GlyphSequence encapsulates a sequence of character codes, a sequence of glyph codes,
@@ -37,197 +38,473 @@ import java.util.ListIterator;
  * character code sequence with which the glyph codes are associated.
  * @author Glenn Adams
  */
-public class GlyphSequence implements CharSequence {
+public class GlyphSequence implements Cloneable {
+
+    /** default character buffer capacity in case new character buffer is created */
+    private static final int DEFAULT_CHARS_CAPACITY = 8;
+
+    /** character buffer */
+    private IntBuffer characters;
+    /** glyph buffer */
+    private IntBuffer glyphs;
+    /** association list */
+    private List associations;
+
+    /**
+     * Instantiate a glyph sequence, reusing (i.e., not copying) the referenced
+     * character and glyph buffers and associations. If characters is null, then
+     * an empty character buffer is created. If glyphs is null, then a glyph buffer
+     * is created whose capacity is that of the character buffer. If associations is
+     * null, then identity associations are created.
+     * @param characters a (possibly null) buffer of associated (originating) characters
+     * @param glyphs a (possibly null) buffer of glyphs
+     * @param associations a (possibly null) array of glyph to character associations
+     */
+    public GlyphSequence ( IntBuffer characters, IntBuffer glyphs, List associations ) {
+        if ( characters == null ) {
+            characters = IntBuffer.allocate ( DEFAULT_CHARS_CAPACITY );
+        }
+        if ( glyphs == null ) {
+            glyphs = IntBuffer.allocate ( characters.capacity() );
+        }
+        if ( associations == null ) {
+            associations = makeIdentityAssociations ( characters.limit(), glyphs.limit() );
+        }
+        this.characters = characters;
+        this.glyphs = glyphs;
+        this.associations = associations;
+    }
 
-    private CharSequence characters;
-    private CharSequence glyphs;
-    private CharAssociation[] associations;
+    /**
+     * Instantiate a glyph sequence using an existing glyph sequence, where the new glyph sequence shares
+     * the character array of the existing sequence (but not the buffer object), and creates new copies
+     * of glyphs buffer and association list.
+     * @param gs an existing glyph sequence
+     */
+    public GlyphSequence ( GlyphSequence gs ) {
+        this ( gs.characters.duplicate(), copyBuffer ( gs.glyphs ), copyAssociations ( gs.associations ) );
+    }
 
     /**
-     * Instantiate a glyph sequence.
-     * @param characters a (possibly empty) sequence of associated (originating) characters
-     * @param sequences a (possibly empty) list of glyph sequences
-     * @param associations a (possibly empty) list of glyph to character associations, one for each glyph in the concatenated glyph sequences
-     * @param reverse a boolean indicating if the glyphs are in reverse order with respect to the nominal inline progression direction
+     * Instantiate a glyph sequence using an existing glyph sequence, where the new glyph sequence shares
+     * the character array of the existing sequence (but not the buffer object), but uses the specified
+     * backtrack, input, and lookahead glyph arrays to populate the glyphs, and uses the specified
+     * of glyphs buffer and association list.
+     * backtrack, input, and lookahead association arrays to populate the associations.
+     * @param gs an existing glyph sequence
+     * @param bga backtrack glyph array
+     * @param iga input glyph array
+     * @param lga lookahead glyph array
+     * @param bal backtrack association list
+     * @param ial input association list
+     * @param lal lookahead association list
      */
-    public GlyphSequence ( CharSequence characters, List/*<GlyphSequence>*/ sequences, List/*<CharAssociation>*/ associations, boolean reverse ) {
-        this ( characters, concatenateSequences ( sequences, reverse ), concatenateAssociations ( associations, reverse ) );
+    public GlyphSequence ( GlyphSequence gs, int[] bga, int[] iga, int[] lga, CharAssociation[] bal, CharAssociation[] ial, CharAssociation[] lal ) {
+        this ( gs.characters.duplicate(), concatGlyphs ( bga, iga, lga ), concatAssociations ( bal, ial, lal ) );
     }
 
     /**
-     * Instantiate a glyph sequence.
-     * @param characters a (possibly empty) sequence of associated (originating) characters
-     * @param glyphs a (possibly empty) list of glyphs
-     * @param associations a (possibly empty) list of glyph to character associations, one for each glyph in the concatenated glyph sequences
+     * Obtain reference to underlying character buffer.
+     * @return character buffer reference
      */
-    public GlyphSequence ( CharSequence characters, CharSequence glyphs, CharAssociation[] associations ) {
-        if ( ( characters == null ) || ( glyphs == null ) ) {
-            throw new IllegalArgumentException ( "characters and glyphs must be non-null" );
-        } else if ( ( associations != null ) && ( associations.length != glyphs.length() ) ) {
-            throw new IllegalArgumentException ( "number of associations must match number of glyphs" );
+    public IntBuffer getCharacters() {
+        return characters;
+    }
+
+    /**
+     * Obtain array of characters. If <code>copy</code> is true, then
+     * a newly instantiated array is returned, otherwise a reference to
+     * the underlying buffer's array is returned. N.B. in case a reference
+     * to the undelying buffer's array is returned, the length
+     * of the array is not necessarily the number of characters in array.
+     * To determine the number of characters, use {@link #getCharacterCount}.
+     * @param copy true if to return a newly instantiated array of characters
+     * @return array of characters
+     */
+    public int[] getCharacterArray ( boolean copy ) {
+        if ( copy ) {
+            return toArray ( characters );
         } else {
-            this.characters = characters;
-            this.glyphs = glyphs;
-            if ( associations == null ) {
-                associations = makeIdentityAssociations ( characters, glyphs );
-            }
-            this.associations = associations;
+            return characters.array();
         }
     }
 
-    /** @return sequence of corresponding (originating) characters */
-    public CharSequence getCharacters() {
-        return characters;
+    /**
+     * Obtain the number of characters in character array, where
+     * each character constitutes a unicode scalar value.
+     * @return number of characters available in character array
+     */
+    public int getCharacterCount() {
+        return characters.limit();
     }
 
-    /** @return sequence of glyphs in glyph sequence */
-    public CharSequence getGlyphs() {
-        return glyphs;
+    /**
+     * Obtain glyph id at specified index.
+     * @param index to obtain glyph
+     * @return the glyph identifier of glyph at specified index
+     * @throws IndexOutOfBoundsException if index is less than zero
+     * or exceeds last valid position
+     */
+    public int getGlyph ( int index ) throws IndexOutOfBoundsException {
+        return glyphs.get ( index );
     }
 
-    /** @return glyph to character associations, one for each glyph */
-    public CharAssociation[] getAssociations() {
-        return associations;
+    /**
+     * Set glyph id at specified index.
+     * @param index to set glyph
+     * @param gi glyph index
+     * @throws IndexOutOfBoundsException if index is greater or equal to
+     * the limit of the underlying glyph buffer
+     */
+    public void setGlyph ( int index, int gi ) throws IndexOutOfBoundsException {
+        if ( gi > 65535 ) {
+            gi = 65535;
+        }
+        glyphs.put ( index, gi );
     }
 
     /**
-     * Obtain the sequence of characters that corresponds to the glyph sequence at interval
-     * [offset,offset+count).
-     * @param offset to first glyph
-     * @param count of glyphs
-     * @return corresponding character sequence
+     * Obtain reference to underlying glyph buffer.
+     * @return glyph buffer reference
      */
-    public CharSequence getCharsForGlyphs ( int offset, int count ) throws DiscontinuousAssociationException {          // CSOK: JavadocMethodCheck
-        int sFirst = -1, eLast = -1;
-        for ( int i = 0, n = count; i < n; i++ ) {
-            CharAssociation ca = associations [ offset + i ];
-            int s = ca.getStart();
-            int e = ca.getEnd();
-            if ( sFirst < 0 ) {
-                sFirst = s;
-            }
-            if ( eLast < 0 ) {
-                eLast = e;
-            } else if ( s == eLast ) {
-                eLast = e;
-            } else {
-                throw new DiscontinuousAssociationException();
+    public IntBuffer getGlyphs() {
+        return glyphs;
+    }
+
+    /**
+     * Obtain count glyphs starting at offset. If <code>count</code> is
+     * negative, then it is treated as if the number of available glyphs
+     * were specified.
+     * @param offset into glyph sequence
+     * @param count of glyphs to obtain starting at offset, or negative,
+     * indicating all avaialble glyphs starting at offset
+     * @return glyph array
+     */
+    public int[] getGlyphs ( int offset, int count ) {
+        int ng = getGlyphCount();
+        if ( offset < 0 ) {
+            offset = 0;
+        } else if ( offset > ng ) {
+            offset = ng;
+        }
+        if ( count < 0 ) {
+            count = ng - offset;
+        }
+        int[] ga = new int [ count ];
+        for ( int i = offset, n = offset + count, k = 0; i < n; i++ ) {
+            if ( k < ga.length ) {
+                ga [ k++ ] = glyphs.get ( i );
             }
         }
-        return characters.subSequence ( sFirst, eLast );
+        return ga;
     }
 
     /**
-     * Obtain the glyph subsequence corresponding to the half-open interval [start,end).
-     * @param start of subsequence
-     * @param end of subsequence
-     * @return a subsequence of this sequence
+     * Obtain array of glyphs. If <code>copy</code> is true, then
+     * a newly instantiated array is returned, otherwise a reference to
+     * the underlying buffer's array is returned. N.B. in case a reference
+     * to the undelying buffer's array is returned, the length
+     * of the array is not necessarily the number of glyphs in array.
+     * To determine the number of glyphs, use {@link #getGlyphCount}.
+     * @param copy true if to return a newly instantiated array of glyphs
+     * @return array of glyphs
      */
-    public GlyphSequence getGlyphSubsequence ( int start, int end ) {
-        CharAssociation[] subset = new CharAssociation[end - start];
-        System.arraycopy(associations, start, subset, 0, end - start);
-        return new GlyphSequence ( characters, glyphs.subSequence ( start, end ), subset );
+    public int[] getGlyphArray ( boolean copy ) {
+        if ( copy ) {
+            return toArray ( glyphs );
+        } else {
+            return glyphs.array();
+        }
     }
 
-    /** @return the number of glyphs in this glyph sequence */
-    public int length() {
-        return glyphs.length();
+    /**
+     * Obtain the number of glyphs in glyphs array, where
+     * each glyph constitutes a font specific glyph index.
+     * @return number of glyphs available in character array
+     */
+    public int getGlyphCount() {
+        return glyphs.limit();
     }
 
     /**
-     * Obtain glyph id at specified index.
-     * @param index to obtain glyph
-     * @return the glyph identifier of glyph at specified index
+     * Obtain association at specified index.
+     * @param index into associations array
+     * @return glyph to character associations at specified index
+     * @throws IndexOutOfBoundsException if index is less than zero
+     * or exceeds last valid position
      */
-    public char charAt ( int index ) {
-        return glyphs.charAt ( index );
+    public CharAssociation getAssociation ( int index ) throws IndexOutOfBoundsException {
+        return (CharAssociation) associations.get ( index );
     }
 
     /**
-     * Obtain glyph code subsequence over interval [start,end).
-     * @param start of subsequence
-     * @param end of subsequence
-     * @return the glyph code subsequence
+     * Obtain reference to underlying associations list.
+     * @return associations list
      */
-    public CharSequence subSequence ( int start, int end ) {
-        return glyphs.subSequence ( start, end );
+    public List getAssociations() {
+        return associations;
+    }
+
+    /**
+     * Obtain count associations starting at offset.
+     * @param offset into glyph sequence
+     * @param count of associations to obtain starting at offset, or negative,
+     * indicating all avaialble associations starting at offset
+     * @return associations
+     */
+    public CharAssociation[] getAssociations ( int offset, int count ) {
+        int ng = getGlyphCount();
+        if ( offset < 0 ) {
+            offset = 0;
+        } else if ( offset > ng ) {
+            offset = ng;
+        }
+        if ( count < 0 ) {
+            count = ng - offset;
+        }
+        CharAssociation[] aa = new CharAssociation [ count ];
+        for ( int i = offset, n = offset + count, k = 0; i < n; i++ ) {
+            if ( k < aa.length ) {
+                aa [ k++ ] = (CharAssociation) associations.get ( i );
+            }
+        }
+        return aa;
+    }
+
+    /**
+     * Compare glyphs.
+     * @param gb buffer containing glyph indices with which this glyph sequence's glyphs are to be compared
+     * @return zero if glyphs are the same, otherwise returns 1 or -1 according to whether this glyph sequence's
+     * glyphs are lexicographically greater or lesser than the glyphs in the specified string buffer
+     */
+    public int compareGlyphs ( IntBuffer gb ) {
+        int ng = getGlyphCount();
+        for ( int i = 0, n = gb.limit(); i < n; i++ ) {
+            if ( i < ng ) {
+                int g1 = glyphs.get ( i );
+                int g2 = gb.get ( i );
+                if ( g1 > g2 ) {
+                    return 1;
+                } else if ( g1 < g2 ) {
+                    return -1;
+                }
+            } else {
+                return -1;              // this gb is a proper prefix of specified gb
+            }
+        }
+        return 0;                       // same lengths with no difference
+    }
+
+    /** {@inheritDoc} */
+    public Object clone() {
+        try {
+            GlyphSequence gs = (GlyphSequence) super.clone();
+            gs.characters = copyBuffer ( characters );
+            gs.glyphs = copyBuffer ( glyphs );
+            gs.associations = copyAssociations ( associations );
+            return gs;
+        } catch ( CloneNotSupportedException e ) {
+            return null;
+        }
     }
 
     /** {@inheritDoc} */
     public String toString() {
-        return glyphs.toString();
+        StringBuffer sb = new StringBuffer();
+        sb.append ( '{' );
+        sb.append ( "chars = [" );
+        sb.append ( characters );
+        sb.append ( "], glyphs = [" );
+        sb.append ( glyphs );
+        sb.append ( "], associations = [" );
+        sb.append ( associations );
+        sb.append ( "]" );
+        sb.append ( '}' );
+        return sb.toString();
     }
 
-    private CharAssociation[] makeIdentityAssociations ( CharSequence characters, CharSequence glyphs ) {
-        int nc = characters.length();
-        int ng = glyphs.length();
-        CharAssociation[] ca = new CharAssociation [ ng ];
-        for ( int i = 0, n = ng; i < n; i++ ) {
-            int k = ( i > nc ) ? nc : i;
-            ca [ i ] = new CharAssociation ( i, ( k == nc ) ? 0 : 1 );
+    /**
+     * Determine if two arrays of glyphs are identical.
+     * @param ga1 first glyph array
+     * @param ga2 second glyph array
+     * @return true if arrays are botth null or both non-null and have identical elements
+     */
+    public static boolean sameGlyphs ( int[] ga1, int[] ga2 ) {
+        if ( ga1 == ga2 ) {
+            return true;
+        } else if ( ( ga1 == null ) || ( ga2 == null ) ) {
+            return false;
+        } else if ( ga1.length != ga2.length ) {
+            return false;
+        } else {
+            for ( int i = 0, n = ga1.length; i < n; i++ ) {
+                if ( ga1[i] != ga2[i] ) {
+                    return false;
+                }
+            }
+            return true;
         }
-        return ca;
     }
 
-    private static CharSequence concatenateSequences ( List/*<GlyphSequence>*/ sequences, boolean reverse ) {
+    /**
+     * Concatenante glyph arrays.
+     * @param bga backtrack glyph array
+     * @param iga input glyph array
+     * @param lga lookahead glyph array
+     * @return new integer buffer containing concatenated glyphs
+     */
+    public static IntBuffer concatGlyphs ( int[] bga, int[] iga, int[] lga ) {
         int ng = 0;
-        for ( Iterator it = sequences.iterator(); it.hasNext();) {
-            GlyphSequence gs = (GlyphSequence) it.next();
-            ng += gs.length();
+        if ( bga != null ) {
+            ng += bga.length;
         }
-        CharBuffer cb = CharBuffer.allocate ( ng );
-        if ( ! reverse ) {
-            for ( ListIterator it = sequences.listIterator(); it.hasNext();) {
-                GlyphSequence gs = (GlyphSequence) it.next();
-                cb.append ( (CharSequence) gs );
-            }
-        } else {
-            for ( ListIterator it = sequences.listIterator ( sequences.size() ); it.hasPrevious();) {
-                GlyphSequence gs = (GlyphSequence) it.previous();
-                cb.append ( (CharSequence) gs );
-            }
+        if ( iga != null ) {
+            ng += iga.length;
+        }
+        if ( lga != null ) {
+            ng += lga.length;
+        }
+        IntBuffer gb = IntBuffer.allocate ( ng );
+        if ( bga != null ) {
+            gb.put ( bga );
+        }
+        if ( iga != null ) {
+            gb.put ( iga );
+        }
+        if ( lga != null ) {
+            gb.put ( lga );
         }
-        cb.rewind();
-        return cb;
+        gb.flip();
+        return gb;
     }
 
-    private static CharAssociation[] concatenateAssociations ( List/*<CharAssociation>*/ associations, boolean reverse ) {
+    /**
+     * Concatenante association arrays.
+     * @param baa backtrack association array
+     * @param iaa input association array
+     * @param laa lookahead association array
+     * @return new list containing concatenated associations
+     */
+    public static List concatAssociations ( CharAssociation[] baa, CharAssociation[] iaa, CharAssociation[] laa ) {
         int na = 0;
-        CharAssociation[] ca = new CharAssociation [ associations.size() ];
-        if ( ! reverse ) {
-            for ( ListIterator it = associations.listIterator(); it.hasNext();) {
-                CharAssociation a = (CharAssociation) it.next();
-                ca [ na++ ] = a;
+        if ( baa != null ) {
+            na += baa.length;
+        }
+        if ( iaa != null ) {
+            na += iaa.length;
+        }
+        if ( laa != null ) {
+            na += laa.length;
+        }
+        if ( na > 0 ) {
+            List gl = new ArrayList ( na );
+            if ( baa != null ) {
+                for ( int i = 0; i < baa.length; i++ ) {
+                    gl.add ( baa[i] );
+                }
             }
-        } else {
-            for ( ListIterator it = associations.listIterator ( associations.size() ); it.hasPrevious();) {
-                CharAssociation a = (CharAssociation) it.previous();
-                ca [ na++ ] = a;
+            if ( iaa != null ) {
+                for ( int i = 0; i < iaa.length; i++ ) {
+                    gl.add ( iaa[i] );
+                }
             }
+            if ( laa != null ) {
+                for ( int i = 0; i < laa.length; i++ ) {
+                    gl.add ( laa[i] );
+                }
+            }
+            return gl;
+        } else {
+            return null;
+        }
+    }
+
+    private static int[] toArray ( IntBuffer ib ) {
+        if ( ib != null ) {
+            int n = ib.limit();
+            int[] ia = new int[n];
+            ib.get ( ia, 0, n );
+            return ia;
+        } else {
+            return new int[0];
+        }
+    }
+
+    private static List makeIdentityAssociations ( int numChars, int numGlyphs ) {
+        int nc = numChars;
+        int ng = numGlyphs;
+        List av = new ArrayList ( ng );
+        for ( int i = 0, n = ng; i < n; i++ ) {
+            int k = ( i > nc ) ? nc : i;
+            av.add ( new CharAssociation ( i, ( k == nc ) ? 0 : 1 ) );
+        }
+        return av;
+    }
+
+    private static IntBuffer copyBuffer ( IntBuffer ib ) {
+        if ( ib != null ) {
+            int[] ia = new int [ ib.capacity() ];
+            int   p  = ib.position();
+            int   l  = ib.limit();
+            System.arraycopy ( ib.array(), 0, ia, 0, ia.length );
+            return IntBuffer.wrap ( ia, p, l - p );
+        } else {
+            return null;
+        }
+    }
+
+    private static List copyAssociations ( List ca ) {
+        if ( ca != null ) {
+            return new ArrayList ( ca );
+        } else {
+            return ca;
         }
-        return ca;
     }
 
     /**
      * A structure class encapsulating an interval of character codes (in a CharSequence)
-     * expressed as an offset and count (of code elements in a CharSequence, i.e., numbere of
+     * expressed as an offset and count (of code elements in a CharSequence, i.e., number of
      * UTF-16 code elements. N.B. count does not necessarily designate the number of Unicode
      * scalar values expressed by the CharSequence; in particular, it does not do so if there
      * is one or more UTF-16 surrogate pairs present in the CharSequence.)
      */
-    public static class CharAssociation {
+    public static class CharAssociation implements Cloneable {
 
         private final int offset;
         private final int count;
+        private final int[] subIntervals;
 
         /**
          * Instantiate a character association.
          * @param offset into array of UTF-16 code elements (in associated CharSequence)
          * @param count of UTF-16 character code elements (in associated CharSequence)
+         * @param subIntervals if disjoint, then array of sub-intervals, otherwise null; even
+         * members of array are sub-interval starts, and odd members are sub-interval
+         * ends (exclusive)
          */
-        public CharAssociation ( int offset, int count ) {
+        public CharAssociation ( int offset, int count, int[] subIntervals ) {
             this.offset = offset;
             this.count = count;
+            this.subIntervals = ( ( subIntervals != null ) && ( subIntervals.length > 2 ) ) ? subIntervals : null;
+        }
+
+        /**
+         * Instantiate a non-disjoint character association.
+         * @param offset into array of UTF-16 code elements (in associated CharSequence)
+         * @param count of UTF-16 character code elements (in associated CharSequence)
+         */
+        public CharAssociation ( int offset, int count ) {
+            this ( offset, count, null );
+        }
+
+        /**
+         * Instantiate a non-disjoint character association.
+         * @param subIntervals if disjoint, then array of sub-intervals, otherwise null; even
+         * members of array are sub-interval starts, and odd members are sub-interval
+         * ends (exclusive)
+         */
+        public CharAssociation ( int[] subIntervals ) {
+            this ( getSubIntervalsStart ( subIntervals ), getSubIntervalsLength ( subIntervals ), subIntervals );
         }
 
         /** @return offset (start of association interval) */
@@ -250,5 +527,231 @@ public class GlyphSequence implements CharSequence {
             return getOffset() + getCount();
         }
 
+        /** @return true if association is disjoint */
+        public boolean isDisjoint() {
+            return subIntervals != null;
+        }
+
+        /** @return subintervals of disjoint association */
+        public int[] getSubIntervals() {
+            return subIntervals;
+        }
+
+        /** @return count of subintervals of disjoint association */
+        public int getSubIntervalCount() {
+            return ( subIntervals != null ) ? ( subIntervals.length / 2 ) : 0;
+        }
+
+        /** {@inheritDoc} */
+        public Object clone() {
+            try {
+                return super.clone();
+            } catch ( CloneNotSupportedException e ) {
+                return null;
+            }
+        }
+
+        /**
+         * Replicate association to form <code>repeat</code> new associations.
+         * @param a association to replicate
+         * @param repeat count
+         * @return array of replicated associations
+         */
+        public static CharAssociation[] replicate ( CharAssociation a, int repeat ) {
+            CharAssociation[] aa = new CharAssociation [ repeat ];
+            for ( int i = 0, n = aa.length; i < n; i++ ) {
+                aa [ i ] = (CharAssociation) a.clone();
+            }
+            return aa;
+        }
+
+        /**
+         * Join (merge) multiple associations into a single, potentially disjoint
+         * association.
+         * @param aa array of associations to join
+         * @return (possibly disjoint) association containing joined associations
+         */
+        public static CharAssociation join ( CharAssociation[] aa ) {
+            // extract sorted intervals
+            int[] ia = extractIntervals ( aa );
+            if ( ( ia == null ) || ( ia.length == 0 ) ) {
+                return new CharAssociation ( 0, 0 );
+            } else if ( ia.length == 2 ) {
+                int s = ia[0];
+                int e = ia[1];
+                return new CharAssociation ( s, e - s );
+            } else {
+                return new CharAssociation ( mergeIntervals ( ia ) );
+            }
+        }
+
+        private static int getSubIntervalsStart ( int[] ia ) {
+            int us = Integer.MAX_VALUE;
+            int ue = Integer.MIN_VALUE;
+            if ( ia != null ) {
+                for ( int i = 0, n = ia.length; i < n; i += 2 ) {
+                    int s = ia [ i + 0 ];
+                    int e = ia [ i + 1 ];
+                    if ( s < us ) {
+                        us = s;
+                    }
+                    if ( e > ue ) {
+                        ue = e;
+                    }
+                }
+                if ( ue < 0 ) {
+                    ue = 0;
+                }
+                if ( us > ue ) {
+                    us = ue;
+                }
+            }
+            return us;
+        }
+
+        private static int getSubIntervalsLength ( int[] ia ) {
+            int us = Integer.MAX_VALUE;
+            int ue = Integer.MIN_VALUE;
+            if ( ia != null ) {
+                for ( int i = 0, n = ia.length; i < n; i += 2 ) {
+                    int s = ia [ i + 0 ];
+                    int e = ia [ i + 1 ];
+                    if ( s < us ) {
+                        us = s;
+                    }
+                    if ( e > ue ) {
+                        ue = e;
+                    }
+                }
+                if ( ue < 0 ) {
+                    ue = 0;
+                }
+                if ( us > ue ) {
+                    us = ue;
+                }
+            }
+            return ue - us;
+        }
+
+        /**
+         * Extract sorted sub-intervals.
+         */
+        private static int[] extractIntervals ( CharAssociation[] aa ) {
+            int ni = 0;
+            for ( int i = 0, n = aa.length; i < n; i++ ) {
+                CharAssociation a = aa [ i ];
+                if ( a.isDisjoint() ) {
+                    ni += a.getSubIntervalCount();
+                } else {
+                    ni += 1;
+                }
+            }
+            int[] sa = new int [ ni ];
+            int[] ea = new int [ ni ];
+            for ( int i = 0, k = 0; i < aa.length; i++ ) {
+                CharAssociation a = aa [ i ];
+                if ( a.isDisjoint() ) {
+                    int[] da = a.getSubIntervals();
+                    for ( int j = 0; j < da.length; j += 2 ) {
+                        sa [ k ] = da [ j + 0 ];
+                        ea [ k ] = da [ j + 1 ];
+                        k++;
+                    }
+                } else {
+                    sa [ k ] = a.getStart();
+                    ea [ k ] = a.getEnd();
+                    k++;
+                }
+            }
+            return sortIntervals ( sa, ea );
+        }
+
+        private static final int[] sortIncrements16                                                             // CSOK: ConstantNameCheck
+            = { 1391376, 463792, 198768, 86961, 33936, 13776, 4592, 1968, 861, 336, 112, 48, 21, 7, 3, 1 };
+
+        private static final int[] sortIncrements03                                                             // CSOK: ConstantNameCheck
+            = { 7, 3, 1 };
+
+        /**
+         * Sort sub-intervals using modified Shell Sort.
+         */
+        private static int[] sortIntervals ( int[] sa, int[] ea ) {
+            assert sa != null;
+            assert ea != null;
+            assert sa.length == ea.length;
+            int ni = sa.length;
+            int[] incr = ( ni < 21 ) ? sortIncrements03 : sortIncrements16;
+            for ( int k = 0; k < incr.length; k++ ) {
+                for ( int h = incr [ k ], i = h, n = ni, j; i < n; i++ ) {
+                    int s1 = sa [ i ];
+                    int e1 = ea [ i ];
+                    for ( j = i; j >= h; j -= h) {
+                        int s2 = sa [ j - h ];
+                        int e2 = ea [ j - h ];
+                        if ( s2 > s1 ) {
+                            sa [ j ] = s2;
+                            ea [ j ] = e2;
+                        } else if ( ( s2 == s1 ) && ( e2 > e1 ) ) {
+                            sa [ j ] = s2;
+                            ea [ j ] = e2;
+                        } else {
+                            break;
+                        }
+                    }
+                    sa [ j ] = s1;
+                    ea [ j ] = e1;
+                }
+            }
+            int[] ia = new int [ ni * 2 ];
+            for ( int i = 0; i < ni; i++ ) {
+                ia [ ( i * 2 ) + 0 ] = sa [ i ];
+                ia [ ( i * 2 ) + 1 ] = ea [ i ];
+            }
+            return ia;
+        }
+
+        /**
+         * Merge overlapping and abutting sub-intervals.
+         */
+        private static int[] mergeIntervals ( int[] ia ) {
+            int ni = ia.length;
+            int i, n, nm, is, ie;
+            // count merged sub-intervals
+            for ( i = 0, n = ni, nm = 0, is = ie = -1; i < n; i += 2 ) {
+                int s = ia [ i + 0 ];
+                int e = ia [ i + 1 ];
+                if ( ( ie < 0 ) || ( s > ie ) ) {
+                    is = s;
+                    ie = e;
+                    nm++;
+                } else if ( s >= is ) {
+                    if ( e > ie ) {
+                        ie = e;
+                    }
+                }
+            }
+            int[] mi = new int [ nm * 2 ];
+            // populate merged sub-intervals
+            for ( i = 0, n = ni, nm = 0, is = ie = -1; i < n; i += 2 ) {
+                int s = ia [ i + 0 ];
+                int e = ia [ i + 1 ];
+                int k = nm * 2;
+                if ( ( ie < 0 ) || ( s > ie ) ) {
+                    is = s;
+                    ie = e;
+                    mi [ k + 0 ] = is;
+                    mi [ k + 1 ] = ie;
+                    nm++;
+                } else if ( s >= is ) {
+                    if ( e > ie ) {
+                        ie = e;
+                    }
+                    mi [ k - 1 ] = ie;
+                }
+            }
+            return mi;
+        }
+
     }
+
 }
index 652367cc9586b74815b1e26e59397df8cb1f78a9..876462738234a077c9d1691b86ee16a695d0c643 100644 (file)
@@ -22,7 +22,7 @@ package org.apache.fop.fonts;
 // CSOFF: LineLengthCheck
 
 /**
- * The <code>GlyphSubstitution</code> interface is implemented by a font related object
+ * The <code>GlyphSubstitution</code> interface is implemented by a glyph substitution subtable
  * that supports the determination of glyph substitution information based on script and
  * language of the corresponding character content.
  * @author Glenn Adams
@@ -30,13 +30,12 @@ package org.apache.fop.fonts;
 public interface GlyphSubstitution {
 
     /**
-     * Perform glyph substitutions. If no substitution applies, then returns the unmodified input sequence.
-     * @param gs sequence to map to output glyph sequence
-     * @param script the script associated with the characters corresponding to the glyph sequence
-     * @param language the language associated with the characters corresponding to the glyph sequence
-     * @return resulting glyph sequence, where each 'glyph' in the returned sequence has been mapped
-     * (or not) by substitution
+     * Perform glyph substitution at the current index, mutating the substitution state object as required.
+     * Only the context associated with the current index is processed.
+     * @param ss glyph substitution state object
+     * @return true if the glyph subtable was applied, meaning that the current context matches the
+     * associated input context glyph coverage table
      */
-    GlyphSequence substitute ( GlyphSequence gs, String script, String language );
+    boolean substitute ( GlyphSubstitutionState ss );
 
 }
diff --git a/src/java/org/apache/fop/fonts/GlyphSubstitutionState.java b/src/java/org/apache/fop/fonts/GlyphSubstitutionState.java
new file mode 100644 (file)
index 0000000..9868283
--- /dev/null
@@ -0,0 +1,219 @@
+/*
+ * 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.fonts;
+
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+// CSOFF: LineLengthCheck
+// CSOFF: NoWhitespaceAfterCheck
+
+/**
+ * The <code>GlyphSubstitutionState</code> implements an state object used during glyph substitution
+ * processing.
+ * @author Glenn Adams
+ */
+
+public class GlyphSubstitutionState extends GlyphProcessingState {
+
+    /** alternates index */
+    private int[] alternatesIndex;
+    /** current output glyph sequence */
+    private IntBuffer ogb;
+    /** current output glyph to character associations */
+    private List oal;
+
+    /**
+     * Construct glyph substitution state.
+     * @param gs input glyph sequence
+     * @param script script identifier
+     * @param language language identifier
+     * @param feature feature identifier
+     * @param sct script context tester (or null)
+     */
+    public GlyphSubstitutionState ( GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct ) {
+        super ( gs, script, language, feature, sct );
+        this.ogb = IntBuffer.allocate ( gs.getGlyphCount() );
+        this.oal = new ArrayList ( gs.getGlyphCount() );
+    }
+
+    /**
+     * Construct glyph substitution state using an existing state object using shallow copy
+     * except as follows: input glyph sequence is copied deep except for its characters array.
+     * @param ss existing positioning state to copy from
+     */
+    public GlyphSubstitutionState ( GlyphSubstitutionState ss ) {
+        super ( ss );
+        this.ogb = IntBuffer.allocate ( indexLast );
+        this.oal = new ArrayList ( indexLast );
+    }
+
+    /**
+     * Set alternates indices.
+     * @param alternates array of alternates indices ordered by coverage index
+     */
+    public void setAlternates ( int[] alternates ) {
+        this.alternatesIndex = alternates;
+    }
+
+    /**
+     * Obtain alternates index associated with specified coverage index. An alternates
+     * index is used to select among stylistic alternates of a glyph at a particular
+     * coverage index. This information must be provided by the document itself (in the
+     * form of an extension attribute value), since a font has no way to determine which
+     * alternate the user desires.
+     * @param ci coverage index
+     * @return an alternates index
+     */
+    public int getAlternatesIndex ( int ci ) {
+        if ( alternatesIndex == null ) {
+            return 0;
+        } else if ( ( ci < 0 ) || ( ci > alternatesIndex.length ) ) {
+            return 0;
+        } else {
+            return alternatesIndex [ ci ];
+        }
+    }
+
+    /**
+     * Put (write) glyph into glyph output buffer.
+     * @param glyph to write
+     * @param a character association that applies to glyph
+     */
+    public void putGlyph ( int glyph, GlyphSequence.CharAssociation a ) {
+        if ( ! ogb.hasRemaining() ) {
+            ogb = growBuffer ( ogb ); 
+        }
+        ogb.put ( glyph );
+        oal.add ( a );
+    }
+
+    /**
+     * Put (write) array of glyphs into glyph output buffer.
+     * @param glyphs to write
+     * @param associations array of character associations that apply to glyphs
+     */
+    public void putGlyphs ( int[] glyphs, GlyphSequence.CharAssociation[] associations ) {
+        assert glyphs != null;
+        assert associations != null;
+        assert associations.length >= glyphs.length;
+        for ( int i = 0, n = glyphs.length; i < n; i++ ) {
+            putGlyph ( glyphs [ i ], associations [ i ] );
+        }
+    }
+
+    /**
+     * Obtain output glyph sequence.
+     * @return newly constructed glyph sequence comprised of original
+     * characters, output glyphs, and output associations
+     */
+    public GlyphSequence getOutput() {
+        int position = ogb.position();
+        if ( position > 0 ) {
+            ogb.limit ( position );
+            ogb.rewind();
+            return new GlyphSequence ( igs.getCharacters(), ogb, oal );
+        } else {
+            return igs;
+        }
+    }
+
+    /**
+     * Apply substitution subtable to current state at current position (only),
+     * resulting in the consumption of zero or more input glyphs, and possibly
+     * replacing the current input glyphs starting at the current position, in
+     * which case it is possible that indexLast is altered to be either less than
+     * or greater than its value prior to this application.
+     * @param st the glyph substitution subtable to apply
+     * @return true if subtable applied, or false if it did not (e.g., its
+     * input coverage table did not match current input context)
+     */
+    public boolean apply ( GlyphSubstitutionSubtable st ) {
+        assert st != null;
+        updateSubtableState ( st );
+        boolean applied = st.substitute ( this );
+        resetSubtableState();
+        return applied;
+    }
+
+    /**
+     * Apply a sequence of matched rule lookups to the <code>nig</code> input glyphs
+     * starting at the current position. If lookups are non-null and non-empty, then
+     * all input glyphs specified by <code>nig</code> are consumed irregardless of
+     * whether any specified lookup applied.
+     * @param lookups array of matched lookups (or null)
+     * @param nig number of glyphs in input sequence, starting at current position, to which
+     * the lookups are to apply, and to be consumed once the application has finished
+     * @return true if lookups are non-null and non-empty; otherwise, false
+     */
+    public boolean apply ( GlyphTable.RuleLookup[] lookups, int nig ) {
+        // int nbg = index;
+        int nlg = indexLast - ( index + nig );
+        int nog = 0;
+        if ( ( lookups != null ) && ( lookups.length > 0 ) ) {
+            // apply each rule lookup to extracted input glyph array
+            for ( int i = 0, n = lookups.length; i < n; i++ ) {
+                GlyphTable.RuleLookup l = lookups [ i ];
+                if ( l != null ) {
+                    GlyphTable.LookupTable lt = l.getLookup();
+                    if ( lt != null ) {
+                        // perform substitution on a copy of previous state
+                        GlyphSubstitutionState ss = new GlyphSubstitutionState ( this );
+                        // apply lookup table substitutions
+                        GlyphSequence gs = lt.substitute ( ss, l.getSequenceIndex() );
+                        // replace current input sequence starting at current position with result
+                        if ( replaceInput ( 0, -1, gs ) ) {
+                            nog = gs.getGlyphCount() - nlg;
+                        }
+                    }
+                }
+            }
+            // output glyphs and associations
+            putGlyphs ( getGlyphs ( 0, nog, false, null, null, null ), getAssociations ( 0, nog, false, null, null, null ) );
+            // consume replaced input glyphs
+            consume ( nog );
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Apply default application semantices; namely, consume one input glyph,
+     * writing that glyph (and its association) to the output glyphs (and associations).
+     */
+    public void applyDefault() {
+        super.applyDefault();
+        int gi = getGlyph();
+        if ( gi != 65535 ) {
+            putGlyph ( gi, getAssociation() );
+        }
+    }
+
+    private static IntBuffer growBuffer ( IntBuffer ib ) {
+        int capacity = ib.capacity();
+        int capacityNew = capacity * 2;
+        IntBuffer ibNew = IntBuffer.allocate ( capacityNew );
+        ib.rewind();
+        return ibNew.put ( ib );
+    }
+
+}
index dcdafedca6725f036d6f116ef22054e28c100eef..7d9923210d4a4b0acc2e0187dbd4614a30b655a1 100644 (file)
@@ -20,6 +20,7 @@
 package org.apache.fop.fonts;
 
 // CSOFF: LineLengthCheck
+// CSOFF: NoWhitespaceAfterCheck
 
 /**
  * The <code>GlyphSubstitutionSubtable</code> implements an abstract base of a glyph substitution subtable,
@@ -51,12 +52,60 @@ public abstract class GlyphSubstitutionSubtable extends GlyphSubtable implements
     }
 
     /** {@inheritDoc} */
-    public GlyphSequence substitute ( GlyphSequence gs, String script, String language ) {
-        if ( gs == null ) {
-            throw new IllegalArgumentException ( "invalid glyph sequence: must not be null" );
-        } else {
-            return gs;
+    public boolean isCompatible ( GlyphSubtable subtable ) {
+        return subtable instanceof GlyphSubstitutionSubtable;
+    }
+
+    /** {@inheritDoc} */
+    public boolean usesReverseScan() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public boolean substitute ( GlyphSubstitutionState ss ) {
+        return false;
+    }
+
+    /**
+     * Apply substitutions using specified state and subtable array. For each position in input sequence,
+     * apply subtables in order until some subtable applies or none remain. If no subtable applied or no
+     * input was consumed for a given position, then apply default action (copy input glyph and advance).
+     * If <code>sequenceIndex</code> is non-negative, then apply subtables only when current position
+     * matches <code>sequenceIndex</code> in relation to the starting position. Furthermore, upon
+     * successful application at <code>sequenceIndex</code>, then apply default action for all remaining
+     * glyphs in input sequence.
+     * @param ss substitution state
+     * @param sta array of subtables to apply
+     * @param sequenceIndex if non negative, then apply subtables only at specified sequence index
+     * @return output glyph sequence
+     */
+    public static final GlyphSequence substitute ( GlyphSubstitutionState ss, GlyphSubstitutionSubtable[] sta, int sequenceIndex ) {
+        int sequenceStart = ss.getPosition();
+        boolean appliedOneShot = false;
+        while ( ss.hasNext() ) {
+            boolean applied = false;
+            if ( ! appliedOneShot && ss.maybeApplicable() ) {
+                for ( int i = 0, n = sta.length; ! applied && ( i < n ); i++ ) {
+                    if ( sequenceIndex < 0 ) {
+                        applied = ss.apply ( sta [ i ] );
+                    } else if ( ss.getPosition() == ( sequenceStart + sequenceIndex ) ) {
+                        applied = ss.apply ( sta [ i ] );
+                        if ( applied ) {
+                            appliedOneShot = true;
+                        }
+                    }
+                }
+            }
+            if ( ! applied || ! ss.didConsume() ) {
+                ss.applyDefault();
+            }
+            ss.next();
         }
+        return ss.getOutput();
+    }
+
+    static final GlyphSequence substitute ( GlyphSequence gs, String script, String language, String feature, GlyphSubstitutionSubtable[] sta, ScriptContextTester sct ) {
+        return substitute ( new GlyphSubstitutionState ( gs, script, language, feature, sct ), sta, -1 );
     }
 
 }
index 6573679c6fda1b25633bf8509d92114e79f6b59b..89826dfc929cf0bdddca1d8239ed5d21b50d0f3d 100644 (file)
@@ -26,15 +26,22 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
 // CSOFF: InnerAssignmentCheck
 // CSOFF: LineLengthCheck
+// CSOFF: NoWhitespaceAfterCheck
 
 /**
  * The <code>GlyphSubstitutionTable</code> class is a glyph table that implements
  * <code>GlyphSubstitution</code> functionality.
  * @author Glenn Adams
  */
-public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitution {
+public class GlyphSubstitutionTable extends GlyphTable {
+
+    /** logging instance */
+    private static final Log log = LogFactory.getLog(GlyphSubstitutionTable.class);                                     // CSOK: ConstantNameCheck
 
     /** single substitution subtable type */
     public static final int GSUB_LOOKUP_TYPE_SINGLE = 1;
@@ -44,23 +51,24 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
     public static final int GSUB_LOOKUP_TYPE_ALTERNATE = 3;
     /** ligature substitution subtable type */
     public static final int GSUB_LOOKUP_TYPE_LIGATURE = 4;
-    /** context substitution subtable type */
-    public static final int GSUB_LOOKUP_TYPE_CONTEXT = 5;
-    /** chaining context substitution subtable type */
-    public static final int GSUB_LOOKUP_TYPE_CHAINING_CONTEXT = 6;
+    /** contextual substitution subtable type */
+    public static final int GSUB_LOOKUP_TYPE_CONTEXTUAL = 5;
+    /** chained contextual substitution subtable type */
+    public static final int GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL = 6;
     /** extension substitution substitution subtable type */
     public static final int GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION = 7;
-    /** reverse chaining context single substitution subtable type */
-    public static final int GSUB_LOOKUP_TYPE_REVERSE_CHAINING_CONTEXT_SINGLE = 8;
+    /** reverse chained contextual single substitution subtable type */
+    public static final int GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE = 8;
 
     /**
      * Instantiate a <code>GlyphSubstitutionTable</code> object using the specified lookups
      * and subtables.
+     * @param gdef glyph definition table that applies
      * @param lookups a map of lookup specifications to subtable identifier strings
      * @param subtables a list of identified subtables
      */
-    public GlyphSubstitutionTable ( Map lookups, List subtables ) {
-        super ( lookups );
+    public GlyphSubstitutionTable ( GlyphDefinitionTable gdef, Map lookups, List subtables ) {
+        super ( gdef, lookups );
         if ( ( subtables == null ) || ( subtables.size() == 0 ) ) {
             throw new IllegalArgumentException ( "subtables must be non-empty" );
         } else {
@@ -72,7 +80,27 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
                     throw new IllegalArgumentException ( "subtable must be a glyph substitution subtable" );
                 }
             }
+            freezeSubtables();
+        }
+    }
+
+    /**
+     * Perform substitution processing using all matching lookups.
+     * @param gs an input glyph sequence
+     * @param script a script identifier
+     * @param language a language identifier
+     * @return the substituted (output) glyph sequence
+     */
+    public GlyphSequence substitute ( GlyphSequence gs, String script, String language ) {
+        GlyphSequence ogs;
+        Map/*<LookupSpec,List<LookupTable>>*/ lookups = matchLookups ( script, language, "*" );
+        if ( ( lookups != null ) && ( lookups.size() > 0 ) ) {
+            ScriptProcessor sp = ScriptProcessor.getInstance ( script );
+            ogs = sp.substitute ( this, gs, script, language, lookups );
+        } else {
+            ogs = gs;
         }
+        return ogs;
     }
 
     /**
@@ -91,14 +119,14 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
             t = GSUB_LOOKUP_TYPE_ALTERNATE;
         } else if ( "ligature".equals ( s ) ) {
             t = GSUB_LOOKUP_TYPE_LIGATURE;
-        } else if ( "context".equals ( s ) ) {
-            t = GSUB_LOOKUP_TYPE_CONTEXT;
-        } else if ( "chainingcontext".equals ( s ) ) {
-            t = GSUB_LOOKUP_TYPE_CHAINING_CONTEXT;
+        } else if ( "contextual".equals ( s ) ) {
+            t = GSUB_LOOKUP_TYPE_CONTEXTUAL;
+        } else if ( "chainedcontextual".equals ( s ) ) {
+            t = GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL;
         } else if ( "extensionsubstitution".equals ( s ) ) {
             t = GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION;
-        } else if ( "reversechainiingcontextsingle".equals ( s ) ) {
-            t = GSUB_LOOKUP_TYPE_REVERSE_CHAINING_CONTEXT_SINGLE;
+        } else if ( "reversechainiingcontextualsingle".equals ( s ) ) {
+            t = GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE;
         } else {
             t = -1;
         }
@@ -125,17 +153,17 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
         case GSUB_LOOKUP_TYPE_LIGATURE:
             tn = "ligature";
             break;
-        case GSUB_LOOKUP_TYPE_CONTEXT:
-            tn = "context";
+        case GSUB_LOOKUP_TYPE_CONTEXTUAL:
+            tn = "contextual";
             break;
-        case GSUB_LOOKUP_TYPE_CHAINING_CONTEXT:
-            tn = "chainingcontext";
+        case GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL:
+            tn = "chainedcontextual";
             break;
         case GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION:
             tn = "extensionsubstitution";
             break;
-        case GSUB_LOOKUP_TYPE_REVERSE_CHAINING_CONTEXT_SINGLE:
-            tn = "reversechainiingcontextsingle";
+        case GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE:
+            tn = "reversechainiingcontextualsingle";
             break;
         default:
             tn = "unknown";
@@ -155,32 +183,29 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
      * @param entries subtable entries
      * @return a glyph subtable instance
      */
-    public static GlyphSubtable createSubtable ( int type, String id, int sequence, int flags, int format, List coverage, List entries ) {
+    public static GlyphSubtable createSubtable ( int type, String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
         GlyphSubtable st = null;
         switch ( type ) {
         case GSUB_LOOKUP_TYPE_SINGLE:
-            st = new SimpleSubtable ( id, sequence, flags, format, coverage, entries );
+            st = SingleSubtable.create ( id, sequence, flags, format, coverage, entries );
             break;
         case GSUB_LOOKUP_TYPE_MULTIPLE:
-            st = new MultipleSubtable ( id, sequence, flags, format, coverage, entries );
+            st = MultipleSubtable.create ( id, sequence, flags, format, coverage, entries );
             break;
         case GSUB_LOOKUP_TYPE_ALTERNATE:
-            st = new AlternateSubtable ( id, sequence, flags, format, coverage, entries );
+            st = AlternateSubtable.create ( id, sequence, flags, format, coverage, entries );
             break;
         case GSUB_LOOKUP_TYPE_LIGATURE:
-            st = new LigatureSubtable ( id, sequence, flags, format, coverage, entries );
+            st = LigatureSubtable.create ( id, sequence, flags, format, coverage, entries );
             break;
-        case GSUB_LOOKUP_TYPE_CONTEXT:
-            st = new ContextSubtable ( id, sequence, flags, format, coverage, entries );
+        case GSUB_LOOKUP_TYPE_CONTEXTUAL:
+            st = ContextualSubtable.create ( id, sequence, flags, format, coverage, entries );
             break;
-        case GSUB_LOOKUP_TYPE_CHAINING_CONTEXT:
-            st = new ChainingContextSubtable ( id, sequence, flags, format, coverage, entries );
-            break;
-        case GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION:
-            st = new ExtensionSubtable ( id, sequence, flags, format, coverage, entries );
+        case GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL:
+            st = ChainedContextualSubtable.create ( id, sequence, flags, format, coverage, entries );
             break;
-        case GSUB_LOOKUP_TYPE_REVERSE_CHAINING_CONTEXT_SINGLE:
-            st = new ReverseChainingSingleSubtable ( id, sequence, flags, format, coverage, entries );
+        case GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE:
+            st = ReverseChainedSingleSubtable.create ( id, sequence, flags, format, coverage, entries );
             break;
         default:
             break;
@@ -188,69 +213,138 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
         return st;
     }
 
-    /** {@inheritDoc} */
-    public GlyphSequence substitute ( GlyphSequence gs, String script, String language ) {
-        GlyphSequence ogs;
-        Map/*<LookupSpec,GlyphSubtable[]>*/ lookups = matchLookups ( script, language, "*" );
-        if ( ( lookups != null ) && ( lookups.size() > 0 ) ) {
-            ScriptProcessor sp = ScriptProcessor.getInstance ( script );
-            ogs = sp.substitute ( gs, script, language, lookups );
-        } else {
-            ogs = gs;
-        }
-        return ogs;
+    /**
+     * Create a substitution subtable according to the specified arguments.
+     * @param type subtable type
+     * @param id subtable identifier
+     * @param sequence subtable sequence
+     * @param flags subtable flags
+     * @param format subtable format
+     * @param coverage list of coverage table entries
+     * @param entries subtable entries
+     * @return a glyph subtable instance
+     */
+    public static GlyphSubtable createSubtable ( int type, String id, int sequence, int flags, int format, List coverage, List entries ) {
+        return createSubtable ( type, id, sequence, flags, format, GlyphCoverageTable.createCoverageTable ( coverage ), entries );
     }
 
-    static class SimpleSubtable extends GlyphSubstitutionSubtable {
-        private int[] map;
-        public SimpleSubtable ( String id, int sequence, int flags, int format, List coverage, List entries ) {
-            super ( id, sequence, flags, format, GlyphCoverageTable.createCoverageTable ( coverage ) );
-            populate ( entries );
+    private abstract static class SingleSubtable extends GlyphSubstitutionSubtable {
+        SingleSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
         }
         /** {@inheritDoc} */
         public int getType() {
             return GSUB_LOOKUP_TYPE_SINGLE;
         }
         /** {@inheritDoc} */
-        public List getEntries() {
-            List entries = new ArrayList ( map.length );
-            for ( int i = 0, n = map.length; i < n; i++ ) {
-                entries.add ( Integer.valueOf ( map[i] ) );
-            }
-            return entries;
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof SingleSubtable;
         }
         /** {@inheritDoc} */
-        public GlyphSequence substitute ( GlyphSequence gs, String script, String language ) {
-            CharBuffer cb = CharBuffer.allocate ( gs.length() );
-            int ng = 0;
-            for ( int i = 0; i < gs.length(); i++ ) {
-                int gi = gs.charAt ( i );
-                int ci, go = gi;
-                if ( ( ci = getCoverageIndex ( gi ) ) >= 0 ) {
-                    assert ci < map.length : "coverage index out of range";
-                    if ( ci < map.length ) {
-                        go = map [ ci ];
-                    }
-                }
+        public boolean substitute ( GlyphSubstitutionState ss ) {
+            int gi = ss.getGlyph(), ci;
+            if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) {
+                return false;
+            } else {
+                int go = getGlyphForCoverageIndex ( ci, gi );
                 if ( ( go < 0 ) || ( go > 65535 ) ) {
                     go = 65535;
                 }
-                cb.put ( (char) go );
-                ng++;
+                ss.putGlyph ( go, ss.getAssociation() );
+                ss.consume(1);
+                return true;
+            }
+        }
+        /**
+         * Obtain glyph for coverage index.
+         * @param ci coverage index
+         * @param gi original glyph index
+         * @return substituted glyph value
+         * @throws IllegalArgumentException if coverage index is not valid
+         */
+        public abstract int getGlyphForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException;
+        static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new SingleSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else if ( format == 2 ) {
+                return new SingleSubtableFormat2 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class SingleSubtableFormat1 extends SingleSubtable {
+        private int delta;
+        private int ciMax;
+        SingleSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            List entries = new ArrayList ( 1 );
+            entries.add ( Integer.valueOf ( delta ) );
+            return entries;
+        }
+        /** {@inheritDoc} */
+        public int getGlyphForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException {
+            if ( ci <= ciMax ) {
+                return gi + delta;
+            } else {
+                throw new IllegalArgumentException ( "coverage index " + ci + " out of range, maximum coverage index is " + ciMax );
+            }
+        }
+        private void populate ( List entries ) {
+            if ( ( entries == null ) || ( entries.size() != 1 ) ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null and contain exactly one entry" );
+            } else {
+                Object o = entries.get(0);
+                int delta = 0;
+                if ( o instanceof Integer ) {
+                    delta = ( (Integer) o ) . intValue();
+                } else {
+                    throw new IllegalArgumentException ( "illegal entries entry, must be Integer, but is: " + o );
+                }
+                this.delta = delta;
+                this.ciMax = getCoverageSize() - 1;
+            }
+        }
+    }
+
+    private static class SingleSubtableFormat2 extends SingleSubtable {
+        private int[] glyphs;
+        SingleSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            List entries = new ArrayList ( glyphs.length );
+            for ( int i = 0, n = glyphs.length; i < n; i++ ) {
+                entries.add ( Integer.valueOf ( glyphs[i] ) );
+            }
+            return entries;
+        }
+        /** {@inheritDoc} */
+        public int getGlyphForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException {
+            if ( glyphs == null ) {
+                return -1;
+            } else if ( ci >=glyphs.length ) {
+                throw new IllegalArgumentException ( "coverage index " + ci + " out of range, maximum coverage index is " + glyphs.length );
+            } else {
+                return glyphs [ ci ];
             }
-            cb.limit(ng);
-            cb.rewind();
-            return new GlyphSequence ( gs.getCharacters(), (CharSequence) cb, null );
         }
         private void populate ( List entries ) {
             int i = 0, n = entries.size();
-            int[] map = new int [ n ];
+            int[] glyphs = new int [ n ];
             for ( Iterator it = entries.iterator(); it.hasNext();) {
                 Object o = it.next();
                 if ( o instanceof Integer ) {
                     int gid = ( (Integer) o ) .intValue();
                     if ( ( gid >= 0 ) && ( gid < 65536 ) ) {
-                        map [ i++ ] = gid;
+                        glyphs [ i++ ] = gid;
                     } else {
                         throw new IllegalArgumentException ( "illegal glyph index: " + gid );
                     }
@@ -259,127 +353,247 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
                 }
             }
             assert i == n;
-            assert this.map == null;
-            this.map = map;
+            assert this.glyphs == null;
+            this.glyphs = glyphs;
+        }
+    }
+
+    private abstract static class MultipleSubtable extends GlyphSubstitutionSubtable {
+        public MultipleSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
         }
         /** {@inheritDoc} */
-        public String toString() {
-            StringBuffer sb = new StringBuffer(super.toString());
-            sb.append('{');
-            sb.append("coverage=");
-            sb.append(getCoverage().toString());
-            sb.append(",entries={");
-            for ( int i = 0, n = map.length; i < n; i++ ) {
-                if ( i > 0 ) {
-                    sb.append(',');
+        public int getType() {
+            return GSUB_LOOKUP_TYPE_MULTIPLE;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof MultipleSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean substitute ( GlyphSubstitutionState ss ) {
+            int gi = ss.getGlyph(), ci;
+            if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) {
+                return false;
+            } else {
+                int[] ga = getGlyphsForCoverageIndex ( ci, gi );
+                if ( ga != null ) {
+                    ss.putGlyphs ( ga, GlyphSequence.CharAssociation.replicate ( ss.getAssociation(), ga.length ) );
+                    ss.consume(1);
                 }
-                sb.append(Integer.toString(map[i]));
+                return true;
+            }
+        }
+        /**
+         * Obtain glyph sequence for coverage index.
+         * @param ci coverage index
+         * @param gi original glyph index
+         * @return sequence of glyphs to substitute for input glyph
+         * @throws IllegalArgumentException if coverage index is not valid
+         */
+        public abstract int[] getGlyphsForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException;
+        static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new MultipleSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
             }
-            sb.append('}');
-            sb.append('}');
-            return sb.toString();
         }
     }
 
-    static class MultipleSubtable extends GlyphSubstitutionSubtable {
-        public MultipleSubtable ( String id, int sequence, int flags, int format, List coverage, List entries ) {
-            super ( id, sequence, flags, format, GlyphCoverageTable.createCoverageTable ( coverage ) );
+    private static class MultipleSubtableFormat1 extends MultipleSubtable {
+        private int[][] gsa;                            // glyph sequence array, ordered by coverage index
+        MultipleSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
         }
         /** {@inheritDoc} */
-        public int getType() {
-            return GSUB_LOOKUP_TYPE_MULTIPLE;
+        public List getEntries() {
+            if ( gsa != null ) {
+                List entries = new ArrayList ( 1 );
+                entries.add ( gsa );
+                return entries;
+            } else {
+                return null;
+            }
         }
         /** {@inheritDoc} */
-        public List getEntries() {
-            return null; // [TBD] - implement me
+        public int[] getGlyphsForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException {
+            if ( gsa == null ) {
+                return null;
+            } else if ( ci >= gsa.length ) {
+                throw new IllegalArgumentException ( "coverage index " + ci + " out of range, maximum coverage index is " + gsa.length );
+            } else {
+                return gsa [ ci ];
+            }
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 1 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof int[][] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an int[][], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    gsa = (int[][]) o;
+                }
+            }
         }
     }
 
-    static class AlternateSubtable extends GlyphSubstitutionSubtable {
-        public AlternateSubtable ( String id, int sequence, int flags, int format, List coverage, List entries ) {
-            super ( id, sequence, flags, format, GlyphCoverageTable.createCoverageTable ( coverage ) );
+    private abstract static class AlternateSubtable extends GlyphSubstitutionSubtable {
+        public AlternateSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
         }
         /** {@inheritDoc} */
         public int getType() {
             return GSUB_LOOKUP_TYPE_ALTERNATE;
         }
         /** {@inheritDoc} */
-        public List getEntries() {
-            return null; // [TBD] - implement me
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof AlternateSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean substitute ( GlyphSubstitutionState ss ) {
+            int gi = ss.getGlyph(), ci;
+            if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) {
+                return false;
+            } else {
+                int[] ga = getAlternatesForCoverageIndex ( ci, gi );
+                int ai = ss.getAlternatesIndex ( ci );
+                int go;
+                if ( ( ai < 0 ) || ( ai >= ga.length ) ) {
+                    go = gi;
+                } else {
+                    go = ga [ ai ];
+                }
+                if ( ( go < 0 ) || ( go > 65535 ) ) {
+                    go = 65535;
+                }
+                ss.putGlyph ( go, ss.getAssociation() );
+                ss.consume(1);
+                return true;
+            }
+        }
+        /**
+         * Obtain glyph alternates for coverage index.
+         * @param ci coverage index
+         * @param gi original glyph index
+         * @return sequence of glyphs to substitute for input glyph
+         * @throws IllegalArgumentException if coverage index is not valid
+         */
+        public abstract int[] getAlternatesForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException;
+        static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new AlternateSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
         }
     }
 
-    static class LigatureSubtable extends GlyphSubstitutionSubtable {
-        private LigatureSet[] map;
-        public LigatureSubtable ( String id, int sequence, int flags, int format, List coverage, List entries ) {
-            super ( id, sequence, flags, format, GlyphCoverageTable.createCoverageTable ( coverage ) );
+    private static class AlternateSubtableFormat1 extends AlternateSubtable {
+        private int[][] gaa;                            // glyph alternates array, ordered by coverage index
+        AlternateSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
             populate ( entries );
         }
         /** {@inheritDoc} */
-        public int getType() {
-            return GSUB_LOOKUP_TYPE_LIGATURE;
-        }
-        /** {@inheritDoc} */
         public List getEntries() {
-            List entries = new ArrayList ( map.length );
-            for ( int i = 0, n = map.length; i < n; i++ ) {
-                entries.add ( map[i] );
+            List entries = new ArrayList ( gaa.length );
+            for ( int i = 0, n = gaa.length; i < n; i++ ) {
+                entries.add ( gaa[i] );
             }
             return entries;
         }
         /** {@inheritDoc} */
-        public GlyphSequence substitute ( GlyphSequence gs, String script, String language ) {
-            CharBuffer cb = CharBuffer.allocate ( gs.length() );
-            int ng = 0;
-            for ( int i = 0, n = gs.length(); i < n; i++ ) {
-                int gi = gs.charAt ( i );
-                int ci, go = gi;
-                LigatureSet ls = null;
-                if ( ( ci = getCoverageIndex ( gi ) ) >= 0 ) {
-                    assert ci < map.length : "coverage index out of range";
-                    if ( ci < map.length ) {
-                        ls = map [ ci ];
-                    }
-                }
-                if ( ls != null ) {
-                    Ligature l;
-                    if ( ( l = findLigature ( ls, gs, i ) ) != null ) {
-                        go = l.getLigature();
-                        i += l.getNumComponents();
-                    }
-                }
-                if ( ( go < 0 ) || ( go > 65535 ) ) {
-                    go = 65535;
-                }
-                cb.put ( (char) go );
-                ng++;
+        public int[] getAlternatesForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException {
+            if ( gaa == null ) {
+                return null;
+            } else if ( ci >= gaa.length ) {
+                throw new IllegalArgumentException ( "coverage index " + ci + " out of range, maximum coverage index is " + gaa.length );
+            } else {
+                return gaa [ ci ];
             }
-            cb.limit(ng);
-            cb.rewind();
-            return new GlyphSequence ( gs.getCharacters(), (CharSequence) cb, null );
         }
         private void populate ( List entries ) {
             int i = 0, n = entries.size();
-            LigatureSet[] map = new LigatureSet [ n ];
+            int[][] gaa = new int [ n ][];
             for ( Iterator it = entries.iterator(); it.hasNext();) {
                 Object o = it.next();
-                if ( o instanceof LigatureSet ) {
-                    map [ i++ ] = (LigatureSet) o;
+                if ( o instanceof int[] ) {
+                    gaa [ i++ ] = (int[]) o;
                 } else {
-                    throw new IllegalArgumentException ( "illegal ligatures entry, must be LigatureSet: " + o );
+                    throw new IllegalArgumentException ( "illegal entries entry, must be int[]: " + o );
                 }
             }
             assert i == n;
-            assert this.map == null;
-            this.map = map;
+            assert this.gaa == null;
+            this.gaa = gaa;
+        }
+    }
+
+    private abstract static class LigatureSubtable extends GlyphSubstitutionSubtable {
+        public LigatureSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
+        }
+        /** {@inheritDoc} */
+        public int getType() {
+            return GSUB_LOOKUP_TYPE_LIGATURE;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof LigatureSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean substitute ( GlyphSubstitutionState ss ) {
+            int gi = ss.getGlyph(), ci;
+            if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) {
+                return false;
+            } else {
+                LigatureSet ls = getLigatureSetForCoverageIndex ( ci, gi );
+                if ( ls != null ) { 
+                    boolean reverse = false;
+                    GlyphTester ignores = ss.getIgnoreDefault();
+                    int[] counts = ss.getGlyphsAvailable ( 0, reverse, ignores );
+                    int nga = counts[0], ngi;
+                    if ( nga > 1 ) {
+                        int[] iga = ss.getGlyphs ( 0, nga, reverse, ignores, null, counts );
+                        Ligature l = findLigature ( ls, iga );
+                        if ( l != null ) {
+                            int go = l.getLigature();
+                            if ( ( go < 0 ) || ( go > 65535 ) ) {
+                                go = 65535;
+                            }
+                            int nmg = 1 + l.getNumComponents();
+                            // fetch matched number of component glyphs to determine matched and ignored count
+                            ss.getGlyphs ( 0, nmg, reverse, ignores, null, counts );
+                            nga = counts[0];
+                            ngi = counts[1];
+                            // fetch associations of matched component glyphs
+                            GlyphSequence.CharAssociation[] laa = ss.getAssociations ( 0, nga );
+                            // output ligature glyph and its association
+                            ss.putGlyph ( go, GlyphSequence.CharAssociation.join ( laa ) );
+                            // fetch and output ignored glyphs (if necessary)
+                            if ( ngi > 0 ) {
+                                ss.putGlyphs ( ss.getIgnoredGlyphs ( 0, ngi ), ss.getIgnoredAssociations ( 0, ngi ) );
+                            }
+                            ss.consume ( nga + ngi );
+                        }
+                    }
+                }
+                return true;
+            }
         }
-        private Ligature findLigature ( LigatureSet ls, CharSequence cs, int offset ) {
+        private Ligature findLigature ( LigatureSet ls, int[] glyphs ) {
             Ligature[] la = ls.getLigatures();
             int k = -1;
             int maxComponents = -1;
             for ( int i = 0, n = la.length; i < n; i++ ) {
                 Ligature l = la [ i ];
-                if ( l.matchesComponents ( cs, offset + 1 ) ) {
+                if ( l.matchesComponents ( glyphs ) ) {
                     int nc = l.getNumComponents();
                     if ( nc > maxComponents ) {
                         maxComponents = nc;
@@ -393,78 +607,710 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
                 return null;
             }
         }
+        /**
+         * Obtain ligature set for coverage index.
+         * @param ci coverage index
+         * @param gi original glyph index
+         * @return ligature set (or null if none defined)
+         * @throws IllegalArgumentException if coverage index is not valid
+         */
+        public abstract LigatureSet getLigatureSetForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException;
+        static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new LigatureSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class LigatureSubtableFormat1 extends LigatureSubtable {
+        private LigatureSet[] ligatureSets;
+        public LigatureSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
         /** {@inheritDoc} */
-        public String toString() {
-            StringBuffer sb = new StringBuffer(super.toString());
-            sb.append('{');
-            sb.append("coverage=");
-            sb.append(getCoverage().toString());
-            sb.append(",entries={");
-            for ( int i = 0, n = map.length; i < n; i++ ) {
-                if ( i > 0 ) {
-                    sb.append(',');
+        public List getEntries() {
+            List entries = new ArrayList ( ligatureSets.length );
+            for ( int i = 0, n = ligatureSets.length; i < n; i++ ) {
+                entries.add ( ligatureSets[i] );
+            }
+            return entries;
+        }
+        /** {@inheritDoc} */
+        public LigatureSet getLigatureSetForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException {
+            if ( ligatureSets == null ) {
+                return null;
+            } else if ( ci >= ligatureSets.length ) {
+                throw new IllegalArgumentException ( "coverage index " + ci + " out of range, maximum coverage index is " + ligatureSets.length );
+            } else {
+                return ligatureSets [ ci ];
+            }
+        }
+        private void populate ( List entries ) {
+            int i = 0, n = entries.size();
+            LigatureSet[] ligatureSets = new LigatureSet [ n ];
+            for ( Iterator it = entries.iterator(); it.hasNext();) {
+                Object o = it.next();
+                if ( o instanceof LigatureSet ) {
+                    ligatureSets [ i++ ] = (LigatureSet) o;
+                } else {
+                    throw new IllegalArgumentException ( "illegal ligatures entry, must be LigatureSet: " + o );
                 }
-                sb.append(map[i]);
             }
-            sb.append('}');
-            sb.append('}');
-            return sb.toString();
+            assert i == n;
+            assert this.ligatureSets == null;
+            this.ligatureSets = ligatureSets;
         }
     }
 
-    static class ContextSubtable extends GlyphSubstitutionSubtable {
-        public ContextSubtable ( String id, int sequence, int flags, int format, List coverage, List entries ) {
-            super ( id, sequence, flags, format, GlyphCoverageTable.createCoverageTable ( coverage ) );
+    private abstract static class ContextualSubtable extends GlyphSubstitutionSubtable {
+        public ContextualSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
         }
         /** {@inheritDoc} */
         public int getType() {
-            return GSUB_LOOKUP_TYPE_CONTEXT;
+            return GSUB_LOOKUP_TYPE_CONTEXTUAL;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof ContextualSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean substitute ( GlyphSubstitutionState ss ) {
+            int gi = ss.getGlyph(), ci;
+            if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) {
+                return false;
+            } else {
+                int[] rv = new int[1];
+                RuleLookup[] la = getLookups ( ci, gi, ss, rv );
+                if ( la != null ) {
+                    ss.apply ( la, rv[0] );
+                }
+                return true;
+            }
+        }
+        /**
+         * Obtain rule lookups set associated current input glyph context.
+         * @param ci coverage index of glyph at current position
+         * @param gi glyph index of glyph at current position
+         * @param ss glyph substitution state
+         * @param rv array of ints used to receive multiple return values, must be of length 1 or greater,
+         * where the first entry is used to return the input sequence length of the matched rule
+         * @return array of rule lookups or null if none applies
+         */
+        public abstract RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv );
+        static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new ContextualSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else if ( format == 2 ) {
+                return new ContextualSubtableFormat2 ( id, sequence, flags, format, coverage, entries );
+            } else if ( format == 3 ) {
+                return new ContextualSubtableFormat3 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class ContextualSubtableFormat1 extends ContextualSubtable {
+        private RuleSet[] rsa;                          // rule set array, ordered by glyph coverage index
+        ContextualSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
         }
         /** {@inheritDoc} */
         public List getEntries() {
-            return null; // [TBD] - implement me
+            if ( rsa != null ) {
+                List entries = new ArrayList ( 1 );
+                entries.add ( rsa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            GlyphTable.resolveLookupReferences ( rsa, lookupTables );
+        }
+        /** {@inheritDoc} */
+        public RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv  ) {
+            assert ss != null;
+            assert ( rv != null ) && ( rv.length > 0 );
+            assert rsa != null;
+            if ( rsa.length > 0 ) {
+                RuleSet rs = rsa [ 0 ];
+                if ( rs != null ) {
+                    Rule[] ra = rs.getRules();
+                    for ( int i = 0, n = ra.length; i < n; i++ ) {
+                        Rule r = ra [ i ];
+                        if ( ( r != null ) && ( r instanceof ChainedGlyphSequenceRule ) ) {
+                            ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r;
+                            int[] iga = cr.getGlyphs ( gi );
+                            if ( matches ( ss, iga, 0, rv ) ) {
+                                return r.getLookups();
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        static boolean matches ( GlyphSubstitutionState ss, int[] glyphs, int offset, int[] rv ) {
+            if ( ( glyphs == null ) || ( glyphs.length == 0 ) ) {
+                return true;                            // match null or empty glyph sequence
+            } else {
+                boolean reverse = offset < 0;
+                GlyphTester ignores = ss.getIgnoreDefault();
+                int[] counts = ss.getGlyphsAvailable ( offset, reverse, ignores );
+                int nga = counts[0];
+                int ngm = glyphs.length;
+                if ( nga < ngm ) {
+                    return false;                       // insufficient glyphs available to match
+                } else {
+                    int[] ga = ss.getGlyphs ( offset, ngm, reverse, ignores, null, counts );
+                    for ( int k = 0; k < ngm; k++ ) {
+                        if ( ga [ k ] != glyphs [ k ] ) {
+                            return false;               // match fails at ga [ k ]
+                        }
+                    }
+                    if ( rv != null ) {
+                        rv[0] = counts[0] + counts[1];
+                    }
+                    return true;                        // all glyphs match
+                }
+            }
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 1 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    rsa = (RuleSet[]) o;
+                }
+            }
         }
     }
 
-    static class ChainingContextSubtable extends GlyphSubstitutionSubtable {
-        public ChainingContextSubtable ( String id, int sequence, int flags, int format, List coverage, List entries ) {
-            super ( id, sequence, flags, format, GlyphCoverageTable.createCoverageTable ( coverage ) );
+    private static class ContextualSubtableFormat2 extends ContextualSubtable {
+        private GlyphClassTable cdt;                    // class def table
+        private int ngc;                                // class set count
+        private RuleSet[] rsa;                          // rule set array, ordered by class number [0...ngc - 1]
+        ContextualSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
         }
         /** {@inheritDoc} */
-        public int getType() {
-            return GSUB_LOOKUP_TYPE_CHAINING_CONTEXT;
+        public List getEntries() {
+            if ( rsa != null ) {
+                List entries = new ArrayList ( 3 );
+                entries.add ( cdt );
+                entries.add ( Integer.valueOf ( ngc ) );
+                entries.add ( rsa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            GlyphTable.resolveLookupReferences ( rsa, lookupTables );
+        }
+        /** {@inheritDoc} */
+        public RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv  ) {
+            assert ss != null;
+            assert ( rv != null ) && ( rv.length > 0 );
+            assert rsa != null;
+            if ( rsa.length > 0 ) {
+                RuleSet rs = rsa [ 0 ];
+                if ( rs != null ) {
+                    Rule[] ra = rs.getRules();
+                    for ( int i = 0, n = ra.length; i < n; i++ ) {
+                        Rule r = ra [ i ];
+                        if ( ( r != null ) && ( r instanceof ChainedClassSequenceRule ) ) {
+                            ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r;
+                            int[] ca = cr.getClasses ( cdt.getClassIndex ( gi, ss.getClassMatchSet ( gi ) ) );
+                            if ( matches ( ss, cdt, ca, 0, rv ) ) {
+                                return r.getLookups();
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        static boolean matches ( GlyphSubstitutionState ss, GlyphClassTable cdt, int[] classes, int offset, int[] rv ) {
+            if ( ( cdt == null ) || ( classes == null ) || ( classes.length == 0 ) ) {
+                return true;                            // match null class definitions, null or empty class sequence
+            } else {
+                boolean reverse = offset < 0;
+                GlyphTester ignores = ss.getIgnoreDefault();
+                int[] counts = ss.getGlyphsAvailable ( offset, reverse, ignores );
+                int nga = counts[0];
+                int ngm = classes.length;
+                if ( nga < ngm ) {
+                    return false;                       // insufficient glyphs available to match
+                } else {
+                    int[] ga = ss.getGlyphs ( offset, ngm, reverse, ignores, null, counts );
+                    for ( int k = 0; k < ngm; k++ ) {
+                        int gi = ga [ k ];
+                        int ms = ss.getClassMatchSet ( gi );
+                        int gc = cdt.getClassIndex ( gi, ms );
+                        if ( ( gc < 0 ) || ( gc >= cdt.getClassSize ( ms ) ) ) {
+                            return false;               // none or invalid class fails mat ch
+                        } else if ( gc != classes [ k ] ) {
+                            return false;               // match fails at ga [ k ]
+                        }
+                    }
+                    if ( rv != null ) {
+                        rv[0] = counts[0] + counts[1];
+                    }
+                    return true;                        // all glyphs match
+                }
+            }
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 3 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 3 entries" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphClassTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an GlyphClassTable, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    cdt = (GlyphClassTable) o;
+                }
+                if ( ( ( o = entries.get(1) ) == null ) || ! ( o instanceof Integer ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, second entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    ngc = ((Integer)(o)).intValue();
+                }
+                if ( ( ( o = entries.get(2) ) == null ) || ! ( o instanceof RuleSet[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, third entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    rsa = (RuleSet[]) o;
+                    if ( rsa.length != ngc ) {
+                        throw new IllegalArgumentException ( "illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes" );
+                    }
+                }
+            }
+        }
+    }
+
+    private static class ContextualSubtableFormat3 extends ContextualSubtable {
+        private RuleSet[] rsa;                          // rule set array, containing a single rule set
+        ContextualSubtableFormat3 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
         }
         /** {@inheritDoc} */
         public List getEntries() {
-            return null; // [TBD] - implement me
+            if ( rsa != null ) {
+                List entries = new ArrayList ( 1 );
+                entries.add ( rsa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            GlyphTable.resolveLookupReferences ( rsa, lookupTables );
+        }
+        /** {@inheritDoc} */
+        public RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv  ) {
+            assert ss != null;
+            assert ( rv != null ) && ( rv.length > 0 );
+            assert rsa != null;
+            if ( rsa.length > 0 ) {
+                RuleSet rs = rsa [ 0 ];
+                if ( rs != null ) {
+                    Rule[] ra = rs.getRules();
+                    for ( int i = 0, n = ra.length; i < n; i++ ) {
+                        Rule r = ra [ i ];
+                        if ( ( r != null ) && ( r instanceof ChainedCoverageSequenceRule ) ) {
+                            ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r;
+                            GlyphCoverageTable[] gca = cr.getCoverages();
+                            if ( matches ( ss, gca, 0, rv ) ) {
+                                return r.getLookups();
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        static boolean matches ( GlyphSubstitutionState ss, GlyphCoverageTable[] gca, int offset, int[] rv ) {
+            if ( ( gca == null ) || ( gca.length == 0 ) ) {
+                return true;                            // match null or empty coverage array
+            } else {
+                boolean reverse = offset < 0;
+                GlyphTester ignores = ss.getIgnoreDefault();
+                int[] counts = ss.getGlyphsAvailable ( offset, reverse, ignores );
+                int nga = counts[0];
+                int ngm = gca.length;
+                if ( nga < ngm ) {
+                    return false;                       // insufficient glyphs available to match
+                } else {
+                    int[] ga = ss.getGlyphs ( offset, ngm, reverse, ignores, null, counts );
+                    for ( int k = 0; k < ngm; k++ ) {
+                        GlyphCoverageTable ct = gca [ k ];
+                        if ( ct != null ) {
+                            if ( ct.getCoverageIndex ( ga [ k ] ) < 0 ) {
+                                return false;           // match fails at ga [ k ]
+                            }
+                        }
+                    }
+                    if ( rv != null ) {
+                        rv[0] = counts[0] + counts[1];
+                    }
+                    return true;                        // all glyphs match
+                }
+            }
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 1 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    rsa = (RuleSet[]) o;
+                }
+            }
         }
     }
 
-    static class ExtensionSubtable extends GlyphSubstitutionSubtable {
-        public ExtensionSubtable ( String id, int sequence, int flags, int format, List coverage, List entries ) {
-            super ( id, sequence, flags, format, GlyphCoverageTable.createCoverageTable ( coverage ) );
+    private abstract static class ChainedContextualSubtable extends GlyphSubstitutionSubtable {
+        public ChainedContextualSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
         }
         /** {@inheritDoc} */
         public int getType() {
-            return GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION;
+            return GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof ChainedContextualSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean substitute ( GlyphSubstitutionState ss ) {
+            int gi = ss.getGlyph(), ci;
+            if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) {
+                return false;
+            } else {
+                int[] rv = new int[1];
+                RuleLookup[] la = getLookups ( ci, gi, ss, rv );
+                if ( la != null ) {
+                    ss.apply ( la, rv[0] );
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+        }
+        /**
+         * Obtain rule lookups set associated current input glyph context.
+         * @param ci coverage index of glyph at current position
+         * @param gi glyph index of glyph at current position
+         * @param ss glyph substitution state
+         * @param rv array of ints used to receive multiple return values, must be of length 1 or greater
+         * @return array of rule lookups or null if none applies
+         */
+        public abstract RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv );
+        static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new ChainedContextualSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else if ( format == 2 ) {
+                return new ChainedContextualSubtableFormat2 ( id, sequence, flags, format, coverage, entries );
+            } else if ( format == 3 ) {
+                return new ChainedContextualSubtableFormat3 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class ChainedContextualSubtableFormat1 extends ChainedContextualSubtable {
+        private RuleSet[] rsa;                          // rule set array, ordered by glyph coverage index
+        ChainedContextualSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( rsa != null ) {
+                List entries = new ArrayList ( 1 );
+                entries.add ( rsa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            GlyphTable.resolveLookupReferences ( rsa, lookupTables );
+        }
+        /** {@inheritDoc} */
+        public RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv  ) {
+            assert ss != null;
+            assert ( rv != null ) && ( rv.length > 0 );
+            assert rsa != null;
+            if ( rsa.length > 0 ) {
+                RuleSet rs = rsa [ 0 ];
+                if ( rs != null ) {
+                    Rule[] ra = rs.getRules();
+                    for ( int i = 0, n = ra.length; i < n; i++ ) {
+                        Rule r = ra [ i ];
+                        if ( ( r != null ) && ( r instanceof ChainedGlyphSequenceRule ) ) {
+                            ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r;
+                            int[] iga = cr.getGlyphs ( gi );
+                            if ( matches ( ss, iga, 0, rv ) ) {
+                                int[] bga = cr.getBacktrackGlyphs();
+                                if ( matches ( ss, bga, -1, null ) ) {
+                                    int[] lga = cr.getLookaheadGlyphs();
+                                    if ( matches ( ss, lga, rv[0], null ) ) {
+                                        return r.getLookups();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        private boolean matches ( GlyphSubstitutionState ss, int[] glyphs, int offset, int[] rv ) {
+            return ContextualSubtableFormat1.matches ( ss, glyphs, offset, rv );
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 1 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    rsa = (RuleSet[]) o;
+                }
+            }
+        }
+    }
+
+    private static class ChainedContextualSubtableFormat2 extends ChainedContextualSubtable {
+        private GlyphClassTable icdt;                   // input class def table
+        private GlyphClassTable bcdt;                   // backtrack class def table
+        private GlyphClassTable lcdt;                   // lookahead class def table
+        private int ngc;                                // class set count
+        private RuleSet[] rsa;                          // rule set array, ordered by class number [0...ngc - 1]
+        ChainedContextualSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
+        }
+        /** {@inheritDoc} */
+        public List getEntries() {
+            if ( rsa != null ) {
+                List entries = new ArrayList ( 5 );
+                entries.add ( icdt );
+                entries.add ( bcdt );
+                entries.add ( lcdt );
+                entries.add ( Integer.valueOf ( ngc ) );
+                entries.add ( rsa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv  ) {
+            assert ss != null;
+            assert ( rv != null ) && ( rv.length > 0 );
+            assert rsa != null;
+            if ( rsa.length > 0 ) {
+                RuleSet rs = rsa [ 0 ];
+                if ( rs != null ) {
+                    Rule[] ra = rs.getRules();
+                    for ( int i = 0, n = ra.length; i < n; i++ ) {
+                        Rule r = ra [ i ];
+                        if ( ( r != null ) && ( r instanceof ChainedClassSequenceRule ) ) {
+                            ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r;
+                            int[] ica = cr.getClasses ( icdt.getClassIndex ( gi, ss.getClassMatchSet ( gi ) ) );
+                            if ( matches ( ss, icdt, ica, 0, rv ) ) {
+                                int[] bca = cr.getBacktrackClasses();
+                                if ( matches ( ss, bcdt, bca, -1, null ) ) {
+                                    int[] lca = cr.getLookaheadClasses();
+                                    if ( matches ( ss, lcdt, lca, rv[0], null ) ) {
+                                        return r.getLookups();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        private boolean matches ( GlyphSubstitutionState ss, GlyphClassTable cdt, int[] classes, int offset, int[] rv ) {
+            return ContextualSubtableFormat2.matches ( ss, cdt, classes, offset, rv );
+        }
+        /** {@inheritDoc} */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            GlyphTable.resolveLookupReferences ( rsa, lookupTables );
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 5 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 5 entries" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphClassTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an GlyphClassTable, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    icdt = (GlyphClassTable) o;
+                }
+                if ( ( ( o = entries.get(1) ) != null ) && ! ( o instanceof GlyphClassTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, second entry must be an GlyphClassTable, but is: " + o.getClass() );
+                } else {
+                    bcdt = (GlyphClassTable) o;
+                }
+                if ( ( ( o = entries.get(2) ) != null ) && ! ( o instanceof GlyphClassTable ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, third entry must be an GlyphClassTable, but is: " + o.getClass() );
+                } else {
+                    lcdt = (GlyphClassTable) o;
+                }
+                if ( ( ( o = entries.get(3) ) == null ) || ! ( o instanceof Integer ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, fourth entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    ngc = ((Integer)(o)).intValue();
+                }
+                if ( ( ( o = entries.get(4) ) == null ) || ! ( o instanceof RuleSet[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, fifth entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    rsa = (RuleSet[]) o;
+                    if ( rsa.length != ngc ) {
+                        throw new IllegalArgumentException ( "illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes" );
+                    }
+                }
+            }
+        }
+    }
+
+    private static class ChainedContextualSubtableFormat3 extends ChainedContextualSubtable {
+        private RuleSet[] rsa;                          // rule set array, containing a single rule set
+        ChainedContextualSubtableFormat3 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
         }
         /** {@inheritDoc} */
         public List getEntries() {
-            return null; // [TBD] - implement me
+            if ( rsa != null ) {
+                List entries = new ArrayList ( 1 );
+                entries.add ( rsa );
+                return entries;
+            } else {
+                return null;
+            }
+        }
+        /** {@inheritDoc} */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            GlyphTable.resolveLookupReferences ( rsa, lookupTables );
+        }
+        /** {@inheritDoc} */
+        public RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv  ) {
+            assert ss != null;
+            assert ( rv != null ) && ( rv.length > 0 );
+            assert rsa != null;
+            if ( rsa.length > 0 ) {
+                RuleSet rs = rsa [ 0 ];
+                if ( rs != null ) {
+                    Rule[] ra = rs.getRules();
+                    for ( int i = 0, n = ra.length; i < n; i++ ) {
+                        Rule r = ra [ i ];
+                        if ( ( r != null ) && ( r instanceof ChainedCoverageSequenceRule ) ) {
+                            ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r;
+                            GlyphCoverageTable[] igca = cr.getCoverages();
+                            if ( matches ( ss, igca, 0, rv ) ) {
+                                GlyphCoverageTable[] bgca = cr.getBacktrackCoverages();
+                                if ( matches ( ss, bgca, -1, null ) ) {
+                                    GlyphCoverageTable[] lgca = cr.getLookaheadCoverages();
+                                    if ( matches ( ss, lgca, rv[0], null ) ) {
+                                        return r.getLookups();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+        private boolean matches ( GlyphSubstitutionState ss, GlyphCoverageTable[] gca, int offset, int[] rv ) {
+            return ContextualSubtableFormat3.matches ( ss, gca, offset, rv );
+        }
+        private void populate ( List entries ) {
+            if ( entries == null ) {
+                throw new IllegalArgumentException ( "illegal entries, must be non-null" );
+            } else if ( entries.size() != 1 ) {
+                throw new IllegalArgumentException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" );
+            } else {
+                Object o;
+                if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) {
+                    throw new IllegalArgumentException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) );
+                } else {
+                    rsa = (RuleSet[]) o;
+                }
+            }
         }
     }
 
-    static class ReverseChainingSingleSubtable extends GlyphSubstitutionSubtable {
-        public ReverseChainingSingleSubtable ( String id, int sequence, int flags, int format, List coverage, List entries ) {
-            super ( id, sequence, flags, format, GlyphCoverageTable.createCoverageTable ( coverage ) );
+    private abstract static class ReverseChainedSingleSubtable extends GlyphSubstitutionSubtable {
+        public ReverseChainedSingleSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage );
         }
         /** {@inheritDoc} */
         public int getType() {
-            return GSUB_LOOKUP_TYPE_REVERSE_CHAINING_CONTEXT_SINGLE;
+            return GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE;
+        }
+        /** {@inheritDoc} */
+        public boolean isCompatible ( GlyphSubtable subtable ) {
+            return subtable instanceof ReverseChainedSingleSubtable;
+        }
+        /** {@inheritDoc} */
+        public boolean usesReverseScan() {
+            return true;
+        }
+        static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            if ( format == 1 ) {
+                return new ReverseChainedSingleSubtableFormat1 ( id, sequence, flags, format, coverage, entries );
+            } else {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
+
+    private static class ReverseChainedSingleSubtableFormat1 extends ReverseChainedSingleSubtable {
+        ReverseChainedSingleSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) {
+            super ( id, sequence, flags, format, coverage, entries );
+            populate ( entries );
         }
         /** {@inheritDoc} */
         public List getEntries() {
-            return null; // [TBD] - implement me
+            return null;
+        }
+        private void populate ( List entries ) {
         }
     }
 
@@ -491,9 +1337,9 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
                 throw new IllegalArgumentException ( "invalid ligature components, must be non-empty array" );
             } else {
                 for ( int i = 0, n = components.length; i < n; i++ ) {
-                    int c = components [ i ];
-                    if ( ( c < 0 ) || ( c > 65535 ) ) {
-                        throw new IllegalArgumentException ( "invalid component glyph index: " + c );
+                    int gc = components [ i ];
+                    if ( ( gc < 0 ) || ( gc > 65535 ) ) {
+                        throw new IllegalArgumentException ( "invalid component glyph index: " + gc );
                     }
                 }
                 this.ligature = ligature;
@@ -517,17 +1363,16 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
         }
 
         /**
-         * Determine of input sequence at offset matches ligature's components.
-         * @param cs glyph (or character) sequence to match this ligature against
-         * @param offset index at which to start matching the components of this ligature
+         * Determine if input sequence at offset matches ligature's components.
+         * @param glyphs array of glyph components to match (including first, implied glyph)
          * @return true if matches
          */
-        public boolean matchesComponents ( CharSequence cs, int offset ) {
-            if ( ( offset + components.length ) > cs.length() ) {
+        public boolean matchesComponents ( int[] glyphs ) {
+            if ( glyphs.length < ( components.length + 1 ) ) {
                 return false;
             } else {
                 for ( int i = 0, n = components.length; i < n; i++ ) {
-                    if ( (int) cs.charAt ( offset + i ) != components [ i ] ) {
+                    if ( glyphs [ i + 1 ] != components [ i ] ) {
                         return false;
                     }
                 }
@@ -559,6 +1404,7 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
     public static class LigatureSet {
 
         private final Ligature[] ligatures;                     // set of ligatures all of which share the first (implied) component
+        private final int maxComponents;                        // maximum number of components (including first)
 
         /**
          * Instantiate a set of ligatures.
@@ -577,6 +1423,15 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
                 throw new IllegalArgumentException ( "invalid ligatures, must be non-empty array" );
             } else {
                 this.ligatures = ligatures;
+                int ncMax = -1;
+                for ( int i = 0, n = ligatures.length; i < n; i++ ) {
+                    Ligature l = ligatures [ i ];
+                    int nc = l.getNumComponents() + 1;
+                    if ( nc > ncMax ) {
+                        ncMax = nc;
+                    }
+                }
+                maxComponents = ncMax;
             }
         }
 
@@ -590,6 +1445,11 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
             return ligatures.length;
         }
 
+        /** @return maximum number of components in one ligature (including first component) */
+        public int getMaxComponents() {
+            return maxComponents;
+        }
+
         /** {@inheritDoc} */
         public String toString() {
             StringBuffer sb = new StringBuffer();
@@ -607,3 +1467,4 @@ public class GlyphSubstitutionTable extends GlyphTable implements GlyphSubstitut
     }
 
 }
+
index 830bad7075d4af0e0b26ba3bad056ee5139748e6..2b245086394a20e39e3bd0488146e3fd872e7561 100644 (file)
 
 package org.apache.fop.fonts;
 
+import java.lang.ref.WeakReference;
+
 import java.util.List;
+import java.util.Map;
 
+// CSOFF: InnerAssignmentCheck
 // CSOFF: LineLengthCheck
 
 /**
@@ -28,40 +32,64 @@ import java.util.List;
  * encapsulates identification, type, format, and coverage information.
  * @author Glenn Adams
  */
-public abstract class GlyphSubtable {
+public abstract class GlyphSubtable implements Comparable {
+
+    /** lookup flag - right to left */
+    public static final int LF_RIGHT_TO_LEFT = 0x0001;
+    /** lookup flag - ignore base glyphs */
+    public static final int LF_IGNORE_BASE = 0x0002;
+    /** lookup flag - ignore ligatures */
+    public static final int LF_IGNORE_LIGATURE = 0x0004;
+    /** lookup flag - ignore marks */
+    public static final int LF_IGNORE_MARK = 0x0008;
+    /** lookup flag - use mark filtering set */
+    public static final int LF_USE_MARK_FILTERING_SET = 0x0010;
+    /** lookup flag - reserved */
+    public static final int LF_RESERVED = 0x0E00;
+    /** lookup flag - mark attachment type */
+    public static final int LF_MARK_ATTACHMENT_TYPE = 0xFF00;
+    /** internal flag - use reverse scan */
+    public static final int LF_INTERNAL_USE_REVERSE_SCAN = 0x10000;
 
-    private String id;
+    /** lookup identifier, having form of "lu%d" where %d is index of lookup in lookup list; shared by multiple subtables in a single lookup  */
+    private String lookupId;
+    /** subtable sequence (index) number in lookup, zero based */
     private int sequence;
+    /** subtable flags */
     private int flags;
+    /** subtable format */
     private int format;
-    private GlyphCoverageTable coverage;
+    /** subtable mapping table */
+    private GlyphMappingTable mapping;
+    /** weak reference to parent (gsub or gpos) table */
+    private WeakReference table;
 
     /**
      * Instantiate this glyph subtable.
-     * @param id subtable identifier
-     * @param sequence subtable sequence
+     * @param lookupId lookup identifier, having form of "lu%d" where %d is index of lookup in lookup list
+     * @param sequence subtable sequence (within lookup), starting with zero
      * @param flags subtable flags
      * @param format subtable format
-     * @param coverage subtable coverage table
+     * @param mapping subtable mapping table
      */
-    protected GlyphSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage )
+    protected GlyphSubtable ( String lookupId, int sequence, int flags, int format, GlyphMappingTable mapping )
     {
-        if ( ( id == null ) || ( id.length() == 0 ) ) {
+        if ( ( lookupId == null ) || ( lookupId.length() == 0 ) ) {
             throw new IllegalArgumentException ( "invalid lookup identifier, must be non-empty string" );
-        } else if ( coverage == null ) {
-            throw new IllegalArgumentException ( "invalid coverage table, must not be null" );
+        } else if ( mapping == null ) {
+            throw new IllegalArgumentException ( "invalid mapping table, must not be null" );
         } else {
-            this.id = id;
+            this.lookupId = lookupId;
             this.sequence = sequence;
             this.flags = flags;
             this.format = format;
-            this.coverage = coverage;
+            this.mapping = mapping;
         }
     }
 
-    /** @return this subtable's identifer */
-    public String getID() {
-        return id;
+    /** @return this subtable's lookup identifer */
+    public String getLookupId() {
+        return lookupId;
     }
 
     /** @return this subtable's table type */
@@ -73,7 +101,21 @@ public abstract class GlyphSubtable {
     /** @return this subtable's type name */
     public abstract String getTypeName();
 
-    /** @return this subtable's sequence */
+    /**
+     * Determine if a glyph subtable is compatible with this glyph subtable. Two glyph subtables are
+     * compatible if the both may appear in a single lookup table.
+     * @param subtable a glyph subtable to determine compatibility
+     * @return true if specified subtable is compatible with this glyph subtable, where by compatible
+     * is meant that they share the same lookup type
+     */
+    public abstract boolean isCompatible ( GlyphSubtable subtable );
+
+    /** @return true if subtable uses reverse scanning of glyph sequence, meaning from the last glyph
+     * in a glyph sequence to the first glyph
+     */
+    public abstract boolean usesReverseScan();
+
+    /** @return this subtable's sequence (index) within lookup */
     public int getSequence() {
         return sequence;
     }
@@ -88,21 +130,185 @@ public abstract class GlyphSubtable {
         return format;
     }
 
-    /** @return this subtable's coverage table */
-    public GlyphCoverageTable getCoverage() {
-        return coverage;
+    /** @return this subtable's governing glyph definition table or null if none available */
+    public GlyphDefinitionTable getGDEF() {
+        GlyphTable gt = getTable();
+        if ( gt != null ) {
+            return gt.getGlyphDefinitions();
+        } else {
+            return null;
+        }
+    }
+
+    /** @return this subtable's coverage mapping or null if mapping is not a coverage mapping */
+    public GlyphCoverageMapping getCoverage() {
+        if ( mapping instanceof GlyphCoverageMapping ) {
+            return (GlyphCoverageMapping) mapping;
+        } else {
+            return null;
+        }
+    }
+
+    /** @return this subtable's class mapping or null if mapping is not a class mapping */
+    public GlyphClassMapping getClasses() {
+        if ( mapping instanceof GlyphClassMapping ) {
+            return (GlyphClassMapping) mapping;
+        } else {
+            return null;
+        }
     }
 
     /** @return this subtable's lookup entries */
     public abstract List getEntries();
 
+    /** @return this subtable's parent table (or null if undefined) */
+    public synchronized GlyphTable getTable() {
+        WeakReference r = this.table;
+        return ( r != null ) ? (GlyphTable) r.get() : null;
+    }
+
+    /**
+     * Establish a weak reference from this subtable to its parent
+     * table. If table parameter is specified as <code>null</code>, then
+     * clear and remove weak reference.
+     * @param table the table or null
+     * @throws IllegalStateException if table is already set to non-null
+     */
+    public synchronized void setTable ( GlyphTable table ) throws IllegalStateException {
+        WeakReference r = this.table;
+        if ( table == null ) {
+            this.table = null;
+            if ( r != null ) {
+                r.clear();
+            }
+        } else if ( r == null ) {
+            this.table = new WeakReference ( table );
+        } else {
+            throw new IllegalStateException ( "table already set" );
+        }
+    }
+
+    /**
+     * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves.
+     * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
+     */
+    public void resolveLookupReferences ( Map/*<String,GlyphTable.LookupTable>*/ lookupTables ) {
+    }
+
     /**
      * Map glyph id to coverage index.
      * @param gid glyph id
      * @return the corresponding coverage index of the specified glyph id
      */
     public int getCoverageIndex ( int gid ) {
-        return coverage.getCoverageIndex ( gid );
+        if ( mapping instanceof GlyphCoverageMapping ) {
+            return ( (GlyphCoverageMapping) mapping ) .getCoverageIndex ( gid );
+        } else {
+            return -1;
+        }
+    }
+
+    /**
+     * Map glyph id to coverage index.
+     * @return the corresponding coverage index of the specified glyph id
+     */
+    public int getCoverageSize() {
+        if ( mapping instanceof GlyphCoverageMapping ) {
+            return ( (GlyphCoverageMapping) mapping ) .getCoverageSize();
+        } else {
+            return 0;
+        }
+    }
+
+    /** {@inheritDoc} */
+    public int hashCode() {
+        int hc = sequence;
+        hc = ( hc * 3 ) + ( lookupId.hashCode() ^ hc );
+        return hc;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @return true if the lookup identifier and the sequence number of the specified subtable is the same
+     * as the lookup identifier and sequence number of this subtable
+     */
+    public boolean equals ( Object o ) {
+        if ( o instanceof GlyphSubtable ) {
+            GlyphSubtable st = (GlyphSubtable) o;
+            return lookupId.equals ( st.lookupId ) && ( sequence == st.sequence );
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * @return the result of comparing the lookup identifier and the sequence number of the specified subtable with
+     * the lookup identifier and sequence number of this subtable
+     */
+    public int compareTo ( Object o ) {
+        int d;
+        if ( o instanceof GlyphSubtable ) {
+            GlyphSubtable st = (GlyphSubtable) o;
+            if ( ( d = lookupId.compareTo ( st.lookupId ) ) == 0 ) {
+                if ( sequence < st.sequence ) {
+                    d = -1;
+                } else if ( sequence > st.sequence ) {
+                    d = 1;
+                }
+            }
+        } else {
+            d = -1;
+        }
+        return d;
+    }
+
+    /**
+     * Determine if any of the specified subtables uses reverse scanning.
+     * @param subtables array of glyph subtables
+     * @return true if any of the specified subtables uses reverse scanning.
+     */
+    public static boolean usesReverseScan ( GlyphSubtable[] subtables ) {
+        if ( ( subtables == null ) || ( subtables.length == 0 ) ) {
+            return false;
+        } else {
+            for ( int i = 0, n = subtables.length; i < n; i++ ) {
+                if ( subtables[i].usesReverseScan() ) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Determine consistent flags for a set of subtables.
+     * @param subtables array of glyph subtables
+     * @return consistent flags
+     * @throws IllegalStateException if inconsistent flags
+     */
+    public static int getFlags ( GlyphSubtable[] subtables ) throws IllegalStateException {
+        if ( ( subtables == null ) || ( subtables.length == 0 ) ) {
+            return 0;
+        } else {
+            int flags = 0;
+            // obtain first non-zero value of flags in array of subtables
+            for ( int i = 0, n = subtables.length; i < n; i++ ) {
+                int f = subtables[i].getFlags();
+                if ( flags == 0 ) {
+                    flags = f;
+                    break;
+                }
+            }
+            // enforce flag consistency
+            for ( int i = 0, n = subtables.length; i < n; i++ ) {
+                int f = subtables[i].getFlags();
+                if ( f != flags ) {
+                    throw new IllegalStateException ( "inconsistent lookup flags " + f + ", expected " + flags );
+                }
+            }
+            return flags | ( usesReverseScan ( subtables ) ? LF_INTERNAL_USE_REVERSE_SCAN : 0 );
+        }
     }
 
 }
index cd656f03541bedf1ad64e858167016ce2c24bc15..ee80706d19d7769ecba13ce2d1ffbe898cc2ff52 100644 (file)
 
 package org.apache.fop.fonts;
 
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
 
-// CSOFF: NoWhitespaceAfterCheck
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+// CSOFF: EmptyForIteratorPadCheck
 // CSOFF: InnerAssignmentCheck
-// CSOFF: SimplifyBooleanReturnCheck
 // CSOFF: LineLengthCheck
+// CSOFF: NoWhitespaceAfterCheck
+// CSOFF: ParameterNumberCheck
+// CSOFF: SimplifyBooleanReturnCheck
 
 /**
  * Base class for all advanced typographic glyph tables.
@@ -38,6 +47,9 @@ import java.util.Set;
  */
 public class GlyphTable {
 
+    /** logging instance */
+    private static final Log log = LogFactory.getLog(GlyphTable.class);                                                 // CSOK: ConstantNameCheck
+
     /** substitution glyph table type */
     public static final int GLYPH_TABLE_TYPE_SUBSTITUTION = 1;
     /** positioning glyph table type */
@@ -49,48 +61,100 @@ public class GlyphTable {
     /** definition glyph table type */
     public static final int GLYPH_TABLE_TYPE_DEFINITION = 5;
 
-    // map from lookup specs to lists of strings, each naming a subtable
-    private Map /*<LookupSpec,List>*/ lookups;
+    // (optional) glyph definition table in table types other than glyph definition table
+    private GlyphTable gdef;
+
+    // map from lookup specs to lists of strings, each of which identifies a lookup table (consisting of one or more subtables)
+    private Map/*<LookupSpec,List<String>>*/ lookups;
 
-    // map from subtable names to glyph subtables
-    private Map /*<String,GlyphSubtable>*/ subtables;
+    // map from lookup identifiers to lookup tables
+    private Map/*<String,LookupTable>*/ lookupTables;
+
+    // if true, then prevent further subtable addition
+    private boolean frozen;
 
     /**
      * Instantiate glyph table with specified lookups.
+     * @param gdef glyph definition table that applies
      * @param lookups map from lookup specs to lookup tables
      */
-    public GlyphTable ( Map /*<LookupSpec,List>*/ lookups ) {
-        if ( ( lookups == null ) || ( lookups.size() == 0 ) ) {
-            throw new IllegalArgumentException ( "lookups must be non-empty map" );
+    public GlyphTable ( GlyphTable gdef, Map/*<LookupSpec,List<String>>*/ lookups ) {
+        if ( ( gdef != null ) && ! ( gdef instanceof GlyphDefinitionTable ) ) {
+            throw new IllegalArgumentException ( "bad glyph definition table" );
+        } else if ( lookups == null ) {
+            throw new IllegalArgumentException ( "lookups must be non-null map" );
         } else {
+            this.gdef = gdef;
             this.lookups = lookups;
-            this.subtables = new LinkedHashMap();
+            this.lookupTables = new LinkedHashMap/*<String,List<LookupTable>>*/();
         }
     }
 
     /**
-     * Obain array of lookup specifications.
-     * @return (possibly empty) array of all lookup specifications
+     * Obtain glyph definition table.
+     * @return (possibly null) glyph definition table
      */
-    public LookupSpec[] getLookups() {
+    public GlyphDefinitionTable getGlyphDefinitions() {
+        return (GlyphDefinitionTable) gdef;
+    }
+
+    /**
+     * Obtain list of all lookup specifications.
+     * @return (possibly empty) list of all lookup specifications
+     */
+    public List/*<LookupSpec>*/ getLookups() {
         return matchLookupSpecs ( "*", "*", "*" );
     }
 
     /**
-     * Obain array of lookup subtables.
-     * @return (possibly empty) array of all lookup subtables
+     * Obtain ordered list of all lookup tables, where order is by lookup identifier, which
+     * lexicographic ordering follows the lookup list order.
+     * @return (possibly empty) ordered list of all lookup tables
      */
-    public GlyphSubtable[] getSubtables() {
-        Collection values = subtables.values();
-        return (GlyphSubtable[]) values.toArray ( new GlyphSubtable [ values.size() ] );
+    public List/*<LookupTable>*/ getLookupTables() {
+        TreeSet/*<String>*/ lids = new TreeSet/*<String>*/ ( lookupTables.keySet() );
+        List/*<LookupTable>*/ ltl = new ArrayList/*<LookupTable>*/ ( lids.size() );
+        for ( Iterator it = lids.iterator(); it.hasNext(); ) {
+            String lid = (String) it.next();
+            ltl.add ( lookupTables.get ( lid ) );
+        }
+        return ltl;
     }
 
     /**
      * Add a subtable.
      * @param subtable a (non-null) glyph subtable
      */
-    public void addSubtable ( GlyphSubtable subtable ) {
-        subtables.put ( subtable.getID(), subtable );
+    protected void addSubtable ( GlyphSubtable subtable ) {
+        // ensure table is not frozen
+        if ( frozen ) {
+            throw new IllegalStateException ( "glyph table is frozen, subtable addition prohibited" );
+        }
+        // set subtable's table reference to this table
+        subtable.setTable ( this );
+        // add subtable to this table's subtable collection
+        String lid = subtable.getLookupId();
+        if ( lookupTables.containsKey ( lid ) ) {
+            LookupTable lt = (LookupTable) lookupTables.get ( lid );
+            lt.addSubtable ( subtable );
+        } else {
+            LookupTable lt = new LookupTable ( lid, subtable );
+            lookupTables.put ( lid, lt );
+        }
+    }
+
+    /**
+     * Freeze subtables, i.e., do not allow further subtable addition, and
+     * create resulting cached state.
+     */
+    protected void freezeSubtables() {
+        if ( ! frozen ) {
+            for ( Iterator it = lookupTables.values().iterator(); it.hasNext(); ) {
+                LookupTable lt = (LookupTable) it.next();
+                lt.freezeSubtables ( lookupTables );
+            }
+            frozen = true;
+        }
     }
 
     /**
@@ -101,9 +165,9 @@ public class GlyphTable {
      * @param feature a feature identifier
      * @return a (possibly empty) array of matching lookup specifications
      */
-    public LookupSpec[] matchLookupSpecs ( String script, String language, String feature ) {
+    public List/*<LookupSpec>*/ matchLookupSpecs ( String script, String language, String feature ) {
         Set/*<LookupSpec>*/ keys = lookups.keySet();
-        List matches = new ArrayList();
+        List/*<LookupSpec>*/ matches = new ArrayList/*<LookupSpec>*/();
         for ( Iterator it = keys.iterator(); it.hasNext();) {
             LookupSpec ls = (LookupSpec) it.next();
             if ( ! "*".equals(script) ) {
@@ -123,7 +187,7 @@ public class GlyphTable {
             }
             matches.add ( ls );
         }
-        return (LookupSpec[]) matches.toArray ( new LookupSpec [ matches.size() ] );
+        return matches;
     }
 
     /**
@@ -132,38 +196,76 @@ public class GlyphTable {
      * @param script a script identifier
      * @param language a language identifier
      * @param feature a feature identifier
-     * @return a (possibly empty) map of matching lookup specifications and their corresponding subtables
+     * @return a (possibly empty) map from matching lookup specifications to lists of corresponding lookup tables
      */
-    public Map/*<LookupSpec,GlyphSubtable[]>*/ matchLookups ( String script, String language, String feature ) {
-        LookupSpec[] lsa = matchLookupSpecs ( script, language, feature );
+    public Map/*<LookupSpec,List<LookupTable>>*/ matchLookups ( String script, String language, String feature ) {
+        List/*<LookupSpec>*/ lsl = matchLookupSpecs ( script, language, feature );
         Map lm = new LinkedHashMap();
-        for ( int i = 0, n = lsa.length; i < n; i++ ) {
-            lm.put ( lsa [ i ], findSubtables ( lsa [ i ] ) );
+        for ( Iterator it = lsl.iterator(); it.hasNext(); ) {
+            LookupSpec ls = (LookupSpec) it.next();
+            lm.put ( ls, findLookupTables ( ls ) );
         }
         return lm;
     }
 
     /**
-     * Find glyph subtables that match a secific lookup specification.
+     * Obtain ordered list of glyph lookup tables that match a specific lookup specification.
      * @param ls a (non-null) lookup specification
-     * @return a (possibly empty) array of subtables whose lookup specification matches the specified lookup spec
+     * @return a (possibly empty) ordered list of lookup tables whose corresponding lookup specifications match the specified lookup spec
      */
-    public GlyphSubtable[] findSubtables ( LookupSpec ls ) {
-        GlyphSubtable[] staEmpty = new GlyphSubtable [ 0 ];
-        List ids;
-        if ( ( ids = (List) lookups.get ( ls ) ) != null ) {
-            List stl = new ArrayList();
+    public List/*<LookupTable>*/ findLookupTables ( LookupSpec ls ) {
+        TreeSet/*<LookupTable>*/ lts = new TreeSet/*<LookupTable>*/();
+        List/*<String>*/ ids;
+        if ( ( ids = (List/*<String>*/) lookups.get ( ls ) ) != null ) {
             for ( Iterator it = ids.iterator(); it.hasNext();) {
-                String id = (String) it.next();
-                GlyphSubtable st;
-                if ( ( st = (GlyphSubtable) subtables.get ( id ) ) != null ) {
-                    stl.add ( st );
+                String lid = (String) it.next();
+                LookupTable lt;
+                if ( ( lt = (LookupTable) lookupTables.get ( lid ) ) != null ) {
+                    lts.add ( lt );
+                }
+            }
+        }
+        return new ArrayList/*<LookupTable>*/ ( lts );
+    }
+
+    /**
+     * Assemble ordered array of lookup table use specifications according to the specified features and candidate lookups,
+     * where the order of the array is in accordance to the order of the applicable lookup list.
+     * @param features array of feature identifiers to apply
+     * @param lookups a mapping from lookup specifications to lists of look tables from which to select lookup tables according to the specified features
+     * @return ordered array of assembled lookup table use specifications
+     */
+    public UseSpec[] assembleLookups ( String[] features, Map/*<LookupSpec,List<LookupTable>>*/ lookups ) {
+        TreeSet/*<UseSpec>*/ uss = new TreeSet/*<UseSpec>*/();
+        for ( int i = 0, n = features.length; i < n; i++ ) {
+            String feature = features[i];
+            for ( Iterator it = lookups.entrySet().iterator(); it.hasNext(); ) {
+                Map.Entry/*<LookupSpec,List<LookupTable>>*/ e = (Map.Entry/*<LookupSpec,List<LookupTable>>*/) it.next();
+                LookupSpec ls = (LookupSpec) e.getKey();
+                if ( ls.getFeature().equals ( feature ) ) {
+                    List/*<LookupTable>*/ ltl = (List/*<LookupTable>*/) e.getValue();
+                    if ( ltl != null ) {
+                        for ( Iterator ltit = ltl.iterator(); ltit.hasNext(); ) {
+                            LookupTable lt = (LookupTable) ltit.next();
+                            uss.add ( new UseSpec ( lt, feature ) );
+                        }
+                    }
                 }
             }
-            return (GlyphSubtable[]) stl.toArray ( staEmpty );
-        } else {
-            return staEmpty;
         }
+        return (UseSpec[]) uss.toArray ( new UseSpec [ uss.size() ] );
+    }
+    
+    /** {@inheritDoc} */
+    public String toString() {
+        StringBuffer sb = new StringBuffer(super.toString());
+        sb.append("{");
+        sb.append("lookups={");
+        sb.append(lookups.toString());
+        sb.append("},lookupTables={");
+        sb.append(lookupTables.toString());
+        sb.append("}}");
+        return sb.toString();
     }
 
     /**
@@ -190,22 +292,26 @@ public class GlyphTable {
         return t;
     }
 
-    /** {@inheritDoc} */
-    public String toString() {
-        StringBuffer sb = new StringBuffer(super.toString());
-        sb.append("{");
-        sb.append("lookups={");
-        sb.append(lookups.toString());
-        sb.append("},subtables={");
-        sb.append(subtables.toString());
-        sb.append("}}");
-        return sb.toString();
+    /**
+     * Resolve references to lookup tables in a collection of rules sets.
+     * @param rsa array of rule sets
+     * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
+     */
+    public static void resolveLookupReferences ( RuleSet[] rsa, Map/*<String,LookupTable>*/ lookupTables ) {
+        if ( ( rsa != null ) && ( lookupTables != null ) ) {
+            for ( int i = 0, n = rsa.length; i < n; i++ ) {
+                RuleSet rs = rsa [ i ];
+                if ( rs != null ) {
+                    rs.resolveLookupReferences ( lookupTables );
+                }
+            }
+        }
     }
 
     /**
      * A structure class encapsulating a lookup specification as a <script,language,feature> tuple.
      */
-    public static class LookupSpec {
+    public static class LookupSpec implements Comparable {
 
         private final String script;
         private final String language;
@@ -224,6 +330,12 @@ public class GlyphTable {
                 throw new IllegalArgumentException ( "language must be non-empty string" );
             } else if ( ( feature == null ) || ( feature.length() == 0 ) ) {
                 throw new IllegalArgumentException ( "feature must be non-empty string" );
+            } else if ( script.equals("*") ) {
+                throw new IllegalArgumentException ( "script must not be wildcard" );
+            } else if ( language.equals("*") ) {
+                throw new IllegalArgumentException ( "language must not be wildcard" );
+            } else if ( feature.equals("*") ) {
+                throw new IllegalArgumentException ( "feature must not be wildcard" );
             } else {
                 this.script = script;
                 this.language = language;
@@ -248,11 +360,11 @@ public class GlyphTable {
 
         /** {@inheritDoc} */
         public int hashCode() {
-            int h = 0;
-            h = 31 * h + script.hashCode();
-            h = 31 * h + language.hashCode();
-            h = 31 * h + feature.hashCode();
-            return h;
+            int hc = 0;
+            hc =  7 * hc + ( hc ^ script.hashCode() );
+            hc = 11 * hc + ( hc ^ language.hashCode() );
+            hc = 17 * hc + ( hc ^ feature.hashCode() );
+            return hc;
         }
 
         /** {@inheritDoc} */
@@ -273,6 +385,24 @@ public class GlyphTable {
             }
         }
 
+        /** {@inheritDoc} */
+        public int compareTo ( Object o ) {
+            int d;
+            if ( o instanceof LookupSpec ) {
+                LookupSpec ls = (LookupSpec) o;
+                if ( ( d = script.compareTo ( ls.script ) ) == 0 ) {
+                    if ( ( d = language.compareTo ( ls.language ) ) == 0 ) {
+                        if ( ( d = feature.compareTo ( ls.feature ) ) == 0 ) {
+                            d = 0;
+                        }
+                    }
+                }
+            } else {
+                d = -1;
+            }
+            return d;
+        }
+
         /** {@inheritDoc} */
         public String toString() {
             StringBuffer sb = new StringBuffer(super.toString());
@@ -286,4 +416,860 @@ public class GlyphTable {
 
     }
 
+    /**
+     * The <code>LookupTable</code> class comprising an identifier and an ordered list
+     * of glyph subtables, each of which employ the same lookup identifier.
+     */
+    public static class LookupTable implements Comparable {
+
+        private final String id;                                // lookup identifiers
+        private final List/*<GlyphSubtable>*/ subtables;        // list of subtables
+        private boolean doesSub;                                // performs substitutions
+        private boolean doesPos;                                // performs positioning
+        private boolean frozen;                                 // if true, then don't permit further subtable additions
+        // frozen state
+        private GlyphSubtable[] subtablesArray;
+        private static GlyphSubtable[] subtablesArrayEmpty       = new GlyphSubtable[0];
+        
+        /**
+         * Instantiate a LookupTable.
+         * @param id the lookup table's identifier
+         * @param subtable an initial subtable (or null)
+         */
+        public LookupTable ( String id, GlyphSubtable subtable ) {
+            this ( id, makeSingleton ( subtable ) );
+        }
+
+        /**
+         * Instantiate a LookupTable.
+         * @param id the lookup table's identifier
+         * @param subtables a pre-poplated list of subtables or null
+         */
+        public LookupTable ( String id, List/*<GlyphSubtable>*/ subtables ) {
+            assert id != null;
+            assert id.length() != 0;
+            this.id = id;
+            this.subtables = new LinkedList/*<GlyphSubtable>*/();
+            if ( subtables != null ) {
+                for ( Iterator it = subtables.iterator(); it.hasNext(); ) {
+                    GlyphSubtable st = (GlyphSubtable) it.next();
+                    addSubtable ( st );
+                }
+            }
+        }
+
+        /** @return the identifier */
+        public String getId() {
+            return id;
+        }
+
+        /** @return the subtables as an array */
+        public GlyphSubtable[] getSubtables() {
+            if ( frozen ) {
+                return ( subtablesArray != null ) ? subtablesArray : subtablesArrayEmpty;
+            } else {
+                if ( doesSub ) {
+                    return (GlyphSubtable[]) subtables.toArray ( new GlyphSubstitutionSubtable [ subtables.size() ] );
+                } else if ( doesPos ) {
+                    return (GlyphSubtable[]) subtables.toArray ( new GlyphPositioningSubtable [ subtables.size() ] );
+                } else {
+                    return null;
+                }
+            }
+        }
+
+        /**
+         * Add a subtable into this lookup table's collecion of subtables according to its
+         * natural order.
+         * @param subtable to add
+         * @return true if subtable was not already present, otherwise false
+         */
+        public boolean addSubtable ( GlyphSubtable subtable ) {
+            boolean added = false;
+            // ensure table is not frozen
+            if ( frozen ) {
+                throw new IllegalStateException ( "glyph table is frozen, subtable addition prohibited" );
+            }
+            // validate subtable to ensure consistency with current subtables
+            validateSubtable ( subtable );
+            // insert subtable into ordered list
+            for ( ListIterator/*<GlyphSubtable>*/ lit = subtables.listIterator(0); lit.hasNext(); ) {
+                GlyphSubtable st = (GlyphSubtable) lit.next();
+                int d;
+                if ( ( d = subtable.compareTo ( st ) ) < 0 ) {
+                    // insert within list
+                    lit.set ( subtable );
+                    lit.add ( st );
+                    added = true;
+                } else if ( d == 0 ) {
+                    // duplicate entry is ignored
+                    added = false; subtable = null;
+                }
+            }
+            // append at end of list
+            if ( ! added && ( subtable != null ) ) {
+                subtables.add ( subtable );
+                added = true;
+            }
+            return added;
+        }
+
+        private void validateSubtable ( GlyphSubtable subtable ) {
+            if ( subtable == null ) {
+                throw new IllegalArgumentException ( "subtable must be non-null" );
+            }
+            if ( subtable instanceof GlyphSubstitutionSubtable ) {
+                if ( doesPos ) {
+                    throw new IllegalArgumentException ( "subtable must be positioning subtable, but is: " + subtable );
+                } else {
+                    doesSub = true;
+                }
+            }
+            if ( subtable instanceof GlyphPositioningSubtable ) {
+                if ( doesSub ) {
+                    throw new IllegalArgumentException ( "subtable must be substitution subtable, but is: " + subtable );
+                } else {
+                    doesPos = true;
+                }
+            }
+            if ( subtables.size() > 0 ) {
+                GlyphSubtable st = (GlyphSubtable) subtables.get(0);
+                if ( ! st.isCompatible ( subtable ) ) {
+                    throw new IllegalArgumentException ( "subtable " + subtable + " is not compatible with subtable " + st );
+                }
+            }
+        }
+
+        /**
+         * Freeze subtables, i.e., do not allow further subtable addition, and
+         * create resulting cached state. In addition, resolve any references to
+         * lookup tables that appear in this lookup table's subtables.
+         * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
+         */
+        public void freezeSubtables ( Map/*<String,LookupTable>*/ lookupTables ) {
+            if ( ! frozen ) {
+                GlyphSubtable[] sta = getSubtables();
+                resolveLookupReferences ( sta, lookupTables );
+                this.subtablesArray = sta;
+                this.frozen = true;
+            }
+        }
+
+        private void resolveLookupReferences ( GlyphSubtable[] subtables, Map/*<String,LookupTable>*/ lookupTables ) {
+            if ( subtables != null ) {
+                for ( int i = 0, n = subtables.length; i < n; i++ ) {
+                    GlyphSubtable st = subtables [ i ];
+                    if ( st != null ) {
+                        st.resolveLookupReferences ( lookupTables );
+                    }
+                }
+            }
+        }
+
+        /**
+         * Determine if this glyph table performs substitution.
+         * @return true if it performs substitution
+         */
+        public boolean performsSubstitution() {
+            return doesSub;
+        }
+
+        /**
+         * Perform substitution processing using this lookup table's subtables.
+         * @param gs an input glyph sequence
+         * @param script a script identifier
+         * @param language a language identifier
+         * @param feature a feature identifier
+         * @param sct a script specific context tester (or null)
+         * @return the substituted (output) glyph sequence
+         */
+        public GlyphSequence substitute ( GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct ) {
+            if ( performsSubstitution() ) {
+                return GlyphSubstitutionSubtable.substitute ( gs, script, language, feature, (GlyphSubstitutionSubtable[]) subtablesArray, sct );
+            } else {
+                return gs;
+            }
+        }
+
+        /**
+         * Perform substitution processing on an existing glyph substitution state object using this lookup table's subtables.
+         * @param ss a glyph substitution state object
+         * @param sequenceIndex if non negative, then apply subtables only at specified sequence index
+         * @return the substituted (output) glyph sequence
+         */
+        public GlyphSequence substitute ( GlyphSubstitutionState ss, int sequenceIndex ) {
+            if ( performsSubstitution() ) {
+                return GlyphSubstitutionSubtable.substitute ( ss, (GlyphSubstitutionSubtable[]) subtablesArray, sequenceIndex );
+            } else {
+                return ss.getInput();
+            }
+        }
+
+        /**
+         * Determine if this glyph table performs positioning.
+         * @return true if it performs positioning
+         */
+        public boolean performsPositioning() {
+            return doesPos;
+        }
+
+        /**
+         * Perform positioning processing using this lookup table's subtables.
+         * @param gs an input glyph sequence
+         * @param script a script identifier
+         * @param language a language identifier
+         * @param feature a feature identifier
+         * @param fontSize size in device units
+         * @param widths array of default advancements for each glyph in font
+         * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
+         * with one 4-tuple for each element of glyph sequence
+         * @param sct a script specific context tester (or null)
+         * @return true if some adjustment is not zero; otherwise, false
+         */
+        public boolean position ( GlyphSequence gs, String script, String language, String feature, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct ) {
+            if ( performsPositioning() ) {
+                return GlyphPositioningSubtable.position ( gs, script, language, feature, fontSize, (GlyphPositioningSubtable[]) subtablesArray, widths, adjustments, sct );
+            } else {
+                return false;
+            }
+        }
+
+        /**
+         * Perform positioning processing on an existing glyph positioning state object using this lookup table's subtables.
+         * @param ps a glyph positioning state object
+         * @param sequenceIndex if non negative, then apply subtables only at specified sequence index
+         * @return true if some adjustment is not zero; otherwise, false
+         */
+        public boolean position ( GlyphPositioningState ps, int sequenceIndex ) {
+            if ( performsPositioning() ) {
+                return GlyphPositioningSubtable.position ( ps, (GlyphPositioningSubtable[]) subtablesArray, sequenceIndex );
+            } else {
+                return false;
+            }
+        }
+
+        /** {@inheritDoc} */
+        public int hashCode() {
+            return id.hashCode();
+        }
+
+        /**
+         * {@inheritDoc}
+         * @return true if identifier of the specified lookup table is the same
+         * as the identifier of this lookup table
+         */
+        public boolean equals ( Object o ) {
+            if ( o instanceof LookupTable ) {
+                LookupTable lt = (LookupTable) o;
+                return id.equals ( lt.id );
+            } else {
+                return false;
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * @return the result of comparing the identifier of the specified lookup table with
+         * the identifier of this lookup table
+         */
+        public int compareTo ( Object o ) {
+            if ( o instanceof LookupTable ) {
+                LookupTable lt = (LookupTable) o;
+                return id.compareTo ( lt.id );
+            } else {
+                return -1;
+            }
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            sb.append ( "{ " );
+            sb.append ( "id = " + id );
+            sb.append ( ", subtables = " + subtables );
+            sb.append ( " }" );
+            return sb.toString();
+        }
+
+        private static List/*<GlyphSubtable>*/ makeSingleton ( GlyphSubtable subtable ) {
+            if ( subtable == null ) {
+                return null;
+            } else {
+                List/*<GlyphSubtable>*/ stl = new ArrayList/*<GlyphSubtable>*/ ( 1 );
+                stl.add ( subtable );
+                return stl;
+            }
+        }
+
+    }
+
+    /**
+     * The <code>UseSpec</code> class comprises a lookup table reference
+     * and the feature that selected the lookup table.
+     */
+    public static class UseSpec implements Comparable {
+
+        /** lookup table to apply */
+        private final LookupTable lookupTable;
+        /** feature that caused selection of the lookup table */
+        private final String feature;
+
+        /**
+         * Construct a glyph lookup table use specification.
+         * @param lookupTable a glyph lookup table
+         * @param feature a feature that caused lookup table selection 
+         */
+        public UseSpec ( LookupTable lookupTable, String feature ) {
+            this.lookupTable = lookupTable;
+            this.feature = feature;
+        }
+
+        /** @return the lookup table */
+        public LookupTable getLookupTable() {
+            return lookupTable;
+        }
+
+        /** @return the feature that selected this lookup table */
+        public String getFeature() {
+            return feature;
+        }
+
+        /**
+         * Perform substitution processing using this use specification's lookup table.
+         * @param gs an input glyph sequence
+         * @param script a script identifier
+         * @param language a language identifier
+         * @param sct a script specific context tester (or null)
+         * @return the substituted (output) glyph sequence
+         */
+        public GlyphSequence substitute ( GlyphSequence gs, String script, String language, ScriptContextTester sct ) {
+            return lookupTable.substitute ( gs, script, language, feature, sct );
+        }
+
+        /**
+         * Perform positioning processing using this use specification's lookup table.
+         * @param gs an input glyph sequence
+         * @param script a script identifier
+         * @param language a language identifier
+         * @param fontSize size in device units
+         * @param widths array of default advancements for each glyph in font
+         * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
+         * with one 4-tuple for each element of glyph sequence
+         * @param sct a script specific context tester (or null)
+         * @return true if some adjustment is not zero; otherwise, false
+         */
+        public boolean position ( GlyphSequence gs, String script, String language, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct ) {
+            return lookupTable.position ( gs, script, language, feature, fontSize, widths, adjustments, sct );
+        }
+
+        /** {@inheritDoc} */
+        public int hashCode() {
+            return lookupTable.hashCode();
+        }
+
+        /** {@inheritDoc} */
+        public boolean equals ( Object o ) {
+            if ( o instanceof UseSpec ) {
+                UseSpec u = (UseSpec) o;
+                return lookupTable.equals ( u.lookupTable );
+            } else {
+                return false;
+            }
+        }
+
+        /** {@inheritDoc} */
+        public int compareTo ( Object o ) {
+            if ( o instanceof UseSpec ) {
+                UseSpec u = (UseSpec) o;
+                return lookupTable.compareTo ( u.lookupTable );
+            } else {
+                return -1;
+            }
+        }
+
+    }
+
+    /**
+     * The <code>RuleLookup</code> class implements a rule lookup record, comprising
+     * a glyph sequence index and a lookup table index (in an applicable lookup list).
+     */
+    public static class RuleLookup {
+
+        private final int sequenceIndex;                        // index into input glyph sequence
+        private final int lookupIndex;                          // lookup list index
+        private LookupTable lookup;                             // resolved lookup table
+
+        /**
+         * Instantiate a RuleLookup.
+         * @param sequenceIndex the index into the input sequence
+         * @param lookupIndex the lookup table index
+         */
+        public RuleLookup ( int sequenceIndex, int lookupIndex ) {
+            this.sequenceIndex = sequenceIndex;
+            this.lookupIndex = lookupIndex;
+            this.lookup = null;
+        }
+
+        /** @return the sequence index */
+        public int getSequenceIndex() {
+            return sequenceIndex;
+        }
+
+        /** @return the lookup index */
+        public int getLookupIndex() {
+            return lookupIndex;
+        }
+
+        /** @return the lookup table */
+        public LookupTable getLookup() {
+            return lookup;
+        }
+
+        /**
+         * Resolve references to lookup tables.
+         * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
+         */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            if ( lookupTables != null ) {
+                String lid = "lu" + Integer.toString ( lookupIndex );
+                LookupTable lt = (LookupTable) lookupTables.get ( lid );
+                if ( lt != null ) {
+                    this.lookup = lt;
+                } else {
+                    log.warn ( "unable to resolve glyph lookup table reference '" + lid + "' amongst lookup tables: " + lookupTables.values() );
+                }
+            }
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            return "{ sequenceIndex = " + sequenceIndex + ", lookupIndex = " + lookupIndex + " }";
+        }
+
+    }
+
+    /**
+     * The <code>Rule</code> class implements an array of rule lookup records.
+     */
+    public abstract static class Rule {
+
+        private final RuleLookup[] lookups;                     // rule lookups
+        private final int inputSequenceLength;                  // input sequence length
+
+        /**
+         * Instantiate a Rule.
+         * @param lookups the rule's lookups
+         * @param inputSequenceLength the number of glyphs in the input sequence for this rule
+         */
+        protected Rule ( RuleLookup[] lookups, int inputSequenceLength ) {
+            assert lookups != null;
+            this.lookups = lookups;
+            this.inputSequenceLength = inputSequenceLength;
+        }
+
+        /** @return the lookups */
+        public RuleLookup[] getLookups() {
+            return lookups;
+        }
+
+        /** @return the input sequence length */
+        public int getInputSequenceLength() {
+            return inputSequenceLength;
+        }
+
+        /**
+         * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves.
+         * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
+         */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            if ( lookups != null ) {
+                for ( int i = 0, n = lookups.length; i < n; i++ ) {
+                    RuleLookup l = lookups [ i ];
+                    if ( l != null ) {
+                        l.resolveLookupReferences ( lookupTables );
+                    }
+                }
+            }
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            return "{ lookups = " + Arrays.toString ( lookups ) + ", inputSequenceLength = " + inputSequenceLength + " }";
+        }
+
+    }
+
+    /**
+     * The <code>GlyphSequenceRule</code> class implements a subclass of <code>Rule</code>
+     * that supports matching on a specific glyph sequence.
+     */
+    public static class GlyphSequenceRule extends Rule {
+
+        private final int[] glyphs;                             // glyphs
+
+        /**
+         * Instantiate a GlyphSequenceRule.
+         * @param lookups the rule's lookups
+         * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
+         * @param glyphs the rule's glyph sequence to match, starting with second glyph in sequence
+         */
+        public GlyphSequenceRule ( RuleLookup[] lookups, int inputSequenceLength, int[] glyphs ) {
+            super ( lookups, inputSequenceLength );
+            assert glyphs != null;
+            this.glyphs = glyphs;
+        }
+
+        /**
+         * Obtain glyphs. N.B. that this array starts with the second
+         * glyph of the input sequence.
+         * @return the glyphs
+         */
+        public int[] getGlyphs() {
+            return glyphs;
+        }
+
+        /**
+         * Obtain glyphs augmented by specified first glyph entry.
+         * @param firstGlyph to fill in first glyph entry
+         * @return the glyphs augmented by first glyph
+         */
+        public int[] getGlyphs ( int firstGlyph ) {
+            int[] ga = new int [ glyphs.length + 1 ];
+            ga [ 0 ] = firstGlyph;
+            System.arraycopy ( glyphs, 0, ga, 1, glyphs.length );
+            return ga;
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            sb.append ( "{ " );
+            sb.append ( "lookups = " + Arrays.toString ( getLookups() ) );
+            sb.append ( ", glyphs = " + Arrays.toString ( glyphs ) );
+            sb.append ( " }" );
+            return sb.toString();
+        }
+
+    }
+
+    /**
+     * The <code>ClassSequenceRule</code> class implements a subclass of <code>Rule</code>
+     * that supports matching on a specific glyph class sequence.
+     */
+    public static class ClassSequenceRule extends Rule {
+
+        private final int[] classes;                            // glyph classes
+
+        /**
+         * Instantiate a ClassSequenceRule.
+         * @param lookups the rule's lookups
+         * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
+         * @param classes the rule's glyph class sequence to match, starting with second glyph in sequence
+         */
+        public ClassSequenceRule ( RuleLookup[] lookups, int inputSequenceLength, int[] classes ) {
+            super ( lookups, inputSequenceLength );
+            assert classes != null;
+            this.classes = classes;
+        }
+
+        /**
+         * Obtain glyph classes. N.B. that this array starts with the class of the second
+         * glyph of the input sequence.
+         * @return the classes
+         */
+        public int[] getClasses() {
+            return classes;
+        }
+
+        /**
+         * Obtain glyph classes augmented by specified first class entry.
+         * @param firstClass to fill in first class entry
+         * @return the classes augmented by first class
+         */
+        public int[] getClasses ( int firstClass ) {
+            int[] ca = new int [ classes.length + 1 ];
+            ca [ 0 ] = firstClass;
+            System.arraycopy ( classes, 0, ca, 1, classes.length );
+            return ca;
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            sb.append ( "{ " );
+            sb.append ( "lookups = " + Arrays.toString ( getLookups() ) );
+            sb.append ( ", classes = " + Arrays.toString( classes ) );
+            sb.append ( " }" );
+            return sb.toString();
+        }
+
+    }
+
+    /**
+     * The <code>CoverageSequenceRule</code> class implements a subclass of <code>Rule</code>
+     * that supports matching on a specific glyph coverage sequence.
+     */
+    public static class CoverageSequenceRule extends Rule {
+
+        private final GlyphCoverageTable[] coverages;           // glyph coverages
+
+        /**
+         * Instantiate a ClassSequenceRule.
+         * @param lookups the rule's lookups
+         * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
+         * @param coverages the rule's glyph coverage sequence to match, starting with first glyph in sequence
+         */
+        public CoverageSequenceRule ( RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages ) {
+            super ( lookups, inputSequenceLength );
+            assert coverages != null;
+            this.coverages = coverages;
+        }
+
+        /** @return the coverages */
+        public GlyphCoverageTable[] getCoverages() {
+            return coverages;
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            sb.append ( "{ " );
+            sb.append ( "lookups = " + Arrays.toString ( getLookups() ) );
+            sb.append ( ", coverages = " + Arrays.toString( coverages ) );
+            sb.append ( " }" );
+            return sb.toString();
+        }
+
+    }
+
+    /**
+     * The <code>ChainedGlyphSequenceRule</code> class implements a subclass of <code>GlyphSequenceRule</code>
+     * that supports matching on a specific glyph sequence in a specific chained contextual.
+     */
+    public static class ChainedGlyphSequenceRule extends GlyphSequenceRule {
+
+        private final int[] backtrackGlyphs;                    // backtrack glyphs
+        private final int[] lookaheadGlyphs;                    // lookahead glyphs
+
+        /**
+         * Instantiate a ChainedGlyphSequenceRule.
+         * @param lookups the rule's lookups
+         * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
+         * @param glyphs the rule's input glyph sequence to match, starting with second glyph in sequence
+         * @param backtrackGlyphs the rule's backtrack glyph sequence to match, starting with first glyph in sequence
+         * @param lookaheadGlyphs the rule's lookahead glyph sequence to match, starting with first glyph in sequence
+         */
+        public ChainedGlyphSequenceRule ( RuleLookup[] lookups, int inputSequenceLength, int[] glyphs, int[] backtrackGlyphs, int[] lookaheadGlyphs ) {
+            super ( lookups, inputSequenceLength, glyphs );
+            assert backtrackGlyphs != null;
+            assert lookaheadGlyphs != null;
+            this.backtrackGlyphs = backtrackGlyphs;
+            this.lookaheadGlyphs = lookaheadGlyphs;
+        }
+
+        /** @return the backtrack glyphs */
+        public int[] getBacktrackGlyphs() {
+            return backtrackGlyphs;
+        }
+
+        /** @return the lookahead glyphs */
+        public int[] getLookaheadGlyphs() {
+            return lookaheadGlyphs;
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            sb.append ( "{ " );
+            sb.append ( "lookups = " + Arrays.toString ( getLookups() ) );
+            sb.append ( ", glyphs = " + Arrays.toString ( getGlyphs() ) );
+            sb.append ( ", backtrackGlyphs = " + Arrays.toString ( backtrackGlyphs ) );
+            sb.append ( ", lookaheadGlyphs = " + Arrays.toString ( lookaheadGlyphs ) );
+            sb.append ( " }" );
+            return sb.toString();
+        }
+
+    }
+
+    /**
+     * The <code>ChainedClassSequenceRule</code> class implements a subclass of <code>ClassSequenceRule</code>
+     * that supports matching on a specific glyph class sequence in a specific chained contextual.
+     */
+    public static class ChainedClassSequenceRule extends ClassSequenceRule {
+
+        private final int[] backtrackClasses;                    // backtrack classes
+        private final int[] lookaheadClasses;                    // lookahead classes
+
+        /**
+         * Instantiate a ChainedClassSequenceRule.
+         * @param lookups the rule's lookups
+         * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
+         * @param classes the rule's input glyph class sequence to match, starting with second glyph in sequence
+         * @param backtrackClasses the rule's backtrack glyph class sequence to match, starting with first glyph in sequence
+         * @param lookaheadClasses the rule's lookahead glyph class sequence to match, starting with first glyph in sequence
+         */
+        public ChainedClassSequenceRule ( RuleLookup[] lookups, int inputSequenceLength, int[] classes, int[] backtrackClasses, int[] lookaheadClasses ) {
+            super ( lookups, inputSequenceLength, classes );
+            assert backtrackClasses != null;
+            assert lookaheadClasses != null;
+            this.backtrackClasses = backtrackClasses;
+            this.lookaheadClasses = lookaheadClasses;
+        }
+
+        /** @return the backtrack classes */
+        public int[] getBacktrackClasses() {
+            return backtrackClasses;
+        }
+
+        /** @return the lookahead classes */
+        public int[] getLookaheadClasses() {
+            return lookaheadClasses;
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            sb.append ( "{ " );
+            sb.append ( "lookups = " + Arrays.toString ( getLookups() ) );
+            sb.append ( ", classes = " + Arrays.toString ( getClasses() ) );
+            sb.append ( ", backtrackClasses = " + Arrays.toString ( backtrackClasses ) );
+            sb.append ( ", lookaheadClasses = " + Arrays.toString ( lookaheadClasses ) );
+            sb.append ( " }" );
+            return sb.toString();
+        }
+
+    }
+
+    /**
+     * The <code>ChainedCoverageSequenceRule</code> class implements a subclass of <code>CoverageSequenceRule</code>
+     * that supports matching on a specific glyph class sequence in a specific chained contextual.
+     */
+    public static class ChainedCoverageSequenceRule extends CoverageSequenceRule {
+
+        private final GlyphCoverageTable[] backtrackCoverages;  // backtrack coverages
+        private final GlyphCoverageTable[] lookaheadCoverages;  // lookahead coverages
+
+        /**
+         * Instantiate a ChainedCoverageSequenceRule.
+         * @param lookups the rule's lookups
+         * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed)
+         * @param coverages the rule's input glyph class sequence to match, starting with first glyph in sequence
+         * @param backtrackCoverages the rule's backtrack glyph class sequence to match, starting with first glyph in sequence
+         * @param lookaheadCoverages the rule's lookahead glyph class sequence to match, starting with first glyph in sequence
+         */
+        public ChainedCoverageSequenceRule ( RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages, GlyphCoverageTable[] backtrackCoverages, GlyphCoverageTable[] lookaheadCoverages ) {
+            super ( lookups, inputSequenceLength, coverages );
+            assert backtrackCoverages != null;
+            assert lookaheadCoverages != null;
+            this.backtrackCoverages = backtrackCoverages;
+            this.lookaheadCoverages = lookaheadCoverages;
+        }
+
+        /** @return the backtrack coverages */
+        public GlyphCoverageTable[] getBacktrackCoverages() {
+            return backtrackCoverages;
+        }
+
+        /** @return the lookahead coverages */
+        public GlyphCoverageTable[] getLookaheadCoverages() {
+            return lookaheadCoverages;
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            sb.append ( "{ " );
+            sb.append ( "lookups = " + Arrays.toString ( getLookups() ) );
+            sb.append ( ", coverages = " + Arrays.toString ( getCoverages() ) );
+            sb.append ( ", backtrackCoverages = " + Arrays.toString ( backtrackCoverages ) );
+            sb.append ( ", lookaheadCoverages = " + Arrays.toString ( lookaheadCoverages ) );
+            sb.append ( " }" );
+            return sb.toString();
+        }
+
+    }
+
+    /**
+     * The <code>RuleSet</code> class implements a collection of rules, which
+     * may or may not be the same rule type.
+     */
+    public static class RuleSet {
+
+        private final Rule[] rules;                             // set of rules
+
+        /**
+         * Instantiate a Rule Set.
+         * @param rules the rules
+         * @throws IllegalArgumentException if rules or some element of rules is null
+         */
+        public RuleSet ( Rule[] rules ) throws IllegalArgumentException {
+            // enforce rules array instance
+            if ( rules == null ) {
+                throw new IllegalArgumentException ( "rules[] is null" );
+            }
+            this.rules = rules;
+        }
+
+        /** @return the rules */
+        public Rule[] getRules() {
+            return rules;
+        }
+
+        /**
+         * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves.
+         * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables
+         */
+        public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) {
+            if ( rules != null ) {
+                for ( int i = 0, n = rules.length; i < n; i++ ) {
+                    Rule r = rules [ i ];
+                    if ( r != null ) {
+                        r.resolveLookupReferences ( lookupTables );
+                    }
+                }
+            }
+        }
+
+        /** {@inheritDoc} */
+        public String toString() {
+            return "{ rules = " + Arrays.toString ( rules ) + " }";
+        }
+
+    }
+
+    /**
+     * The <code>HomogenousRuleSet</code> class implements a collection of rules, which
+     * must be the same rule type (i.e., same concrete rule class) or null.
+     */
+    public static class HomogeneousRuleSet extends RuleSet {
+
+        /**
+         * Instantiate a Homogeneous Rule Set.
+         * @param rules the rules
+         * @throws IllegalArgumentException if some rule[i] is not an instance of rule[0]
+         */
+        public HomogeneousRuleSet ( Rule[] rules ) throws IllegalArgumentException {
+            super ( rules );
+            // find first non-null rule
+            Rule r0 = null;
+            for ( int i = 1, n = rules.length; ( r0 == null ) && ( i < n ); i++ ) {
+                if ( rules[i] != null ) {
+                    r0 = rules[i];
+                }
+            }
+            // enforce rule instance homogeneity
+            if ( r0 != null ) {
+                Class c = r0.getClass();
+                for ( int i = 1, n = rules.length; i < n; i++ ) {
+                    Rule r = rules[i];
+                    if ( ( r != null ) && ! c.isInstance ( r ) ) {
+                        throw new IllegalArgumentException ( "rules[" + i + "] is not an instance of " + c.getName() );
+                    }
+                }
+            }
+
+        }
+
+    }
+
 }
diff --git a/src/java/org/apache/fop/fonts/GlyphTester.java b/src/java/org/apache/fop/fonts/GlyphTester.java
new file mode 100644 (file)
index 0000000..92d3dd9
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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.fonts;
+
+/**
+ * Interface for testing glyph properties according to glyph identifier.
+ * @author Glenn Adams
+ */
+public interface GlyphTester {
+
+    /**
+     * Perform a test on a glyph identifier.
+     * @param gi glyph identififer
+     * @return true if test is satisfied
+     */
+    boolean test ( int gi );
+
+}
diff --git a/src/java/org/apache/fop/fonts/GlyphUtils.java b/src/java/org/apache/fop/fonts/GlyphUtils.java
deleted file mode 100644 (file)
index e4e22d6..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.fonts;
-
-/**
- * A utility class for glyphs and glyph sequences.
- * @author Glenn Adams
- */
-public final class GlyphUtils {
-
-    private GlyphUtils() {
-    }
-
-    /**
-     * Map a glyph (or character) code sequence to  a string, used only
-     * for debugging and logging purposes.
-     * @param cs character (glyph) id sequence
-     * @return a string representation of code sequence
-     */
-    public static String toString ( CharSequence cs ) {
-        StringBuffer sb = new StringBuffer();
-        sb.append ( '[' );
-        for ( int i = 0, n = cs.length(); i < n; i++ ) {
-            int c = cs.charAt ( i );
-            if ( i > 0 ) {
-                sb.append ( ',' );
-            }
-            sb.append ( Integer.toString ( c ) );
-        }
-        sb.append ( ']' );
-        return sb.toString();
-    }
-
-}
index e58f919b0daa9fa50a489cace0a9086879f66747..1d1a8526d02b2be171ed1c8185df752da8a07994 100644 (file)
@@ -413,7 +413,7 @@ public class LazyFont extends Typeface implements FontDescriptor, Substitutable,
      */
     public boolean performsPositioning() {
         load(true);
-        if ( realFontDescriptor instanceof Substitutable ) {
+        if ( realFontDescriptor instanceof Positionable ) {
             return ((Positionable)realFontDescriptor).performsPositioning();
         } else {
             return false;
@@ -423,10 +423,26 @@ public class LazyFont extends Typeface implements FontDescriptor, Substitutable,
     /**
      * {@inheritDoc}
      */
-    public int[] performPositioning ( CharSequence cs, String script, String language ) {
+    public int[][]
+        performPositioning ( CharSequence cs, String script, String language, int fontSize ) {
         load(true);
-        if ( realFontDescriptor instanceof Substitutable ) {
-            return ((Positionable)realFontDescriptor).performPositioning(cs, script, language);
+        if ( realFontDescriptor instanceof Positionable ) {
+            return ((Positionable)realFontDescriptor)
+                .performPositioning(cs, script, language, fontSize);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public int[][]
+        performPositioning ( CharSequence cs, String script, String language ) {
+        load(true);
+        if ( realFontDescriptor instanceof Positionable ) {
+            return ((Positionable)realFontDescriptor)
+                .performPositioning(cs, script, language);
         } else {
             return null;
         }
index 0f2488dc40a1ad95a51e8fcc38b2a55bf7265a86..ce05d6e536dff9bfa591d3163a0fe8910ee8dc63 100644 (file)
 
 package org.apache.fop.fonts;
 
-//Java
 import java.nio.CharBuffer;
+import java.nio.IntBuffer;
 import java.text.DecimalFormat;
 import java.util.Map;
 
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.apache.fop.util.CharUtilities;
 
 /**
  * Generic MultiByte (CID) font
  */
 public class MultiByteFont extends CIDFont implements Substitutable, Positionable {
 
+    /** logging instance */
+    private static final Log log // CSOK: ConstantNameCheck
+        = LogFactory.getLog(MultiByteFont.class);
+
     private static int uniqueCounter = -1;
 
     private String ttcName = null;
@@ -46,6 +54,7 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
     private BFEntry[] bfentries = null;
 
     /* advanced typographic support */
+    private GlyphDefinitionTable gdef;
     private GlyphSubstitutionTable gsub;
     private GlyphPositioningTable gpos;
 
@@ -278,6 +287,26 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
         return subset.getSubsetChars();
     }
 
+    /**
+     * Establishes the glyph definition table.
+     * @param gdef the glyph definition table to be used by this font
+     */
+    public void setGDEF ( GlyphDefinitionTable gdef ) {
+        if ( ( this.gdef == null ) || ( gdef == null ) ) {
+            this.gdef = gdef;
+        } else {
+            throw new IllegalStateException ( "font already associated with GDEF table" );
+        }
+    }
+
+    /**
+     * Obtain glyph definition table.
+     * @return glyph definition table or null if none is associated with font
+     */
+    public GlyphDefinitionTable getGDEF() {
+        return gdef;
+    }
+
     /**
      * Establishes the glyph substitution table.
      * @param gsub the glyph substitution table to be used by this font
@@ -341,9 +370,36 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
     }
 
     /** {@inheritDoc} */
-    public int[] performPositioning ( CharSequence cs, String script, String language ) {
+    public int[][]
+        performPositioning ( CharSequence cs, String script, String language, int fontSize ) {
         if ( gpos != null ) {
-            return gpos.position ( mapCharsToGlyphs ( cs ), script, language );
+            GlyphSequence gs = mapCharsToGlyphs ( cs );
+            int[][] adjustments = new int [ gs.getGlyphCount() ] [ 4 ];
+            if ( gpos.position ( gs, script, language, fontSize, this.width, adjustments ) ) {
+                return scaleAdjustments ( adjustments, fontSize );
+            } else {
+                return null;
+            }
+        } else {
+            return null;
+        }
+    }
+
+    /** {@inheritDoc} */
+    public int[][] performPositioning ( CharSequence cs, String script, String language ) {
+        throw new UnsupportedOperationException();
+    }
+
+
+    private int[][] scaleAdjustments ( int[][] adjustments, int fontSize ) {
+        if ( adjustments != null ) {
+            for ( int i = 0, n = adjustments.length; i < n; i++ ) {
+                int[] gpa = adjustments [ i ];
+                for ( int k = 0; k < 4; k++ ) {
+                    gpa [ k ] = ( gpa [ k ] * fontSize ) / 1000;
+                }
+            }
+            return adjustments;
         } else {
             return null;
         }
@@ -357,7 +413,8 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
      * @returns a CharSequence containing glyph indices
      */
     private GlyphSequence mapCharsToGlyphs ( CharSequence cs ) {
-        CharBuffer cb = CharBuffer.allocate ( cs.length() );
+        IntBuffer cb = IntBuffer.allocate ( cs.length() );
+        IntBuffer gb = IntBuffer.allocate ( cs.length() );
         int gi, giMissing = findGlyphIndex ( Typeface.NOT_FOUND );
         for ( int i = 0, n = cs.length(); i < n; i++ ) {
             int cc = cs.charAt ( i );
@@ -386,29 +443,33 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
             if ( gi == SingleByteEncoding.NOT_FOUND_CODE_POINT ) {
                 gi = giMissing;
             }
-            cb.put ( (char) gi );
+            cb.put ( cc );
+            gb.put ( gi );
         }
-        cb.rewind();
-        return new GlyphSequence ( cs, (CharSequence) cb, null );
+        cb.flip();
+        gb.flip();
+        return new GlyphSequence ( cb, gb, null );
     }
 
     /**
      * Map sequence GS, comprising a sequence of Glyph Indices, to output sequence CS,
      * comprising a sequence of UTF-16 encoded Unicode Code Points.
-     * @param gs a CharSequence containing glyph indices
+     * @param gs a GlyphSequence containing glyph indices
      * @returns a CharSequence containing UTF-16 encoded Unicode characters
      */
     private CharSequence mapGlyphsToChars ( GlyphSequence gs ) {
-        CharBuffer cb = CharBuffer.allocate ( gs.length() );
-        int cc, ccMissing = Typeface.NOT_FOUND;
-        for ( int i = 0, n = gs.length(); i < n; i++ ) {
-            int gi = gs.charAt ( i );
-            cc = findCharacterFromGlyphIndex ( gi );
-            if ( cc == 0 ) {
-                cc = ccMissing;
-            }
-            if ( cc > 0x10FFFF ) {
+        int ng = gs.getGlyphCount();
+        CharBuffer cb = CharBuffer.allocate ( ng );
+        int ccMissing = Typeface.NOT_FOUND;
+        for ( int i = 0, n = ng; i < n; i++ ) {
+            int gi = gs.getGlyph ( i );
+            int cc = findCharacterFromGlyphIndex ( gi );
+            if ( ( cc == 0 ) || ( cc > 0x10FFFF ) ) {
                 cc = ccMissing;
+                log.warn("Unable to map glyph index " + gi
+                         + " to Unicode scalar in font '"
+                         + getFullName() + "', substituting missing character '"
+                         + (char) cc + "'");
             }
             if ( cc > 0x00FFFF ) {
                 int sh, sl;
@@ -421,7 +482,7 @@ public class MultiByteFont extends CIDFont implements Substitutable, Positionabl
                 cb.put ( (char) cc );
             }
         }
-        cb.rewind();
+        cb.flip();
         return (CharSequence) cb;
     }
 
index 7d76c15847759d9866b51f4b74b6a299f3eac371..6ce30f231119f4f39ad61a5887bd3855213e8e3f 100644 (file)
@@ -39,8 +39,20 @@ public interface Positionable {
      * @param cs character sequence to map to position offsets (advancement adjustments)
      * @param script a script identifier
      * @param language a language identifier
-     * @return array (sequence) of pairs of position offsets, one pair for each element of character sequence
+     * @param fontSize font size
+     * @return array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
+     * with one 4-tuple for each element of glyph sequence, or null if no non-zero adjustment applies
      */
-    int[] performPositioning ( CharSequence cs, String script, String language );
+    int[][] performPositioning ( CharSequence cs, String script, String language, int fontSize );
+
+    /**
+     * Perform glyph positioning using an implied font size.
+     * @param cs character sequence to map to position offsets (advancement adjustments)
+     * @param script a script identifier
+     * @param language a language identifier
+     * @return array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
+     * with one 4-tuple for each element of glyph sequence, or null if no non-zero adjustment applies
+     */
+    int[][] performPositioning ( CharSequence cs, String script, String language );
 
 }
diff --git a/src/java/org/apache/fop/fonts/ScriptContextTester.java b/src/java/org/apache/fop/fonts/ScriptContextTester.java
new file mode 100644 (file)
index 0000000..5f64c50
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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.fonts;
+
+/**
+ * Interface for providing script specific context testers.
+ * @author Glenn Adams
+ */
+public interface ScriptContextTester {
+
+    /**
+     * Obtain a glyph context tester for the specified feature.
+     * @param feature a feature identifier
+     * @return a glyph context tester or null if none available for the specified feature
+     */
+    GlyphContextTester getTester ( String feature );
+
+}
index c5978f1c18f758f3453923fea9a07bb3a17dcca1..99d2823b8d28e7981edb348a0b9f3f7f5dbc305d 100644 (file)
 
 package org.apache.fop.fonts;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 // CSOFF: InnerAssignmentCheck
 // CSOFF: LineLengthCheck
+// CSOFF: ParameterNumberCheck
 
 /**
  * Abstract script processor base class for which an implementation of the substitution and positioning methods
@@ -49,29 +52,118 @@ public abstract class ScriptProcessor {
     }
 
     /** @return script identifier */
-    public String getScript() {
+    public final String getScript() {
         return script;
     }
 
+    /**
+     * Obtain script specific substitution features.
+     * @return array of suppported substitution features or null
+     */
+    public abstract String[] getSubstitutionFeatures();
+
+    /**
+     * Obtain script specific substitution context tester.
+     * @return substitution context tester or null
+     */
+    public abstract ScriptContextTester getSubstitutionContextTester();
+
     /**
      * Perform substitution processing using a specific set of lookup tables.
+     * @param gsub the glyph substitution table that applies
      * @param gs an input glyph sequence
      * @param script a script identifier
      * @param language a language identifier
      * @param lookups a mapping from lookup specifications to glyph subtables to use for substitution processing
      * @return the substituted (output) glyph sequence
      */
-    public abstract GlyphSequence substitute ( GlyphSequence gs, String script, String language, Map/*<LookupSpec,GlyphSubtable[]>*/ lookups );
+    public final GlyphSequence substitute ( GlyphSubstitutionTable gsub, GlyphSequence gs, String script, String language, Map/*<LookupSpec,List<LookupTable>>>*/ lookups ) {
+        return substitute ( gs, script, language, assembleLookups ( gsub, getSubstitutionFeatures(), lookups ), getSubstitutionContextTester() );
+    }
+
+    /**
+     * Perform substitution processing using a specific set of ordered glyph table use specifications.
+     * @param gs an input glyph sequence
+     * @param script a script identifier
+     * @param language a language identifier
+     * @param usa an ordered array of glyph table use specs
+     * @param sct a script specific context tester (or null)
+     * @return the substituted (output) glyph sequence
+     */
+    public final GlyphSequence substitute ( GlyphSequence gs, String script, String language, GlyphTable.UseSpec[] usa, ScriptContextTester sct ) {
+        assert usa != null;
+        for ( int i = 0, n = usa.length; i < n; i++ ) {
+            GlyphTable.UseSpec us = usa [ i ];
+            gs = us.substitute ( gs, script, language, sct );
+        }
+        return gs;
+    }
+
+    /**
+     * Obtain script specific positioning features.
+     * @return array of suppported positioning features or null
+     */
+    public abstract String[] getPositioningFeatures();
+
+    /**
+     * Obtain script specific positioning context tester.
+     * @return positioning context tester or null
+     */
+    public abstract ScriptContextTester getPositioningContextTester();
 
     /**
      * Perform positioning processing using a specific set of lookup tables.
+     * @param gpos the glyph positioning table that applies
      * @param gs an input glyph sequence
      * @param script a script identifier
      * @param language a language identifier
+     * @param fontSize size in device units
      * @param lookups a mapping from lookup specifications to glyph subtables to use for positioning processing
-     * @return the substituted (output) glyph sequence
+     * @param widths array of default advancements for each glyph
+     * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
+     * with one 4-tuple for each element of glyph sequence
+     * @return true if some adjustment is not zero; otherwise, false
+     */
+    public final boolean position ( GlyphPositioningTable gpos, GlyphSequence gs, String script, String language, int fontSize, Map/*<LookupSpec,List<LookupTable>>*/ lookups, int[] widths, int[][] adjustments ) {
+        return position ( gs, script, language, fontSize, assembleLookups ( gpos, getPositioningFeatures(), lookups ), widths, adjustments, getPositioningContextTester() );
+    }
+
+    /**
+     * Perform positioning processing using a specific set of ordered glyph table use specifications.
+     * @param gs an input glyph sequence
+     * @param script a script identifier
+     * @param language a language identifier
+     * @param fontSize size in device units
+     * @param usa an ordered array of glyph table use specs
+     * @param widths array of default advancements for each glyph in font
+     * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
+     * with one 4-tuple for each element of glyph sequence
+     * @param sct a script specific context tester (or null)
+     * @return true if some adjustment is not zero; otherwise, false
      */
-    public abstract int[] position ( GlyphSequence gs, String script, String language, Map/*<LookupSpec,GlyphSubtable[]>*/ lookups );
+    public final boolean position ( GlyphSequence gs, String script, String language, int fontSize, GlyphTable.UseSpec[] usa, int[] widths, int[][] adjustments, ScriptContextTester sct ) {
+        assert usa != null;
+        boolean adjusted = false;
+        for ( int i = 0, n = usa.length; i < n; i++ ) {
+            GlyphTable.UseSpec us = usa [ i ];
+            if ( us.position ( gs, script, language, fontSize, widths, adjustments, sct ) ) {
+                adjusted = true;
+            }
+        }
+        return adjusted;
+    }
+
+    /**
+     * Assemble ordered array of lookup table use specifications according to the specified features and candidate lookups,
+     * where the order of the array is in accordance to the order of the applicable lookup list.
+     * @param table the governing glyph table
+     * @param features array of feature identifiers to apply
+     * @param lookups a mapping from lookup specifications to lists of look tables from which to select lookup tables according to the specified features
+     * @return ordered array of assembled lookup table use specifications
+     */
+    public final GlyphTable.UseSpec[] assembleLookups ( GlyphTable table, String[] features, Map/*<LookupSpec,List<LookupTable>>*/ lookups ) {
+        return table.assembleLookups ( features, lookups );
+    }
 
     /**
      * Obtain script processor instance associated with specified script.
index 1c973cd51c738f452816531c89f615ef3ac35120..89c7890afbaa7488e7b238245573897c8a2e07af 100644 (file)
@@ -67,7 +67,7 @@ public abstract class AbstractFontReader {
         List arguments = new java.util.ArrayList();
         for (int i = 0; i < args.length; i++) {
             if (args[i].startsWith("-")) {
-                if ("-d".equals(args[i]) || "-q".equals(args[i])) {
+                if ("-t".equals(args[i]) || "-d".equals(args[i]) || "-q".equals(args[i])) {
                     options.put(args[i], "");
                 } else if ((i + 1) < args.length && !args[i + 1].startsWith("-")) {
                     options.put(args[i], args[i + 1]);
@@ -101,7 +101,9 @@ public abstract class AbstractFontReader {
      */
     protected static void determineLogLevel(Map options) {
         //Determine log level
-        if (options.get("-d") != null) {
+        if (options.get("-t") != null) {
+            setLogLevel("trace");
+        } else if (options.get("-d") != null) {
             setLogLevel("debug");
         } else if (options.get("-q") != null) {
             setLogLevel("error");
index 7ff435392efa5a2edfcf1282ea59710a85a66c4e..efd7599564fe648936de9e1c6b334c03a6e3fd19 100644 (file)
@@ -30,11 +30,6 @@ import javax.xml.parsers.DocumentBuilderFactory;
 import org.apache.commons.logging.LogFactory;
 import org.apache.fop.Version;
 import org.apache.fop.fonts.FontUtil;
-import org.apache.fop.fonts.GlyphCoverageTable;
-import org.apache.fop.fonts.GlyphPositioningTable;
-import org.apache.fop.fonts.GlyphSubstitutionTable;
-import org.apache.fop.fonts.GlyphSubtable;
-import org.apache.fop.fonts.GlyphTable;
 import org.apache.fop.fonts.truetype.FontFileReader;
 import org.apache.fop.fonts.truetype.TTFCmapEntry;
 import org.apache.fop.fonts.truetype.TTFFile;
@@ -70,6 +65,7 @@ public class TTFReader extends AbstractFontReader {
                 "java " + TTFReader.class.getName() + " [options] fontfile.ttf xmlfile.xml");
         System.out.println();
         System.out.println("where options can be:");
+        System.out.println("-t  Trace mode");
         System.out.println("-d  Debug mode");
         System.out.println("-q  Quiet mode");
         System.out.println("-enc ansi");
@@ -359,8 +355,6 @@ public class TTFReader extends AbstractFontReader {
 
         generateDOM4Kerning(root, ttf, isCid);
 
-        generateDOM4ScriptExtensions(root, ttf, isCid);
-
         return doc;
     }
 
@@ -474,288 +468,6 @@ public class TTFReader extends AbstractFontReader {
         }
     }
 
-    private void generateDOM4LookupReferences ( Element parent, GlyphSubstitutionTable gsub, GlyphTable.LookupSpec[] lookups ) {
-        boolean usedLookup = false;
-        Document d = parent.getOwnerDocument();
-        for ( int i = 0, m = lookups.length; i < m; i++ ) {
-            GlyphTable.LookupSpec ls = lookups [ i ];
-            GlyphSubtable[] sta = gsub.findSubtables ( ls );
-            for ( int j = 0, n = sta.length; j < n; j++ ) {
-                GlyphSubtable st = sta [ j ];
-                Element e = d.createElement("use-lookup");
-                e.setAttribute ( "ref", st.getID() );
-                parent.appendChild(e);
-            }
-        }
-    }
-
-    private void generateDOM4Features ( Element parent, GlyphSubstitutionTable gsub, GlyphTable.LookupSpec[] lookups, String scriptTag, String languageTag ) {
-        Document d = parent.getOwnerDocument();
-        Set features = new java.util.LinkedHashSet();
-        for ( int i = 0, n = lookups.length; i < n; i++ ) {
-            GlyphTable.LookupSpec ls = lookups [ i ];
-            features.add ( ls.getFeature() );
-        }
-        for ( Iterator it = features.iterator(); it.hasNext();) {
-            String featureTag = (String) it.next();
-            Element e = d.createElement("feature");
-            e.setAttribute ( "tag", featureTag );
-            generateDOM4LookupReferences ( e, gsub, gsub.matchLookupSpecs ( scriptTag, languageTag, featureTag ) );
-            if ( e.hasChildNodes() ) {
-                parent.appendChild(e);
-            }
-        }
-    }
-
-    private void generateDOM4Languages ( Element parent, GlyphSubstitutionTable gsub, GlyphTable.LookupSpec[] lookups, String scriptTag ) {
-        Document d = parent.getOwnerDocument();
-        Set languages = new java.util.LinkedHashSet();
-        for ( int i = 0, n = lookups.length; i < n; i++ ) {
-            GlyphTable.LookupSpec ls = lookups [ i ];
-            languages.add ( ls.getLanguage() );
-        }
-        for ( Iterator it = languages.iterator(); it.hasNext();) {
-            String languageTag = (String) it.next();
-            Element e = d.createElement("lang");
-            e.setAttribute ( "tag", languageTag );
-            generateDOM4Features ( e, gsub, gsub.matchLookupSpecs ( scriptTag, languageTag, "*" ), scriptTag, languageTag );
-            parent.appendChild(e);
-        }
-    }
-
-    private void generateDOM4Scripts ( Element parent, GlyphSubstitutionTable gsub, GlyphTable.LookupSpec[] lookups ) {
-        Document d = parent.getOwnerDocument();
-        Set scripts = new java.util.LinkedHashSet();
-        for ( int i = 0, n = lookups.length; i < n; i++ ) {
-            GlyphTable.LookupSpec ls = lookups [ i ];
-            scripts.add ( ls.getScript() );
-        }
-        for ( Iterator it = scripts.iterator(); it.hasNext();) {
-            String scriptTag = (String) it.next();
-            Element e = d.createElement("script");
-            e.setAttribute ( "tag", scriptTag );
-            generateDOM4Languages ( e, gsub, gsub.matchLookupSpecs ( scriptTag, "*", "*" ), scriptTag );
-            parent.appendChild(e);
-        }
-    }
-
-    private void generateDOM4Coverage ( Element parent, GlyphCoverageTable coverage ) {
-        Document d = parent.getOwnerDocument();
-        Element e = d.createElement("coverage");
-        int type = coverage.getType();
-        e.setAttribute ( "format", Integer.toString ( type ) );
-        List entries = coverage.getEntries();
-        if ( type == GlyphCoverageTable.GLYPH_COVERAGE_TYPE_MAPPED ) {
-            for ( Iterator it = entries.iterator(); it.hasNext();) {
-                Integer gid = (Integer) it.next();
-                if ( gid != null ) {
-                    Element g = d.createElement("gid");
-                    g.appendChild(d.createTextNode(gid.toString()));
-                    e.appendChild(g);
-                }
-            }
-        } else if ( type == GlyphCoverageTable.GLYPH_COVERAGE_TYPE_RANGE ) {
-            for ( Iterator it = entries.iterator(); it.hasNext();) {
-                GlyphCoverageTable.CoverageRange cr = (GlyphCoverageTable.CoverageRange) it.next();
-                if ( cr != null ) {
-                    Element r = d.createElement("range");
-                    r.setAttribute ( "gs", Integer.toString(cr.getStart()) );
-                    r.setAttribute ( "ge", Integer.toString(cr.getEnd()) );
-                    r.setAttribute ( "ci", Integer.toString(cr.getIndex()) );
-                    e.appendChild(r);
-                }
-            }
-        }
-        parent.appendChild(e);
-    }
-
-    private void generateDOM4GSUBSingleEntries ( Element parent, List entries, int type, int format ) {
-        if ( entries.size() > 0 ) {
-            Document d = parent.getOwnerDocument();
-            for ( Iterator it = entries.iterator(); it.hasNext();) {
-                Integer gid = (Integer) it.next();
-                if ( gid != null ) {
-                    Element e = d.createElement("gid");
-                    e.appendChild(d.createTextNode(gid.toString()));
-                    parent.appendChild(e);
-                }
-            }
-        }
-    }
-
-    private void generateDOM4GSUBMultipleEntries ( Element parent, List entries, int type, int format ) {
-        // [TBD] - implement me
-    }
-
-    private void generateDOM4GSUBAlternateEntries ( Element parent, List entries, int type, int format ) {
-        // [TBD] - implement me
-    }
-
-    private void generateDOM4LigatureSet ( Element parent, GlyphSubstitutionTable.LigatureSet lset ) {
-        Document d = parent.getOwnerDocument();
-        Element e = d.createElement("ligs");
-        GlyphSubstitutionTable.Ligature[] la = lset.getLigatures();
-        for ( int i = 0, m = la.length; i < m; i++ ) {
-            GlyphSubstitutionTable.Ligature l = la [ i ];
-            Element le = d.createElement("lig");
-            le.setAttribute ( "gid", Integer.toString( l.getLigature() ) );
-            int[] ca = l.getComponents();
-            StringBuffer sb = new StringBuffer();
-            for ( int j = 0, n = ca.length; j < n; j++ ) {
-                if ( j > 0 ) {
-                    sb.append ( ' ' );
-                }
-                sb.append ( Integer.toString ( ca [ j ] ) );
-            }
-            if ( sb.length() > 0 ) {
-                le.appendChild ( d.createTextNode ( sb.toString() ) );
-                e.appendChild ( le );
-            }
-        }
-        parent.appendChild(e);
-    }
-
-    private void generateDOM4GSUBLigatureEntries ( Element parent, List entries, int type, int format ) {
-        if ( entries.size() > 0 ) {
-            for ( Iterator it = entries.iterator(); it.hasNext();) {
-                GlyphSubstitutionTable.LigatureSet lset = (GlyphSubstitutionTable.LigatureSet) it.next();
-                if ( lset != null ) {
-                    generateDOM4LigatureSet ( parent, lset );
-                }
-            }
-        }
-    }
-
-    private void generateDOM4GSUBContextEntries ( Element parent, List entries, int type, int format ) {
-        // [TBD] - implement me
-    }
-
-    private void generateDOM4GSUBChainingContextEntries ( Element parent, List entries, int type, int format ) {
-        // [TBD] - implement me
-    }
-
-    private void generateDOM4GSUBExtensionEntries ( Element parent, List entries, int type, int format ) {
-        // [TBD] - implement me
-    }
-
-    private void generateDOM4GSUBReverseChainingSingleEntries ( Element parent, List entries, int type, int format ) {
-        // [TBD] - implement me
-    }
-
-    private void generateDOM4GSUBEntries ( Element parent, List entries, int type, int format ) {
-        Document d = parent.getOwnerDocument();
-        Element e = d.createElement("entries");
-        switch ( type ) {
-        case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_SINGLE:
-            generateDOM4GSUBSingleEntries ( e, entries, type, format );
-            break;
-        case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_MULTIPLE:
-            generateDOM4GSUBMultipleEntries ( e, entries, type, format );
-            break;
-        case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_ALTERNATE:
-            generateDOM4GSUBAlternateEntries ( e, entries, type, format );
-            break;
-        case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_LIGATURE:
-            generateDOM4GSUBLigatureEntries ( e, entries, type, format );
-            break;
-        case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CONTEXT:
-            generateDOM4GSUBContextEntries ( e, entries, type, format );
-            break;
-        case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CHAINING_CONTEXT:
-            generateDOM4GSUBChainingContextEntries ( e, entries, type, format );
-            break;
-        case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION:
-            generateDOM4GSUBExtensionEntries ( e, entries, type, format );
-            break;
-        case GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_REVERSE_CHAINING_CONTEXT_SINGLE:
-            generateDOM4GSUBReverseChainingSingleEntries ( e, entries, type, format );
-            break;
-        default:
-            break;
-        }
-        parent.appendChild(e);
-    }
-
-    private void generateDOM4GSUBSubtable ( Element parent, GlyphSubstitutionTable gsub, GlyphSubtable st ) {
-        Document d = parent.getOwnerDocument();
-        Element e = d.createElement("lst");
-        e.setAttribute ( "format", Integer.toString ( st.getFormat() ) );
-        generateDOM4Coverage ( e, st.getCoverage() );
-        generateDOM4GSUBEntries ( e, st.getEntries(), st.getType(), st.getFormat() );
-        parent.appendChild(e);
-    }
-
-    private void generateDOM4GSUBSubtables ( Element parent, GlyphSubstitutionTable gsub, GlyphSubtable[] subtables ) {
-        if ( subtables.length > 0 ) {
-            Document d = parent.getOwnerDocument();
-            Element e = d.createElement("lookup");
-            GlyphSubtable st0 = subtables[0];
-            int st0Type = st0.getType();
-            e.setAttribute ( "id", st0.getID() );
-            e.setAttribute ( "type", st0.getTypeName() );
-            for ( int i = 0, n = subtables.length; i < n; i++ ) {
-                GlyphSubtable st = subtables[i];
-                if ( st.getType() == st0Type ) {
-                    generateDOM4GSUBSubtable ( e, gsub, st );
-                }
-            }
-            parent.appendChild(e);
-        }
-    }
-
-    private GlyphSubtable[] matchSubtables ( GlyphSubtable[] subtables, String id ) {
-        List matches = new java.util.ArrayList();
-        for ( int i = 0, n = subtables.length; i < n; i++ ) {
-            GlyphSubtable st =  subtables [ i ];
-            if ( st.getID().equals ( id ) ) {
-                matches.add ( st );
-            }
-        }
-        return (GlyphSubtable[]) matches.toArray ( new GlyphSubtable[matches.size()] );
-    }
-
-    private void generateDOM4GSUBLookups ( Element parent, GlyphSubstitutionTable gsub, GlyphSubtable[] subtables ) {
-        Set lus = new java.util.LinkedHashSet();
-        for ( int i = 0, n = subtables.length; i < n; i++ ) {
-            GlyphSubtable st =  subtables [ i ];
-            lus.add ( st.getID() );
-        }
-        for ( Iterator it = lus.iterator(); it.hasNext();) {
-            String id = (String) it.next();
-            generateDOM4GSUBSubtables ( parent, gsub, matchSubtables ( subtables, id ) );
-        }
-    }
-
-    private void generateDOM4GSUB ( Element parent, GlyphSubstitutionTable gsub ) {
-        Document d = parent.getOwnerDocument();
-        Element e = d.createElement("gsub");
-        parent.appendChild(e);
-        generateDOM4Scripts ( e, gsub, gsub.getLookups() );
-        generateDOM4GSUBLookups ( e, gsub, gsub.getSubtables() );
-    }
-
-    private void generateDOM4GPOS ( Element parent, GlyphPositioningTable gpos ) {
-        Document d = parent.getOwnerDocument();
-        Element te = d.createElement("gpos");
-        parent.appendChild(te);
-    }
-
-    private void generateDOM4ScriptExtensions(Element parent, TTFFile ttf, boolean isCid) {
-        if ( ttf.hasScriptExtension() ) {
-            Document d = parent.getOwnerDocument();
-            Element se = d.createElement("script-extras");
-            parent.appendChild(se);
-            GlyphSubstitutionTable st;
-            if ( ( st = ttf.getGSUB() ) != null ) {
-                generateDOM4GSUB ( se, st );
-            }
-            GlyphPositioningTable pt;
-            if ( ( pt = ttf.getGPOS() ) != null ) {
-                generateDOM4GPOS ( se, pt );
-            }
-        }
-    }
-
     /**
      * Bugzilla 40739, check that attr has a metrics-version attribute
      * compatible with ours.
index 641ad5a679f0501744707e4336b128bc5d2ddbee..01c2924e608aee16fa13b33df1a72bf391a03fc6 100644 (file)
@@ -20,6 +20,7 @@
 package org.apache.fop.fonts.truetype;
 
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Iterator;
 import java.util.List;
@@ -33,7 +34,11 @@ import org.apache.xmlgraphics.fonts.Glyphs;
 
 
 import org.apache.fop.fonts.FontUtil;
+import org.apache.fop.fonts.GlyphClassTable;
 import org.apache.fop.fonts.GlyphCoverageTable;
+import org.apache.fop.fonts.GlyphDefinitionSubtable;
+import org.apache.fop.fonts.GlyphDefinitionTable;
+import org.apache.fop.fonts.GlyphMappingTable;
 import org.apache.fop.fonts.GlyphPositioningSubtable;
 import org.apache.fop.fonts.GlyphPositioningTable;
 import org.apache.fop.fonts.GlyphSubstitutionSubtable;
@@ -41,6 +46,8 @@ import org.apache.fop.fonts.GlyphSubstitutionTable;
 import org.apache.fop.fonts.GlyphSubtable;
 import org.apache.fop.fonts.GlyphTable;
 
+import org.apache.fop.util.CharUtilities;
+
 // CSOFF: AvoidNestedBlocksCheck
 // CSOFF: NoWhitespaceAfterCheck
 // CSOFF: InnerAssignmentCheck
@@ -59,9 +66,6 @@ public class TTFFile {
     static final int MAX_CHAR_CODE = 255;
     static final int ENC_BUF_SIZE = 1024;
 
-    /** Set to true to get even more debug output than with level DEBUG */
-    public static final boolean TRACE_ENABLED = false;
-
     private String encoding = "WinAnsiEncoding";    // Default encoding
 
     private short firstChar = 0;
@@ -74,7 +78,7 @@ public class TTFFile {
     private Map kerningTab;                          // for CIDs
     private Map ansiKerningTab;                      // For winAnsiEncoding
     private List cmaps;
-    private List unicodeMapping;
+    private Set unicodeMappings;
 
     private int upem;                                // unitsPerEm from "head" table
     private int nhmtx;                               // Number of horizontal metrics
@@ -140,9 +144,10 @@ public class TTFFile {
     private Map/*<String,Object[3]>*/ seScripts;
     private Map/*<String,Object[2]>*/ seLanguages;
     private Map/*<String,List<String>>*/ seFeatures;
-    private List seCoverage;
+    private GlyphMappingTable seMapping;
     private List seEntries;
     private List seSubtables;
+    private GlyphDefinitionTable gdef;
     private GlyphSubstitutionTable gsub;
     private GlyphPositioningTable gpos;
 
@@ -154,7 +159,7 @@ public class TTFFile {
     /**
      * Key-value helper class
      */
-    class UnicodeMapping {
+    class UnicodeMapping implements Comparable {
 
         private int unicodeIndex;
         private int glyphIndex;
@@ -181,6 +186,46 @@ public class TTFFile {
         public int getUnicodeIndex() {
             return unicodeIndex;
         }
+
+
+        /** {@inheritDoc} */
+        public int hashCode() {
+            int hc = unicodeIndex;
+            hc = 19 * hc + ( hc ^ glyphIndex );
+            return hc;
+        }
+
+        /** {@inheritDoc} */
+        public boolean equals ( Object o ) {
+            if ( o instanceof UnicodeMapping ) {
+                UnicodeMapping m = (UnicodeMapping) o;
+                if ( unicodeIndex != m.unicodeIndex ) {
+                    return false;
+                } else if ( glyphIndex != m.glyphIndex ) {
+                    return false;
+                } else {
+                    return true;
+                }
+            } else {
+                return false;
+            }
+        }
+
+        /** {@inheritDoc} */
+        public int compareTo ( Object o ) {
+            if ( o instanceof UnicodeMapping ) {
+                UnicodeMapping m = (UnicodeMapping) o;
+                if ( unicodeIndex > m.unicodeIndex ) {
+                    return 1;
+                } else if ( unicodeIndex < m.unicodeIndex ) {
+                    return -1;
+                } else {
+                    return 0;
+                }
+            } else {
+                return -1;
+            }
+        }
     }
 
     /**
@@ -229,7 +274,7 @@ public class TTFFile {
      */
     private boolean readCMAP(FontFileReader in) throws IOException {
 
-        unicodeMapping = new java.util.ArrayList();
+        unicodeMappings = new java.util.TreeSet();
 
         seekTab(in, "cmap", 2);
         int numCMap = in.readTTFUShort();    // Number of cmap subtables
@@ -370,7 +415,7 @@ public class TTFFile {
                             glyphIdx = (in.readTTFUShort() + cmapDeltas[i])
                                        & 0xffff;
 
-                            unicodeMapping.add(new UnicodeMapping(glyphIdx, j));
+                            unicodeMappings.add(new UnicodeMapping(glyphIdx, j));
                             mtxTab[glyphIdx].getUnicodeIndex().add(new Integer(j));
 
                             if (encodingID == 0 && j >= 0xF020 && j <= 0xF0FF) {
@@ -380,7 +425,7 @@ public class TTFFile {
                                 int mapped = j - 0xF000;
                                 if (!eightBitGlyphs.get(mapped)) {
                                     //Only map if Unicode code point hasn't been mapped before
-                                    unicodeMapping.add(new UnicodeMapping(glyphIdx, mapped));
+                                    unicodeMappings.add(new UnicodeMapping(glyphIdx, mapped));
                                     mtxTab[glyphIdx].getUnicodeIndex().add(new Integer(mapped));
                                 }
                             }
@@ -421,7 +466,7 @@ public class TTFFile {
                                                    + mtxTab.length);
                             }
 
-                            unicodeMapping.add(new UnicodeMapping(glyphIdx, j));
+                            unicodeMappings.add(new UnicodeMapping(glyphIdx, j));
                             if (glyphIdx < mtxTab.length) {
                                 mtxTab[glyphIdx].getUnicodeIndex().add(new Integer(j));
                             } else {
@@ -579,21 +624,83 @@ public class TTFFile {
             return false;
         }
         // Create cmaps for bfentries
+        augmentCMaps();
         createCMaps();
-        // print_max_min();
 
         readKerning(in);
+        readGDEF(in);
         readGSUB(in);
         readGPOS(in);
         guessVerticalMetricsFromGlyphBBox();
         return true;
     }
 
+    /**
+     * Augment the previously ingested CMAP data with new entries to ensure
+     * that every glyph index has a corresponding Unicode value. This is required
+     * by GSUB/GPOS processing which can emit glyph indices that are not in the
+     * normal CMAP (and, for which, on other platforms, the glyph indices are used
+     * directly for rendering purposes (rather than character codes). However, in
+     * the case of FOP IF representation, character codes are used, and, consequently
+     * every glyph needs some character value. Here, we assign them to the Unicode
+     * private use range, starting at 0xE000 up to 0xF8FF. If there are existing
+     * assignments in this range, we just skip over them. Note that it is possible
+     * to exhaust this range of 6400 code values in the case a font has an
+     * extraordinary number of unmapped glyphs. In that case, we do not make
+     * any further assignments, but print a warning message.
+     */
+    private void augmentCMaps() {
+        int numMapped = 0;
+        int numUnmapped = 0;
+        int nextPrivateUse = 0xE000;
+        int firstPrivate = 0;
+        int lastPrivate = 0;
+        int firstUnmapped = 0;
+        int lastUnmapped = 0;
+        for ( int i = 0, n = numberOfGlyphs; i < n; i++ ) {
+            Integer uc = glyphToUnicode ( i );
+            if ( uc == null ) {
+                while ( ( nextPrivateUse < 0xF900 ) && ( unicodeToGlyphMap.get(new Integer(nextPrivateUse)) != null ) ) {
+                    nextPrivateUse++;
+                }
+                if ( nextPrivateUse < 0xF900 ) {
+                    int pu = nextPrivateUse;
+                    unicodeMappings.add ( new UnicodeMapping ( i, pu ) );
+                    if ( firstPrivate == 0 ) {
+                        firstPrivate = pu;
+                    }
+                    lastPrivate = pu;
+                    numMapped++;
+                } else {
+                    if ( firstUnmapped == 0 ) {
+                        firstUnmapped = i;
+                    }
+                    lastUnmapped = i;
+                    numUnmapped++;
+                }
+            }
+        }
+        if ( numMapped > 0 ) {
+            if (log.isDebugEnabled()) {
+                log.debug ( "augment CMAP for "
+                            + numMapped
+                            + " glyphs, mapped to private use characters in the range ["
+                            + CharUtilities.format ( firstPrivate ) + ","
+                            + CharUtilities.format ( lastPrivate ) + "] (inclusive)" );
+            }
+        }
+        if ( numUnmapped > 0 ) {
+            log.warn ( "Exhausted private use area: unable to map "
+                       + numUnmapped + " glyphs in glyph index range ["
+                       + firstUnmapped + "," + lastUnmapped + "] (inclusive) of font '" + getFullName() + "'" );
+        }
+    }
+
     private void createCMaps() {
         cmaps = new java.util.ArrayList();
         TTFCmapEntry tce = new TTFCmapEntry();
 
-        Iterator e = unicodeMapping.listIterator();
+        Iterator e = unicodeMappings.iterator();
         UnicodeMapping um = (UnicodeMapping)e.next();
         UnicodeMapping lastMapping = um;
 
@@ -955,8 +1062,8 @@ public class TTFFile {
         int mtxSize = Math.max(numberOfGlyphs, nhmtx);
         mtxTab = new TTFMtxEntry[mtxSize];
 
-        if (TRACE_ENABLED) {
-            log.debug("*** Widths array: \n");
+        if (log.isTraceEnabled()) {
+            log.trace("*** Widths array: \n");
         }
         for (int i = 0; i < mtxSize; i++) {
             mtxTab[i] = new TTFMtxEntry();
@@ -965,11 +1072,9 @@ public class TTFFile {
             mtxTab[i].setWx(in.readTTFUShort());
             mtxTab[i].setLsb(in.readTTFUShort());
 
-            if (TRACE_ENABLED) {
-                if (log.isDebugEnabled()) {
-                    log.debug("   width[" + i + "] = "
-                        + convertTTFUnit2PDFUnit(mtxTab[i].getWx()) + ";");
-                }
+            if (log.isTraceEnabled()) {
+                log.trace("   width[" + i + "] = "
+                          + convertTTFUnit2PDFUnit(mtxTab[i].getWx()) + ";");
             }
         }
 
@@ -1517,6 +1622,7 @@ public class TTFFile {
         }
     }
 
+    /** helper method for formatting an integer array for output */
     private String toString ( int[] ia ) {
         StringBuffer sb = new StringBuffer();
         if ( ( ia == null ) || ( ia.length == 0 ) ) {
@@ -1726,6 +1832,14 @@ public class TTFFile {
         return ( 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
@@ -1742,13 +1856,64 @@ public class TTFFile {
         return gpos;
     }
 
+    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 CONTEXT                        = 5;
-        static final int CHAINED_CONTEXT                = 6;
+        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() {
@@ -1768,17 +1933,17 @@ public class TTFFile {
             case GSUBLookupType.LIGATURE:
                 st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_LIGATURE;
                 break;
-            case GSUBLookupType.CONTEXT:
-                st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CONTEXT;
+            case GSUBLookupType.CONTEXTUAL:
+                st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CONTEXTUAL;
                 break;
-            case GSUBLookupType.CHAINED_CONTEXT:
-                st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CHAINING_CONTEXT;
+            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_CHAINING_CONTEXT_SINGLE;
+                st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE;
                 break;
             default:
                 st = -1;
@@ -1801,11 +1966,11 @@ public class TTFFile {
             case LIGATURE:
                 s = "Ligature";
                 break;
-            case CONTEXT:
-                s = "Context";
+            case CONTEXTUAL:
+                s = "Contextual";
                 break;
-            case CHAINED_CONTEXT:
-                s = "ChainedContext";
+            case CHAINED_CONTEXTUAL:
+                s = "ChainedContextual";
                 break;
             case EXTENSION:
                 s = "Extension";
@@ -1828,8 +1993,8 @@ public class TTFFile {
         static final int MARK_TO_BASE                   = 4;
         static final int MARK_TO_LIGATURE               = 5;
         static final int MARK_TO_MARK                   = 6;
-        static final int CONTEXT                        = 7;
-        static final int CHAINED_CONTEXT                = 8;
+        static final int CONTEXTUAL                     = 7;
+        static final int CHAINED_CONTEXTUAL             = 8;
         static final int EXTENSION                      = 9;
         private GPOSLookupType() {
         }
@@ -1854,11 +2019,11 @@ public class TTFFile {
             case MARK_TO_MARK:
                 s = "MarkToMark";
                 break;
-            case CONTEXT:
-                s = "Context";
+            case CONTEXTUAL:
+                s = "Contextual";
                 break;
-            case CHAINED_CONTEXT:
-                s = "ChainedContext";
+            case CHAINED_CONTEXTUAL:
+                s = "ChainedContextual";
                 break;
             case EXTENSION:
                 s = "Extension";
@@ -1930,7 +2095,8 @@ public class TTFFile {
         }
     }
 
-    private void readCoverageTableFormat1(FontFileReader in, String label, long tableOffset, int coverageFormat) throws IOException {
+    private GlyphCoverageTable readCoverageTableFormat1(FontFileReader in, 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 );
@@ -1940,80 +2106,118 @@ public class TTFFile {
         for ( int i = 0, n = ng; i < n; i++ ) {
             int g = in.readTTFUShort();
             ga[i] = g;
-            seCoverage.add ( Integer.valueOf(g) );
+            entries.add ( Integer.valueOf(g) );
         }
         // dump info if debugging
         if (log.isDebugEnabled()) {
             log.debug(label + " glyphs: " + toString(ga) );
         }
+        return GlyphCoverageTable.createCoverageTable ( entries );
     }
 
-    private void readCoverageTableFormat2(FontFileReader in, String label, long tableOffset, int coverageFormat) throws IOException {
+    private GlyphCoverageTable readCoverageTableFormat2(FontFileReader in, 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();
-        int[] rsa = new int[nr];
-        int[] rea = new int[nr];
-        int[] rxa = new int[nr];
         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 index
-            int x = 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 + "]: " + x );
+                log.debug(label + " range[" + i + "]: [" + s + "," + e + "]: " + m );
             }
-            rsa[i] = s;
-            rea[i] = e;
-            rxa[i] = x;
-            seCoverage.add ( new GlyphCoverageTable.CoverageRange ( s, e, x ) );
+            entries.add ( new GlyphCoverageTable.MappingRange ( s, e, m ) );
         }
+        return GlyphCoverageTable.createCoverageTable ( entries );
     }
 
-    private void readCoverageTable(FontFileReader in, String label, long tableOffset) throws IOException {
+    private GlyphCoverageTable readCoverageTable(FontFileReader in, 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 ) {
-            readCoverageTableFormat1 ( in, label, tableOffset, cf );
+            gct = readCoverageTableFormat1 ( in, label, tableOffset, cf );
         } else if ( cf == 2 ) {
-            readCoverageTableFormat2 ( in, label, tableOffset, cf );
+            gct = readCoverageTableFormat2 ( in, label, tableOffset, cf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported coverage table format: " + cf );
         }
         in.seekSet ( cp );
+        return gct;
     }
 
-    /* not used yet
-    private void readClassDefTableFormat1(FontFileReader in, long tableOffset, int classFormat) throws IOException {
+    private GlyphClassTable readClassDefTableFormat1(FontFileReader in, 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 ( Integer.valueOf(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 ( Integer.valueOf(gc) );
+        }
+        // dump info if debugging
+        if (log.isDebugEnabled()) {
+            log.debug(label + " glyph classes: " + toString(ca) );
+        }
+        return GlyphClassTable.createClassTable ( entries );
     }
 
-    private void readClassDefTableFormat2(FontFileReader in, long tableOffset, int classFormat) throws IOException {
+    private GlyphClassTable readClassDefTableFormat2(FontFileReader in, 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 void readClassDefTable(FontFileReader in, long tableOffset) throws IOException {
+    private GlyphClassTable readClassDefTable(FontFileReader in, 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 ) {
-            readClassDefTableFormat1 ( in, tableOffset, cf );
+            gct = readClassDefTableFormat1 ( in, label, tableOffset, cf );
         } else if ( cf == 2 ) {
-            readClassDefTableFormat2 ( in, tableOffset, cf );
+            gct = readClassDefTableFormat2 ( in, label, tableOffset, cf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported class definition table format: " + cf );
         }
         in.seekSet ( cp );
+        return gct;
     }
-    */
 
     private void readSingleSubTableFormat1(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
         String tableTag = "GSUB";
@@ -2026,12 +2230,12 @@ public class TTFFile {
         int dg = in.readTTFShort();
         // dump info if debugging
         if (log.isDebugEnabled()) {
-            log.debug(tableTag + " single substitution format: " + subtableFormat + " (delta)" );
+            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
-        readCoverageTable ( in, tableTag + " single substitution coverage", subtableOffset + co );
+        seMapping = readCoverageTable ( in, tableTag + " single substitution coverage", subtableOffset + co );
         seEntries.add ( Integer.valueOf ( dg ) );
     }
 
@@ -2046,12 +2250,12 @@ public class TTFFile {
         int ng = in.readTTFUShort();
         // dump info if debugging
         if (log.isDebugEnabled()) {
-            log.debug(tableTag + " single substitution format: " + subtableFormat + " (mapped)" );
+            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
-        readCoverageTable ( in, tableTag + " single substitution coverage", subtableOffset + co );
+        seMapping = readCoverageTable ( in, tableTag + " single substitution coverage", subtableOffset + co );
         // read glyph substitutions
         int[] gsa = new int[ng];
         for ( int i = 0, n = ng; i < n; i++ ) {
@@ -2066,12 +2270,14 @@ public class TTFFile {
 
     private int readSingleSubTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
         in.seekSet(subtableOffset);
-        // read substitution format
+        // read substitution subtable format
         int sf = in.readTTFUShort();
         if ( sf == 1 ) {
             readSingleSubTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
         } else if ( sf == 2 ) {
             readSingleSubTableFormat2 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported single substitution subtable format: " + sf );
         }
         return sf;
     }
@@ -2087,40 +2293,49 @@ public class TTFFile {
         int ns = in.readTTFUShort();
         // dump info if debugging
         if (log.isDebugEnabled()) {
-            log.debug(tableTag + " multiple substitution format: " + subtableFormat + " (mapped)" );
+            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
-        readCoverageTable ( in, tableTag + " multiple substitution coverage", subtableOffset + co );
+        seMapping = readCoverageTable ( in, 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];
-            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;
+            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(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
         in.seekSet(subtableOffset);
-        // read substitution format
+        // read substitution subtable format
         int sf = in.readTTFUShort();
         if ( sf == 1 ) {
             readMultipleSubTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported multiple substitution subtable format: " + sf );
         }
         return sf;
     }
@@ -2136,12 +2351,12 @@ public class TTFFile {
         int ns = in.readTTFUShort();
         // dump info if debugging
         if (log.isDebugEnabled()) {
-            log.debug(tableTag + " alternate substitution format: " + subtableFormat + " (mapped)" );
+            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
-        readCoverageTable ( in, tableTag + " alternate substitution coverage", subtableOffset + co );
+        seMapping = readCoverageTable ( in, 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++ ) {
@@ -2161,15 +2376,18 @@ public class TTFFile {
             if (log.isDebugEnabled()) {
                 log.debug(tableTag + " alternate substitution alternate set[" + i + "]: " + toString ( ga ) );
             }
+            seEntries.add ( ga );
         }
     }
 
     private int readAlternateSubTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
         in.seekSet(subtableOffset);
-        // read substitution format
+        // read substitution subtable format
         int sf = in.readTTFUShort();
         if ( sf == 1 ) {
             readAlternateSubTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported alternate substitution subtable format: " + sf );
         }
         return sf;
     }
@@ -2185,12 +2403,12 @@ public class TTFFile {
         int ns = in.readTTFUShort();
         // dump info if debugging
         if (log.isDebugEnabled()) {
-            log.debug(tableTag + " ligature substitution format: " + subtableFormat + " (mapped)" );
+            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
-        readCoverageTable ( in, tableTag + " ligature substitution coverage", subtableOffset + co );
+        seMapping = readCoverageTable ( in, 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++ ) {
@@ -2230,322 +2448,766 @@ public class TTFFile {
 
     private int readLigatureSubTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
         in.seekSet(subtableOffset);
-        // read substitution format
+        // read substitution subtable format
         int sf = in.readTTFUShort();
         if ( sf == 1 ) {
             readLigatureSubTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported ligature substitution subtable format: " + sf );
         }
         return sf;
     }
 
-    private int readContextSubTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+    private GlyphTable.RuleLookup[] readRuleLookups(FontFileReader in, 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(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
+        String tableTag = "GSUB";
         in.seekSet(subtableOffset);
-        // read substitution format
-        int sf = in.readTTFUShort();
-        // [TBD] - implement me
-        return sf;
+        // 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 ( in, 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 ( in, 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 readChainedContextSubTableFormat1(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
+    private void readContextualSubTableFormat2(FontFileReader in, 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 subrule set count
-        int ns = 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 + " chained context substitution format: " + subtableFormat + " (simple)" );
-            log.debug(tableTag + " chained context substitution coverage table offset: " + co );
-            log.debug(tableTag + " chained context substitution subrule set count: " + ns );
+            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
-        readCoverageTable ( in, tableTag + " chained context substitution coverage", subtableOffset + co );
-        // read subrule set table offsets
-        int[] soa = new int[ns];
-        for ( int i = 0, n = ns; i < n; i++ ) {
-            soa[i] = in.readTTFUShort();
+        GlyphCoverageTable ct;
+        if ( co > 0 ) {
+            ct = readCoverageTable ( in, tableTag + " contextual substitution coverage", subtableOffset + co );
+        } else {
+            ct = null;
         }
-        // read subrule set tables
-        for ( int i = 0, n = ns; i < n; i++ ) {
-            int so = soa[i];
-            in.seekSet(subtableOffset + so);
-            // read subrule table count
-            int nst = in.readTTFUShort();
-            int[] stoa = new int[nst];
-            for ( int j = 0; j < nst; j++ ) {
-                stoa[j] = in.readTTFUShort();
-            }
-            for ( int j = 0; j < nst; j++ ) {
-                int sto = stoa[j];
-                in.seekSet(subtableOffset + so + sto);
-                // read backtrack glyph count
-                int nbg = in.readTTFUShort();
-                int[] bga = new int[nbg];
-                // read backtrack glyphs
-                for ( int k = 0; k < nbg; k++ ) {
-                    bga[k] = in.readTTFUShort();
-                }
-                // read input glyph count
-                int nig = in.readTTFUShort();
-                int[] iga = new int [ nig - 1 ];
-                // read input glyphs
-                for ( int k = 0; k < nig - 1; k++ ) {
-                    iga[k] = in.readTTFUShort();
-                }
-                // read lookahead glyph count
-                int nlg = in.readTTFUShort();
-                int[] lga = new int[nlg];
-                // read lookahead glyphs
-                for ( int k = 0; k < nlg; k++ ) {
-                    lga[k] = in.readTTFUShort();
-                }
-                // read substitution lookup record count
-                int nsl = in.readTTFUShort();
-                int[] sia = new int[nsl];
-                int[] lia = new int[nsl];
-                // read substitution lookup records
-                for ( int k = 0; k < nsl; k++ ) {
-                    // read sequence index
-                    sia[k] = in.readTTFUShort();
-                    // read lookup list index
-                    lia[k] = in.readTTFUShort();
+        // read class definition table
+        GlyphClassTable cdt;
+        if ( cdo > 0 ) {
+            cdt = readClassDefTable ( in, 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();
                 }
-                if (log.isDebugEnabled()) {
-                    log.debug(tableTag + " chained context substitution subrule set[" + i + "]: backtrack [" + toString(bga) + "]" );
-                    log.debug(tableTag + " chained context substitution subrule set[" + i + "]: input     [" + toString(iga) + "]" );
-                    log.debug(tableTag + " chained context substitution subrule set[" + i + "]: lookahead [" + toString(lga) + "]" );
-                    log.debug(tableTag + " chained context substitution lookup count: " + nsl );
-                    for ( int k = 0; k < nsl; k++ ) {
-                        log.debug(tableTag + " chained context substitution lookup[" + i + "]: [" + sia[k] + "," + lia[k] + "]" );
+                // 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 ( in, 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 ( Integer.valueOf ( ngc ) );
+        seEntries.add ( rsa );
+    }
+
+    private void readContextualSubTableFormat3(FontFileReader in, 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 ( in, 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 ( in, 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(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+        in.seekSet(subtableOffset);
+        // read substitution subtable format
+        int sf = in.readTTFUShort();
+        if ( sf == 1 ) {
+            readContextualSubTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else if ( sf == 2 ) {
+            readContextualSubTableFormat2 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else if ( sf == 3 ) {
+            readContextualSubTableFormat3 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported contextual substitution subtable format: " + sf );
         }
+        return sf;
     }
 
-    private void readChainedContextSubTableFormat2(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
+    private void readChainedContextualSubTableFormat1(FontFileReader in, 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 classdef table offset
-        int bo = in.readTTFUShort();
-        // read input classdef table offset
-        int io = in.readTTFUShort();
-        // read lookahead classdef table offset
-        int lo = in.readTTFUShort();
-        // read subclass set count
-        int ns = 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 context substitution format: " + subtableFormat + " (class based)" );
-            log.debug(tableTag + " chained context substitution coverage table offset: " + co );
-            log.debug(tableTag + " chained context substitution backtrack classdef table offset: " + bo );
-            log.debug(tableTag + " chained context substitution input classdef table offset: " + io );
-            log.debug(tableTag + " chained context substitution lookahead classdef table offset: " + lo );
-            log.debug(tableTag + " chained context substitution subclass set count: " + ns );
+            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
-        readCoverageTable ( in, tableTag + " chained context substitution coverage", subtableOffset + co );
-        // read subclass set table offsets
-        int[] soa = new int[ns];
-        for ( int i = 0, n = ns; i < n; i++ ) {
-            soa[i] = in.readTTFUShort();
+        GlyphCoverageTable ct;
+        if ( co > 0 ) {
+            ct = readCoverageTable ( in, 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 ( in, 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;
         }
-        // read subclass set tables
-        for ( int i = 0, n = ns; i < n; i++ ) {
-            int so = soa[i];
-            if ( so == 0 ) {
-                continue;
+        // store results
+        seMapping = ct;
+        seEntries.add ( rsa );
+    }
+
+    private void readChainedContextualSubTableFormat2(FontFileReader in, 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] );
             }
-            in.seekSet(subtableOffset + so);
-            // read subclass rule table count
-            int nst = in.readTTFUShort();
-            int[] stoa = new int[nst];
-            for ( int j = 0; j < nst; j++ ) {
-                stoa[j] = in.readTTFUShort();
-            }
-            for ( int j = 0; j < nst; j++ ) {
-                int sto = stoa[j];
-                in.seekSet(subtableOffset + so + sto);
-                // read backtrack class count
-                int nbc = in.readTTFUShort();
-                int[] bca = new int[nbc];
-                // read backtrack classes
-                for ( int k = 0; k < nbc; k++ ) {
-                    bca[k] = in.readTTFUShort();
-                }
-                // read input class count
-                int nic = in.readTTFUShort();
-                int[] ica = new int [ nic - 1 ];
-                // read inpput classes
-                for ( int k = 0; k < nic - 1; k++ ) {
-                    ica[k] = in.readTTFUShort();
-                }
-                // read lookahead class count
-                int nlc = in.readTTFUShort();
-                int[] lca = new int[nlc];
-                // read lookahead classes
-                for ( int k = 0; k < nlc; k++ ) {
-                    lca[k] = in.readTTFUShort();
-                }
-                // read substitution lookup record count
-                int nsl = in.readTTFUShort();
-                int[] sia = new int[nsl];
-                int[] lia = new int[nsl];
-                // read substitution lookup records
-                for ( int k = 0; k < nsl; k++ ) {
-                    // read sequence index
-                    sia[k] = in.readTTFUShort();
-                    // read lookup list index
-                    lia[k] = in.readTTFUShort();
+        }
+        // read coverage table
+        GlyphCoverageTable ct;
+        if ( co > 0 ) {
+            ct = readCoverageTable ( in, tableTag + " chained contextual substitution coverage", subtableOffset + co );
+        } else {
+            ct = null;
+        }
+        // read backtrack class definition table
+        GlyphClassTable bcdt;
+        if ( bcdo > 0 ) {
+            bcdt = readClassDefTable ( in, tableTag + " contextual substitution backtrack class definition", subtableOffset + bcdo );
+        } else {
+            bcdt = null;
+        }
+        // read input class definition table
+        GlyphClassTable icdt;
+        if ( icdo > 0 ) {
+            icdt = readClassDefTable ( in, tableTag + " contextual substitution input class definition", subtableOffset + icdo );
+        } else {
+            icdt = null;
+        }
+        // read lookahead class definition table
+        GlyphClassTable lcdt;
+        if ( lcdo > 0 ) {
+            lcdt = readClassDefTable ( in, 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();
                 }
-                if (log.isDebugEnabled()) {
-                    log.debug(tableTag + " chained context substitution subclass set[" + i + "]: backtrack [" + toString(bca) + "]" );
-                    log.debug(tableTag + " chained context substitution subclass set[" + i + "]: input     [" + toString(ica) + "]" );
-                    log.debug(tableTag + " chained context substitution subclass set[" + i + "]: lookahead [" + toString(lca) + "]" );
-                    log.debug(tableTag + " chained context substitution lookup count: " + nsl );
-                    for ( int k = 0; k < nsl; k++ ) {
-                        log.debug(tableTag + " chained context substitution lookup[" + i + "]: [" + sia[k] + "," + lia[k] + "]" );
+                // 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 ( in, 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 ( Integer.valueOf ( ngc ) );
+        seEntries.add ( rsa );
     }
 
-    private void readChainedContextSubTableFormat3(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
+    private void readChainedContextualSubTableFormat3(FontFileReader in, 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 table offsets
-        int[] boa = new int[nbg];
+        // read backtrack glyph coverage offsets
+        int[] bgcoa = new int [ nbg ];
         for ( int i = 0; i < nbg; i++ ) {
-            boa[i] = in.readTTFUShort();
+            bgcoa [ i ] = in.readTTFUShort();
         }
         // read input glyph count
         int nig = in.readTTFUShort();
-        // read input glyph coverage table offsets
-        int[] ioa = new int[nig];
+        // read backtrack glyph coverage offsets
+        int[] igcoa = new int [ nig ];
         for ( int i = 0; i < nig; i++ ) {
-            ioa[i] = in.readTTFUShort();
+            igcoa [ i ] = in.readTTFUShort();
         }
         // read lookahead glyph count
         int nlg = in.readTTFUShort();
-        // read lookahead glyph coverage table offsets
-        int[] loa = new int[nlg];
+        // read backtrack glyph coverage offsets
+        int[] lgcoa = new int [ nlg ];
         for ( int i = 0; i < nlg; i++ ) {
-            loa[i] = in.readTTFUShort();
-        }
-        // read substitution lookup record count
-        int nsl = in.readTTFUShort();
-        int[] sia = new int[nsl];
-        int[] lia = new int[nsl];
-        // read substitution lookup records
-        for ( int i = 0; i < nsl; i++ ) {
-            // read sequence index
-            sia[i] = in.readTTFUShort();
-            // read lookup list index
-            lia[i] = in.readTTFUShort();
+            lgcoa [ i ] = in.readTTFUShort();
         }
+        // read substitution lookup count
+        int nl = in.readTTFUShort();
         // dump info if debugging
         if (log.isDebugEnabled()) {
-            log.debug(tableTag + " chained context substitution format: " + subtableFormat + " (coverage based)" );
-            log.debug(tableTag + " chained context substitution backtrack coverage table offsets: " + toString(boa) );
-            log.debug(tableTag + " chained context substitution input coverage table offsets: " + toString(ioa) );
-            log.debug(tableTag + " chained context substitution lookahead coverage table offsets: " + toString(loa) );
-            log.debug(tableTag + " chained context substitution lookup count: " + nsl );
-            for ( int i = 0; i < nsl; i++ ) {
-                log.debug(tableTag + " chained context substitution lookup[" + i + "]: [" + sia[i] + "," + lia[i] + "]" );
+            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
-        for ( int i = 0; i < boa.length; i++ ) {
-            if (log.isDebugEnabled()) {
-                log.debug(tableTag + " chained context substitution backtrack coverage table[" + i + "]" );
+        GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg];
+        for ( int i = 0; i < nbg; i++ ) {
+            int bgco = bgcoa [ i ];
+            GlyphCoverageTable bgct;
+            if ( bgco > 0 ) {
+                bgct = readCoverageTable ( in, tableTag + " chained contextual substitution backtrack coverage[" + i + "]", subtableOffset + bgco );
+            } else {
+                bgct = null;
             }
-            readCoverageTable ( in, tableTag + " chained context substitution coverage", subtableOffset + boa [ i ] );
+            bgca[i] = bgct;
         }
         // read input coverage tables
-        for ( int i = 0; i < ioa.length; i++ ) {
-            if (log.isDebugEnabled()) {
-                log.debug(tableTag + " chained context substitution input coverage table[" + i + "]" );
+        GlyphCoverageTable[] igca = new GlyphCoverageTable[nig];
+        for ( int i = 0; i < nig; i++ ) {
+            int igco = igcoa [ i ];
+            GlyphCoverageTable igct;
+            if ( igco > 0 ) {
+                igct = readCoverageTable ( in, tableTag + " chained contextual substitution input coverage[" + i + "]", subtableOffset + igco );
+            } else {
+                igct = null;
             }
-            readCoverageTable ( in, tableTag + " chained context substitution coverage", subtableOffset + ioa [ i ] );
+            igca[i] = igct;
         }
         // read lookahead coverage tables
-        for ( int i = 0; i < loa.length; i++ ) {
-            if (log.isDebugEnabled()) {
-                log.debug(tableTag + " chained context substitution lookahead coverage table[" + i + "]" );
+        GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg];
+        for ( int i = 0; i < nlg; i++ ) {
+            int lgco = lgcoa [ i ];
+            GlyphCoverageTable lgct;
+            if ( lgco > 0 ) {
+                lgct = readCoverageTable ( in, tableTag + " chained contextual substitution lookahead coverage[" + i + "]", subtableOffset + lgco );
+            } else {
+                lgct = null;
             }
-            readCoverageTable ( in, tableTag + " chained context substitution coverage", subtableOffset + loa [ i ] );
+            lgca[i] = lgct;
         }
+        // read rule lookups
+        String header = null;
+        if (log.isDebugEnabled()) {
+            header = tableTag + " chained contextual substitution lookups: ";
+        }
+        GlyphTable.RuleLookup[] lookups = readRuleLookups ( in, 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 readChainedContextSubTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+    private int readChainedContextualSubTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
         in.seekSet(subtableOffset);
-        // read substitution format
+        // read substitution subtable format
         int sf = in.readTTFUShort();
         if ( sf == 1 ) {
-            readChainedContextSubTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+            readChainedContextualSubTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
         } else if ( sf == 2 ) {
-            readChainedContextSubTableFormat2 ( in, lookupType, lookupFlags, subtableOffset, sf );
+            readChainedContextualSubTableFormat2 ( in, lookupType, lookupFlags, subtableOffset, sf );
         } else if ( sf == 3 ) {
-            readChainedContextSubTableFormat3 ( in, lookupType, lookupFlags, subtableOffset, sf );
+            readChainedContextualSubTableFormat3 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported chained contextual substitution subtable format: " + sf );
         }
         return sf;
     }
 
-    private int readExtensionSubTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+    private void readExtensionSubTableFormat1(FontFileReader in, int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset, int subtableFormat) throws IOException {
+        String tableTag = "GSUB";
         in.seekSet(subtableOffset);
-        // read substitution format
-        int sf = in.readTTFUShort();
-        // [TBD] - implement me
-        return sf;
+        // 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 ( in, lt, lookupFlags, lookupSequence, subtableSequence, subtableOffset + eo );
     }
 
-    private int readReverseChainedSingleSubTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+    private int readExtensionSubTable(FontFileReader in, int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException {
         in.seekSet(subtableOffset);
-        // read substitution format
+        // read substitution subtable format
         int sf = in.readTTFUShort();
-        // [TBD] - implement me
+        if ( sf == 1 ) {
+            readExtensionSubTableFormat1 ( in, lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported extension substitution subtable format: " + sf );
+        }
         return sf;
     }
 
-    private void readGSUBSubtable(FontFileReader in, int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException {
-        initSESubState();
-        int subtableFormat = -1;
-        switch ( lookupType ) {
-        case GSUBLookupType.SINGLE:
-            subtableFormat = readSingleSubTable ( in, lookupType, lookupFlags, subtableOffset );
-            break;
-        case GSUBLookupType.MULTIPLE:
-            subtableFormat = readMultipleSubTable ( in, lookupType, lookupFlags, subtableOffset );
-            break;
-        case GSUBLookupType.ALTERNATE:
-            subtableFormat = readAlternateSubTable ( in, lookupType, lookupFlags, subtableOffset );
-            break;
+    private void readReverseChainedSingleSubTableFormat1(FontFileReader in, 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 ( in, 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 ( in, 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 ( in, 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(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+        in.seekSet(subtableOffset);
+        // read substitution subtable format
+        int sf = in.readTTFUShort();
+        if ( sf == 1 ) {
+            readReverseChainedSingleSubTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported reverse chained single substitution subtable format: " + sf );
+        }
+        return sf;
+    }
+
+    private void readGSUBSubtable(FontFileReader in, int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException {
+        initSESubState();
+        int subtableFormat = -1;
+        switch ( lookupType ) {
+        case GSUBLookupType.SINGLE:
+            subtableFormat = readSingleSubTable ( in, lookupType, lookupFlags, subtableOffset );
+            break;
+        case GSUBLookupType.MULTIPLE:
+            subtableFormat = readMultipleSubTable ( in, lookupType, lookupFlags, subtableOffset );
+            break;
+        case GSUBLookupType.ALTERNATE:
+            subtableFormat = readAlternateSubTable ( in, lookupType, lookupFlags, subtableOffset );
+            break;
         case GSUBLookupType.LIGATURE:
             subtableFormat = readLigatureSubTable ( in, lookupType, lookupFlags, subtableOffset );
             break;
-        case GSUBLookupType.CONTEXT:
-            subtableFormat = readContextSubTable ( in, lookupType, lookupFlags, subtableOffset );
+        case GSUBLookupType.CONTEXTUAL:
+            subtableFormat = readContextualSubTable ( in, lookupType, lookupFlags, subtableOffset );
             break;
-        case GSUBLookupType.CHAINED_CONTEXT:
-            subtableFormat = readChainedContextSubTable ( in, lookupType, lookupFlags, subtableOffset );
+        case GSUBLookupType.CHAINED_CONTEXTUAL:
+            subtableFormat = readChainedContextualSubTable ( in, lookupType, lookupFlags, subtableOffset );
             break;
         case GSUBLookupType.REVERSE_CHAINED_SINGLE:
             subtableFormat = readReverseChainedSingleSubTable ( in, lookupType, lookupFlags, subtableOffset );
             break;
         case GSUBLookupType.EXTENSION:
-            subtableFormat = readExtensionSubTable ( in, lookupType, lookupFlags, subtableOffset );
+            subtableFormat = readExtensionSubTable ( in, lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset );
             break;
         default:
             break;
@@ -2554,75 +3216,1419 @@ public class TTFFile {
         resetSESubState();
     }
 
+    private GlyphPositioningTable.DeviceTable readPosDeviceTable(FontFileReader in, 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();
+        // read deltas
+        int n = ( es - ss ) + 1;
+        int[] da = new int [ n ];
+        int s1, m1, dm, dd, 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 {
+            throw new UnsupportedOperationException ( "unsupported device table delta format: " + df );
+        }
+        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(FontFileReader in, long subtableOffset, int valueFormat) throws IOException {
+        // XPlacement
+        int xp;
+        if ( ( valueFormat & GlyphPositioningTable.Value.X_PLACEMENT ) != 0 ) {
+            xp = convertTTFUnit2PDFUnit ( in.readTTFShort() );
+        } else {
+            xp = 0;
+        }
+        // YPlacement
+        int yp;
+        if ( ( valueFormat & GlyphPositioningTable.Value.Y_PLACEMENT ) != 0 ) {
+            yp = convertTTFUnit2PDFUnit ( in.readTTFShort() );
+        } else {
+            yp = 0;
+        }
+        // XAdvance
+        int xa;
+        if ( ( valueFormat & GlyphPositioningTable.Value.X_ADVANCE ) != 0 ) {
+            xa = convertTTFUnit2PDFUnit ( in.readTTFShort() );
+        } else {
+            xa = 0;
+        }
+        // YAdvance
+        int ya;
+        if ( ( valueFormat & GlyphPositioningTable.Value.Y_ADVANCE ) != 0 ) {
+            ya = convertTTFUnit2PDFUnit ( in.readTTFShort() );
+        } else {
+            ya = 0;
+        }
+        // XPlaDevice
+        GlyphPositioningTable.DeviceTable xpd;
+        if ( ( valueFormat & GlyphPositioningTable.Value.X_PLACEMENT_DEVICE ) != 0 ) {
+            int xpdo = in.readTTFUShort();
+            xpd = readPosDeviceTable ( in, subtableOffset, xpdo );
+        } else {
+            xpd = null;
+        }
+        // YPlaDevice
+        GlyphPositioningTable.DeviceTable ypd;
+        if ( ( valueFormat & GlyphPositioningTable.Value.Y_PLACEMENT_DEVICE ) != 0 ) {
+            int ypdo = in.readTTFUShort();
+            ypd = readPosDeviceTable ( in, subtableOffset, ypdo );
+        } else {
+            ypd = null;
+        }
+        // XAdvDevice
+        GlyphPositioningTable.DeviceTable xad;
+        if ( ( valueFormat & GlyphPositioningTable.Value.X_ADVANCE_DEVICE ) != 0 ) {
+            int xado = in.readTTFUShort();
+            xad = readPosDeviceTable ( in, subtableOffset, xado );
+        } else {
+            xad = null;
+        }
+        // YAdvDevice
+        GlyphPositioningTable.DeviceTable yad;
+        if ( ( valueFormat & GlyphPositioningTable.Value.Y_ADVANCE_DEVICE ) != 0 ) {
+            int yado = in.readTTFUShort();
+            yad = readPosDeviceTable ( in, subtableOffset, yado );
+        } else {
+            yad = null;
+        }
+        return new GlyphPositioningTable.Value ( xp, yp, xa, ya, xpd, ypd, xad, yad );
+    }
+
+    private void readSinglePosTableFormat1(FontFileReader in, 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 ( in, 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 ( in, tableTag + " single positioning coverage", subtableOffset + co );
+        // store results
+        seMapping = ct;
+        seEntries.add ( v );
+    }
+
+    private void readSinglePosTableFormat2(FontFileReader in, 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 ( in, 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 ( in, 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(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
         in.seekSet(subtableOffset);
-        // read substitution format
+        // read positionining subtable format
         int sf = in.readTTFUShort();
-        // [TBD] - implement me
+        if ( sf == 1 ) {
+            readSinglePosTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else if ( sf == 2 ) {
+            readSinglePosTableFormat2 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported single positioning subtable format: " + sf );
+        }
         return sf;
     }
 
+    private GlyphPositioningTable.PairValues readPosPairValues(FontFileReader in, 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 ( in, subtableOffset, vf1 );
+        } else {
+            v1 = null;
+        }
+        // read second value (if present)
+        GlyphPositioningTable.Value v2;
+        if ( vf2 != 0 ) {
+            v2 = readPosValue ( in, subtableOffset, vf2 );
+        } else {
+            v2 = null;
+        }
+        return new GlyphPositioningTable.PairValues ( glyph, v1, v2 );
+    }
+
+    private GlyphPositioningTable.PairValues[] readPosPairSetTable(FontFileReader in, 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 ( in, 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(FontFileReader in, 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 ( in, 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 ( in, subtableOffset, pso, vf1, vf2 );
+        }
+        // store results
+        seMapping = ct;
+        seEntries.add ( pvm );
+    }
+
+    private void readPairPosTableFormat2(FontFileReader in, 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 ( in, tableTag + " pair positioning coverage", subtableOffset + co );
+        // read class definition table #1
+        GlyphClassTable cdt1 = readClassDefTable ( in, tableTag + " pair positioning class definition #1", subtableOffset + cd1o );
+        // read class definition table #2
+        GlyphClassTable cdt2 = readClassDefTable ( in, 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 ( in, 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 ( Integer.valueOf ( nc1 ) );
+        seEntries.add ( Integer.valueOf ( nc2 ) );
+        seEntries.add ( pvm );
+    }
+
     private int readPairPosTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
         in.seekSet(subtableOffset);
-        // read substitution format
+        // read positioning subtable format
         int sf = in.readTTFUShort();
-        // [TBD] - implement me
+        if ( sf == 1 ) {
+            readPairPosTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else if ( sf == 2 ) {
+            readPairPosTableFormat2 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported pair positioning subtable format: " + sf );
+        }
         return sf;
     }
 
+    private GlyphPositioningTable.Anchor readPosAnchor(FontFileReader in, 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 = convertTTFUnit2PDFUnit ( in.readTTFShort() );
+            // read y coordinate
+            int y = convertTTFUnit2PDFUnit ( in.readTTFShort() );
+            a = new GlyphPositioningTable.Anchor ( x, y );
+        } else if ( af == 2 ) {
+            // read x coordinate
+            int x = convertTTFUnit2PDFUnit ( in.readTTFShort() );
+            // read y coordinate
+            int y = 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 = convertTTFUnit2PDFUnit ( in.readTTFShort() );
+            // read y coordinate
+            int y = 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 ( in, cp, xdo );
+            } else {
+                xd = null;
+            }
+            // read y device table (if present)
+            GlyphPositioningTable.DeviceTable yd;
+            if ( ydo != 0 ) {
+                yd = readPosDeviceTable ( in, cp, ydo );
+            } else {
+                yd = null;
+            }
+            a = new GlyphPositioningTable.Anchor ( x, y, xd, yd );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported positioning anchor format: " + af );
+        }
+        in.seekSet(cp);
+        return a;
+    }
+
+    private void readCursivePosTableFormat1(FontFileReader in, 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 ( in, 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 ( in, subtableOffset + eno );
+            } else {
+                ena = null;
+            }
+            // read exit anchor
+            GlyphPositioningTable.Anchor exa;
+            if ( exo > 0 ) {
+                exa = readPosAnchor ( in, 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(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
         in.seekSet(subtableOffset);
-        // read substitution format
+        // read positioning subtable format
+        int sf = in.readTTFUShort();
+        if ( sf == 1 ) {
+            readCursivePosTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported cursive positioning subtable format: " + sf );
+        }
+        return sf;
+    }
+
+    private void readMarkToBasePosTableFormat1(FontFileReader in, 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 ( in, tableTag + " mark-to-base positioning mark coverage", subtableOffset + mco );
+        // read base coverage table
+        GlyphCoverageTable bct = readCoverageTable ( in, 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 ( in, 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 ( in, 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 ( Integer.valueOf ( nmc ) );
+        seEntries.add ( maa );
+        seEntries.add ( bam );
+    }
+
+    private int readMarkToBasePosTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+        in.seekSet(subtableOffset);
+        // read positioning subtable format
+        int sf = in.readTTFUShort();
+        if ( sf == 1 ) {
+            readMarkToBasePosTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported mark-to-base positioning subtable format: " + sf );
+        }
+        return sf;
+    }
+
+    private void readMarkToLigaturePosTableFormat1(FontFileReader in, 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 ( in, tableTag + " mark-to-ligature positioning mark coverage", subtableOffset + mco );
+        // read ligature coverage table
+        GlyphCoverageTable lct = readCoverageTable ( in, 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 ( in, 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 ( in, 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 ( Integer.valueOf ( nmc ) );
+        seEntries.add ( Integer.valueOf ( mxc ) );
+        seEntries.add ( maa );
+        seEntries.add ( lam );
+    }
+
+    private int readMarkToLigaturePosTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+        in.seekSet(subtableOffset);
+        // read positioning subtable format
+        int sf = in.readTTFUShort();
+        if ( sf == 1 ) {
+            readMarkToLigaturePosTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported mark-to-ligature positioning subtable format: " + sf );
+        }
+        return sf;
+    }
+
+    private void readMarkToMarkPosTableFormat1(FontFileReader in, 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 ( in, tableTag + " mark-to-mark positioning mark #1 coverage", subtableOffset + m1co );
+        // read mark #2 coverage table
+        GlyphCoverageTable mct2 = readCoverageTable ( in, 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 ( in, 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 ( in, 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 ( Integer.valueOf ( nmc ) );
+        seEntries.add ( maa );
+        seEntries.add ( mam );
+    }
+
+    private int readMarkToMarkPosTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+        in.seekSet(subtableOffset);
+        // read positioning subtable format
+        int sf = in.readTTFUShort();
+        if ( sf == 1 ) {
+            readMarkToMarkPosTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported mark-to-mark positioning subtable format: " + sf );
+        }
+        return sf;
+    }
+
+    private void readContextualPosTableFormat1(FontFileReader in, 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 ( in, 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 ( in, 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(FontFileReader in, 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 ( in, tableTag + " contextual positioning coverage", subtableOffset + co );
+        } else {
+            ct = null;
+        }
+        // read class definition table
+        GlyphClassTable cdt;
+        if ( cdo > 0 ) {
+            cdt = readClassDefTable ( in, 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 ( in, 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 ( Integer.valueOf ( ngc ) );
+        seEntries.add ( rsa );
+    }
+
+    private void readContextualPosTableFormat3(FontFileReader in, 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 ( in, 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 ( in, 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(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+        in.seekSet(subtableOffset);
+        // read positioning subtable format
         int sf = in.readTTFUShort();
-        // [TBD] - implement me
+        if ( sf == 1 ) {
+            readContextualPosTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else if ( sf == 2 ) {
+            readContextualPosTableFormat2 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else if ( sf == 3 ) {
+            readContextualPosTableFormat3 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported contextual positioning subtable format: " + sf );
+        }
         return sf;
     }
 
-    private int readMarkToBasePosTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+    private void readChainedContextualPosTableFormat1(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
+        String tableTag = "GPOS";
         in.seekSet(subtableOffset);
-        // read substitution format
-        int sf = in.readTTFUShort();
-        // [TBD] - implement me
-        return sf;
+        // 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 ( in, 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 ( in, 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 int readMarkToLigaturePosTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+    private void readChainedContextualPosTableFormat2(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
+        String tableTag = "GPOS";
         in.seekSet(subtableOffset);
-        // read substitution format
-        int sf = in.readTTFUShort();
-        // [TBD] - implement me
-        return sf;
+        // 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 ( in, tableTag + " chained contextual positioning coverage", subtableOffset + co );
+        } else {
+            ct = null;
+        }
+        // read backtrack class definition table
+        GlyphClassTable bcdt;
+        if ( bcdo > 0 ) {
+            bcdt = readClassDefTable ( in, tableTag + " contextual positioning backtrack class definition", subtableOffset + bcdo );
+        } else {
+            bcdt = null;
+        }
+        // read input class definition table
+        GlyphClassTable icdt;
+        if ( icdo > 0 ) {
+            icdt = readClassDefTable ( in, tableTag + " contextual positioning input class definition", subtableOffset + icdo );
+        } else {
+            icdt = null;
+        }
+        // read lookahead class definition table
+        GlyphClassTable lcdt;
+        if ( lcdo > 0 ) {
+            lcdt = readClassDefTable ( in, 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 ( in, 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 ( Integer.valueOf ( ngc ) );
+        seEntries.add ( rsa );
     }
 
-    private int readMarkToMarkPosTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+    private void readChainedContextualPosTableFormat3(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException {
+        String tableTag = "GPOS";
         in.seekSet(subtableOffset);
-        // read substitution format
-        int sf = in.readTTFUShort();
-        // [TBD] - implement me
-        return sf;
+        // 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 ( in, 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 ( in, 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 ( in, 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 ( in, 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 readContextPosTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+    private int readChainedContextualPosTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
         in.seekSet(subtableOffset);
-        // read substitution format
+        // read positioning subtable format
         int sf = in.readTTFUShort();
-        // [TBD] - implement me
+        if ( sf == 1 ) {
+            readChainedContextualPosTableFormat1 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else if ( sf == 2 ) {
+            readChainedContextualPosTableFormat2 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else if ( sf == 3 ) {
+            readChainedContextualPosTableFormat3 ( in, lookupType, lookupFlags, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported chained contextual positioning subtable format: " + sf );
+        }
         return sf;
     }
 
-    private int readChainedContextPosTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+    private void readExtensionPosTableFormat1(FontFileReader in, int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset, int subtableFormat) throws IOException {
+        String tableTag = "GPOS";
         in.seekSet(subtableOffset);
-        // read substitution format
-        int sf = in.readTTFUShort();
-        // [TBD] - implement me
-        return sf;
+        // 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 ( in, lt, lookupFlags, lookupSequence, subtableSequence, subtableOffset + eo );
     }
 
-    private int readExtensionPosTable(FontFileReader in, int lookupType, int lookupFlags, long subtableOffset) throws IOException {
+    private int readExtensionPosTable(FontFileReader in, int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException {
         in.seekSet(subtableOffset);
-        // read substitution format
+        // read positioning subtable format
         int sf = in.readTTFUShort();
-        // [TBD] - implement me
+        if ( sf == 1 ) {
+            readExtensionPosTableFormat1 ( in, lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported extension positioning subtable format: " + sf );
+        }
         return sf;
     }
 
@@ -2648,14 +4654,14 @@ public class TTFFile {
         case GPOSLookupType.MARK_TO_MARK:
             subtableFormat = readMarkToMarkPosTable ( in, lookupType, lookupFlags, subtableOffset );
             break;
-        case GPOSLookupType.CONTEXT:
-            subtableFormat = readContextPosTable ( in, lookupType, lookupFlags, subtableOffset );
+        case GPOSLookupType.CONTEXTUAL:
+            subtableFormat = readContextualPosTable ( in, lookupType, lookupFlags, subtableOffset );
             break;
-        case GPOSLookupType.CHAINED_CONTEXT:
-            subtableFormat = readChainedContextPosTable ( in, lookupType, lookupFlags, subtableOffset );
+        case GPOSLookupType.CHAINED_CONTEXTUAL:
+            subtableFormat = readChainedContextualPosTable ( in, lookupType, lookupFlags, subtableOffset );
             break;
         case GPOSLookupType.EXTENSION:
-            subtableFormat = readExtensionPosTable ( in, lookupType, lookupFlags, subtableOffset );
+            subtableFormat = readExtensionPosTable ( in, lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset );
             break;
         default:
             break;
@@ -2764,6 +4770,197 @@ public class TTFFile {
         }
     }
 
+    private void readGDEFClassDefTable(FontFileReader in, String tableTag, int lookupSequence, long subtableOffset) throws IOException {
+        initSESubState();
+        in.seekSet(subtableOffset);
+        // subtable is a bare class definition table
+        GlyphClassTable ct = readClassDefTable ( in, 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 );
+        resetSESubState();
+    }
+
+    private void readGDEFAttachmentTable(FontFileReader in, String tableTag, int lookupSequence, long subtableOffset) throws IOException {
+        initSESubState();
+        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 ( in, 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 );
+        resetSESubState();
+    }
+
+    private void readGDEFLigatureCaretTable(FontFileReader in, String tableTag, int lookupSequence, long subtableOffset) throws IOException {
+        initSESubState();
+        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 ( in, 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 );
+        resetSESubState();
+    }
+
+    private void readGDEFMarkAttachmentTable(FontFileReader in, String tableTag, int lookupSequence, long subtableOffset) throws IOException {
+        initSESubState();
+        in.seekSet(subtableOffset);
+        // subtable is a bare class definition table
+        GlyphClassTable ct = readClassDefTable ( in, 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 );
+        resetSESubState();
+    }
+
+    private void readGDEFMarkGlyphsTableFormat1(FontFileReader in, String tableTag, int lookupSequence, long subtableOffset, int subtableFormat) throws IOException {
+        initSESubState();
+        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 ( in, 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 );
+        resetSESubState();
+    }
+
+    private void readGDEFMarkGlyphsTable(FontFileReader in, String tableTag, int lookupSequence, long subtableOffset) throws IOException {
+        in.seekSet(subtableOffset);
+        // read mark set subtable format
+        int sf = in.readTTFUShort();
+        if ( sf == 1 ) {
+            readGDEFMarkGlyphsTableFormat1 ( in, tableTag, lookupSequence, subtableOffset, sf );
+        } else {
+            throw new UnsupportedOperationException ( "unsupported mark glyph sets subtable format: " + sf );
+        }
+    }
+
+    /**
+     * Read the GDEF table.
+     * @param in FontFileReader to read from
+     * @throws IOException In case of a I/O problem
+     */
+    private void readGDEF(FontFileReader in) throws IOException {
+        String tableTag = "GDEF";
+        // Initialize temporary state
+        initSEState();
+        // Read glyph definition (GDEF) table
+        TTFDirTabEntry dirTab = (TTFDirTabEntry)dirTabs.get(tableTag);
+        if ( gdef != null ) {
+            if (log.isDebugEnabled()) {
+                log.debug(tableTag + ": ignoring duplicate table");
+            }
+        } else if (dirTab != null) {
+            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 ( in, tableTag, seqno++, to + cdo );
+            }
+            // (optionally) read glyph attachment point subtable
+            if ( apo != 0 ) {
+                readGDEFAttachmentTable ( in, tableTag, seqno++, to + apo );
+            }
+            // (optionally) read ligature caret subtable
+            if ( lco != 0 ) {
+                readGDEFLigatureCaretTable ( in, tableTag, seqno++, to + lco );
+            }
+            // (optionally) read mark attachment class subtable
+            if ( mao != 0 ) {
+                readGDEFMarkAttachmentTable ( in, tableTag, seqno++, to + mao );
+            }
+            // (optionally) read mark glyph sets subtable
+            if ( mgo != 0 ) {
+                readGDEFMarkGlyphsTable ( in, tableTag, seqno++, to + mgo );
+            }
+            GlyphDefinitionTable gdef;
+            if ( ( gdef = constructGDEF() ) != null ) {
+                this.gdef = gdef;
+            }
+        }
+    }
+
     /**
      * Read the GSUB table.
      * @param in FontFileReader to read from
@@ -2840,6 +5037,23 @@ public class TTFFile {
         }
     }
 
+    /**
+     * 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 );
+            }
+        }
+        resetSEState();
+        return gdef;
+    }
+
     /**
      * Construct the (internal representation of the) GSUB table based on previously
      * parsed state.
@@ -2852,7 +5066,7 @@ public class TTFFile {
             List subtables;
             if ( ( subtables = constructGSUBSubtables() ) != null ) {
                 if ( ( lookups.size() > 0 ) && ( subtables.size() > 0 ) ) {
-                    gsub = new GlyphSubstitutionTable ( lookups, subtables );
+                    gsub = new GlyphSubstitutionTable ( gdef, lookups, subtables );
                 }
             }
         }
@@ -2872,7 +5086,7 @@ public class TTFFile {
             List subtables;
             if ( ( subtables = constructGPOSSubtables() ) != null ) {
                 if ( ( lookups.size() > 0 ) && ( subtables.size() > 0 ) ) {
-                    gpos = new GlyphPositioningTable ( lookups, subtables );
+                    gpos = new GlyphPositioningTable ( gdef, lookups, subtables );
                 }
             }
         }
@@ -2939,6 +5153,42 @@ public class TTFFile {
         return lookups;
     }
 
+    private List constructGDEFSubtables() {
+        List/*<GlyphDefinitionSubtable>*/ subtables = new java.util.ArrayList();
+        if ( seSubtables != null ) {
+            for ( Iterator it = seSubtables.iterator(); it.hasNext();) {
+                Object[] stp = (Object[]) it.next();
+                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];
+        Integer lt = (Integer) stp[1];
+        Integer ln = (Integer) stp[2];
+        Integer lf = (Integer) stp[3];
+        Integer sn = (Integer) stp[4];
+        Integer sf = (Integer) stp[5];
+        GlyphMappingTable mapping = (GlyphMappingTable) stp[6];
+        List entries = (List) stp[7];
+        if ( tt.intValue() == GlyphTable.GLYPH_TABLE_TYPE_DEFINITION ) {
+            int type = GDEFLookupType.getSubtableType ( lt.intValue() );
+            String lid = "lu" + ln.intValue();
+            int sequence = sn.intValue();
+            int flags = lf.intValue();
+            int format = sf.intValue();
+            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 ) {
@@ -2960,26 +5210,57 @@ public class TTFFile {
         Integer lt = (Integer) stp[1];
         Integer ln = (Integer) stp[2];
         Integer lf = (Integer) stp[3];
-        // Integer sn = (Integer) stp[4]; // not used yet
+        Integer sn = (Integer) stp[4];
         Integer sf = (Integer) stp[5];
-        List coverage = (List) stp[6];
+        GlyphCoverageTable coverage = (GlyphCoverageTable) stp[6];
         List entries = (List) stp[7];
         if ( tt.intValue() == GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION ) {
             int type = GSUBLookupType.getSubtableType ( lt.intValue() );
-            String id = "lu" + ln.intValue();
-            int sequence = ln.intValue();
+            String lid = "lu" + ln.intValue();
+            int sequence = sn.intValue();
             int flags = lf.intValue();
             int format = sf.intValue();
-            st = GlyphSubstitutionTable.createSubtable ( type, id, sequence, flags, format, coverage, entries );
+            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 ( Iterator it = seSubtables.iterator(); it.hasNext();) {
+                Object[] stp = (Object[]) it.next();
+                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];
+        Integer lt = (Integer) stp[1];
+        Integer ln = (Integer) stp[2];
+        Integer lf = (Integer) stp[3];
+        Integer sn = (Integer) stp[4];
+        Integer sf = (Integer) stp[5];
+        GlyphCoverageTable coverage = (GlyphCoverageTable) stp[6];
+        List entries = (List) stp[7];
+        if ( tt.intValue() == GlyphTable.GLYPH_TABLE_TYPE_POSITIONING ) {
+            int type = GSUBLookupType.getSubtableType ( lt.intValue() );
+            String lid = "lu" + ln.intValue();
+            int sequence = sn.intValue();
+            int flags = lf.intValue();
+            int format = sf.intValue();
+            st = GlyphPositioningTable.createSubtable ( type, lid, sequence, flags, format, coverage, entries );
+        }
+        return st;
+    }
+
     private void initSEState() {
         seScripts = new java.util.LinkedHashMap();
         seLanguages = new java.util.LinkedHashMap();
@@ -2997,13 +5278,13 @@ public class TTFFile {
     }
 
     private void initSESubState() {
-        seCoverage = new java.util.ArrayList();
+        seMapping = null;
         seEntries = new java.util.ArrayList();
     }
 
     private void extractSESubState ( int tableType, int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, int subtableFormat ) {
-        if ( ( seCoverage != null ) && ( seCoverage.size() > 0 ) ) {
-            if ( ( seEntries != null ) && ( seEntries.size() > 0 ) ) {
+        if ( seEntries != null ) {
+            if ( ( tableType == GlyphTable.GLYPH_TABLE_TYPE_DEFINITION ) || ( seEntries.size() > 0 ) ) {
                 if ( seSubtables != null ) {
                     Integer tt = Integer.valueOf ( tableType );
                     Integer lt = Integer.valueOf ( lookupType );
@@ -3011,14 +5292,14 @@ public class TTFFile {
                     Integer lf = Integer.valueOf ( lookupFlags );
                     Integer sn = Integer.valueOf ( subtableSequence );
                     Integer sf = Integer.valueOf ( subtableFormat );
-                    seSubtables.add ( new Object[] { tt, lt, ln, lf, sn, sf, seCoverage, seEntries } );
+                    seSubtables.add ( new Object[] { tt, lt, ln, lf, sn, sf, seMapping, seEntries } );
                 }
             }
         }
     }
 
     private void resetSESubState() {
-        seCoverage = null;
+        seMapping = null;
         seEntries = null;
     }
 
@@ -3200,9 +5481,8 @@ public class TTFFile {
      *
      * @param glyphIndex
      * @return unicode code point
-     * @throws IOException if glyphIndex not found
      */
-    private Integer glyphToUnicode(int glyphIndex) throws IOException {
+    private Integer glyphToUnicode(int glyphIndex) {
         return (Integer) glyphToUnicodeMap.get(new Integer(glyphIndex));
     }
 
@@ -3211,7 +5491,6 @@ public class TTFFile {
      *
      * @param unicodeIndex unicode code point
      * @return glyph index
-     * @throws IOException if unicodeIndex not found
      */
     private Integer unicodeToGlyph(int unicodeIndex) throws IOException {
         final Integer result
index c11a0e4cfff09c75cd7f565b8b1a9dd6f29f6467..c7b8d9e8b6202a26420fa5569302463c0e6a347c 100644 (file)
@@ -235,6 +235,7 @@ public class TTFFontLoader extends FontLoader {
     private void copyAdvanced ( TTFFile ttf ) {
         if ( returnFont instanceof MultiByteFont ) {
             MultiByteFont mbf = (MultiByteFont) returnFont;
+            mbf.setGDEF ( ttf.getGDEF() );
             mbf.setGSUB ( ttf.getGSUB() );
             mbf.setGPOS ( ttf.getGPOS() );
         }
index 73a1456bfd66718e598872fb39a9d185790b691d..e89730db0669f539141dee0649771e25ead19611 100644 (file)
@@ -86,7 +86,7 @@ public class CharacterLayoutManager extends LeafNodeLayoutManager {
             }
         } else {
             int[] levels = ( level >= 0 ) ? new int[] {level} : null;
-            text.addWord(String.valueOf(ch), 0, null, levels, blockProgressionOffset);
+            text.addWord(String.valueOf(ch), 0, null, levels, null, blockProgressionOffset);
         }
         TraitSetter.setProducerID(text, node.getId());
         TraitSetter.addTextDecoration(text, node.getTextDecoration());
index be0f558f319689e12b4301523905b261a25b8f79..b9e43cfa29e7dd6eabd82129e8f133a0a8ddde3d 100644 (file)
@@ -36,6 +36,7 @@ import org.apache.fop.fo.flow.Character;
 import org.apache.fop.fo.properties.StructurePointerPropertySet;
 import org.apache.fop.fonts.Font;
 import org.apache.fop.fonts.FontSelector;
+import org.apache.fop.fonts.GlyphPositioningTable;
 import org.apache.fop.layoutmgr.InlineKnuthSequence;
 import org.apache.fop.layoutmgr.KnuthBox;
 import org.apache.fop.layoutmgr.KnuthElement;
@@ -82,11 +83,12 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
         private boolean breakOppAfter;
         private final Font font;
         private final int level;
+        private final int[][] gposAdjustments;
 
-        AreaInfo(                                                // CSOK: ParameterNumber
-                int startIndex, int breakIndex, int wordSpaceCount, int letterSpaceCount,
-                MinOptMax areaIPD, boolean isHyphenated, boolean isSpace, boolean breakOppAfter,
-                Font font, int level) {
+        AreaInfo                                                // CSOK: ParameterNumber
+            (int startIndex, int breakIndex, int wordSpaceCount, int letterSpaceCount,
+             MinOptMax areaIPD, boolean isHyphenated, boolean isSpace, boolean breakOppAfter,
+             Font font, int level, int[][] gposAdjustments) {
             assert startIndex <= breakIndex;
             this.startIndex = startIndex;
             this.breakIndex = breakIndex;
@@ -98,6 +100,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
             this.breakOppAfter = breakOppAfter;
             this.font = font;
             this.level = level;
+            this.gposAdjustments = gposAdjustments;
         }
 
         private int getCharLength() {
@@ -154,7 +157,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
      * be used to influence the start position of the first letter. The entry i+1 defines the
      * cursor advancement after the character i. A null entry means no special advancement.
      */
-    private final MinOptMax[] letterAdjustArray; //size = textArray.length + 1
+    private final MinOptMax[] letterSpaceAdjustArray; //size = textArray.length + 1
 
     /** Font used for the space between words. */
     private Font spaceFont = null;
@@ -196,7 +199,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
      */
     public TextLayoutManager(FOText node) {
         foText = node;
-        letterAdjustArray = new MinOptMax[node.length() + 1];
+        letterSpaceAdjustArray = new MinOptMax[node.length() + 1];
         areaInfos = new ArrayList();
     }
 
@@ -318,8 +321,8 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
         }
 
         for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) {
-            MinOptMax letterAdjustment = letterAdjustArray[i + 1];
-            if (letterAdjustment != null && letterAdjustment.isElastic()) {
+            MinOptMax letterSpaceAdjustment = letterSpaceAdjustArray[i + 1];
+            if (letterSpaceAdjustment != null && letterSpaceAdjustment.isElastic()) {
                 letterSpaceCount++;
             }
         }
@@ -405,11 +408,13 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
         private int blockProgressionDimension;
         private AreaInfo areaInfo;
         private StringBuffer wordChars;
-        private int[] letterAdjust;
-        private int letterAdjustIndex;
+        private int[] letterSpaceAdjust;
+        private int letterSpaceAdjustIndex;
         private int[] wordLevels;
         private int wordLevelsCount;
         private int wordIPD;
+        private int[][] gposAdjustments;
+        private int gposAdjustmentsIndex;
 
         private TextArea textArea;
 
@@ -521,6 +526,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
 
         private void addWord(int startIndex, int endIndex, int charLength) {
             int blockProgressionOffset = 0;
+            boolean gposAdjusted = false;
             if (isHyphenated(endIndex)) {
                 charLength++;
             }
@@ -529,20 +535,28 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
                 AreaInfo wordAreaInfo = getAreaInfo(i);
                 addWordChars(wordAreaInfo);
                 addLetterAdjust(wordAreaInfo);
+                if ( addGlyphPositionAdjustments(wordAreaInfo) ) {
+                    gposAdjusted = true;
+                }
             }
             if (isHyphenated(endIndex)) {
                 addHyphenationChar();
             }
-            textArea.addWord(wordChars.toString(), wordIPD, letterAdjust, wordLevels,
-                             blockProgressionOffset);
+            if ( !gposAdjusted ) {
+                gposAdjustments = null;
+            }
+            textArea.addWord(wordChars.toString(), wordIPD, letterSpaceAdjust, wordLevels,
+                             gposAdjustments, blockProgressionOffset);
         }
 
         private void initWord(int charLength) {
             wordChars = new StringBuffer(charLength);
-            letterAdjust = new int[charLength];
-            letterAdjustIndex = 0;
+            letterSpaceAdjust = new int[charLength];
+            letterSpaceAdjustIndex = 0;
             wordLevels = new int[charLength];
             wordLevelsCount = 0;
+            gposAdjustments = new int[charLength][4];
+            gposAdjustmentsIndex = 0;
             wordIPD = 0;
         }
 
@@ -586,16 +600,49 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
         private void addLetterAdjust(AreaInfo wordAreaInfo) {
             int letterSpaceCount = wordAreaInfo.letterSpaceCount;
             for (int i = wordAreaInfo.startIndex; i < wordAreaInfo.breakIndex; i++) {
-                if (letterAdjustIndex > 0) {
-                    MinOptMax adj = letterAdjustArray[i];
-                    letterAdjust[letterAdjustIndex] = adj == null ? 0 : adj.getOpt();
+                if (letterSpaceAdjustIndex > 0) {
+                    MinOptMax adj = letterSpaceAdjustArray[i];
+                    letterSpaceAdjust[letterSpaceAdjustIndex] = adj == null ? 0 : adj.getOpt();
                 }
                 if (letterSpaceCount > 0) {
-                    letterAdjust[letterAdjustIndex] += textArea.getTextLetterSpaceAdjust();
+                    letterSpaceAdjust[letterSpaceAdjustIndex]
+                        += textArea.getTextLetterSpaceAdjust();
                     letterSpaceCount--;
                 }
-                letterAdjustIndex++;
+                letterSpaceAdjustIndex++;
+            }
+        }
+
+        private boolean addGlyphPositionAdjustments(AreaInfo wordAreaInfo) {
+            boolean adjusted = false;
+            int[][] gpa = wordAreaInfo.gposAdjustments;
+            if ( gpa != null ) {
+                // ensure that gposAdjustments is of sufficient length
+                int need = gposAdjustmentsIndex + gpa.length;
+                if ( need > gposAdjustments.length ) {
+                    int[][] gposAdjustmentsNew = new int [ need ][];
+                    System.arraycopy ( gposAdjustments, 0,
+                                       gposAdjustmentsNew, 0, gposAdjustments.length );
+                    for ( int i = gposAdjustments.length; i < need; i++ ) {
+                        gposAdjustmentsNew [ i ] = new int[4];
+                    }
+                    gposAdjustments = gposAdjustmentsNew;
+                }
+                // add gpos adjustments from word area info, incrementing gposAdjustmentsIndex
+                for ( int i = gposAdjustmentsIndex,
+                          n = gposAdjustmentsIndex + gpa.length, j = 0; i < n; i++ ) {
+                    int[] wpa1 = gposAdjustments [ i ];
+                    int[] wpa2 = gpa [ j++ ];
+                    for ( int k = 0; k < 4; k++ ) {
+                        int a = wpa2 [ k ];
+                        if ( a != 0 ) {
+                            wpa1 [ k ] += a; adjusted = true;
+                        }
+                    }
+                    gposAdjustmentsIndex++;
+                }
             }
+            return adjusted;
         }
 
         /**
@@ -663,10 +710,10 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
     }
 
     private void addToLetterAdjust(int index, int width) {
-        if (letterAdjustArray[index] == null) {
-            letterAdjustArray[index] = MinOptMax.getInstance(width);
+        if (letterSpaceAdjustArray[index] == null) {
+            letterSpaceAdjustArray[index] = MinOptMax.getInstance(width);
         } else {
-            letterAdjustArray[index] = letterAdjustArray[index].plus(width);
+            letterSpaceAdjustArray[index] = letterSpaceAdjustArray[index].plus(width);
         }
     }
 
@@ -766,14 +813,14 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
                 // preserved space or non-breaking space:
                 // create the AreaInfo object
                 areaInfo = new AreaInfo(nextStart, nextStart + 1, 1, 0, wordSpaceIPD, false, true,
-                        breakOpportunity, spaceFont, level);
+                                        breakOpportunity, spaceFont, level, null);
                 thisStart = nextStart + 1;
             } else if (CharUtilities.isFixedWidthSpace(ch) || CharUtilities.isZeroWidthSpace(ch)) {
                 // create the AreaInfo object
                 Font font = FontSelector.selectFontForCharacterInText(ch, foText, this);
                 MinOptMax ipd = MinOptMax.getInstance(font.getCharWidth(ch));
                 areaInfo = new AreaInfo(nextStart, nextStart + 1, 0, 0, ipd, false, true,
-                        breakOpportunity, font, level);
+                                        breakOpportunity, font, level, null);
                 thisStart = nextStart + 1;
             } else if (CharUtilities.isExplicitBreak(ch)) {
                 //mandatory break-character: only advance index
@@ -845,7 +892,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
         AreaInfo areaInfo = new AreaInfo
             ( thisStart, nextStart, nextStart - thisStart, 0,
               wordSpaceIPD.mult(nextStart - thisStart),
-              false, true, breakOpportunity, spaceFont, -1 );
+              false, true, breakOpportunity, spaceFont, -1, null );
 
         addAreaInfo(areaInfo);
 
@@ -887,20 +934,39 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
         // perform mapping (of chars to glyphs ... to glyphs ... to chars)
         CharSequence mcs = font.performSubstitution ( ics, script, language );
 
+        // memoize mapping
         foText.addMapping ( s, e, mcs );
 
+        // compute glyph position adjustment on (substituted) characters
+        int[][] gpa;
+        if ( font.performsPositioning() ) {
+            gpa = font.performPositioning ( mcs, script, language );
+        } else {
+            gpa = null;
+        }
+
         MinOptMax ipd = MinOptMax.ZERO;
         for ( int i = 0, n = mcs.length(); i < n; i++ ) {
             char c = mcs.charAt ( i );
             int  w = font.getCharWidth ( c );
+            if ( gpa != null ) {
+                w += gpa [ i ] [ GlyphPositioningTable.Value.IDX_X_ADVANCE ];
+            }
             ipd = ipd.plus ( w );
         }
 
-        // [TBD] - handle kerning
+        // [TBD] - handle kerning - note that standard kerning would only apply in
+        // the off-chance that a font supports substitution, but does not support
+        // positioning and yet has kerning data
+        // if ( ! font.performsPositioning() ) {
+        //   // do standard kerning
+        // }
+
         // [TBD] - handle letter spacing
 
         return new AreaInfo
-            ( s, e, 0, nLS, ipd, endsWithHyphen, false, breakOpportunityChar != 0, font, level );
+            ( s, e, 0, nLS, ipd, endsWithHyphen, false,
+              breakOpportunityChar != 0, font, level, gpa );
     }
 
     private AreaInfo processWordNoMapping(int lastIndex, final Font font, AreaInfo prevAreaInfo,
@@ -963,9 +1029,9 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
 
         // create and return the AreaInfo object
         return new AreaInfo(thisStart, lastIndex, 0,
-                iLetterSpaces, wordIPD,
-                endsWithHyphen,
-                false, breakOpportunityChar != 0, font, level);
+                            iLetterSpaces, wordIPD,
+                            endsWithHyphen,
+                            false, breakOpportunityChar != 0, font, level, null);
     }
 
     private AreaInfo processWord(final int alignment, final KnuthSequence sequence,
@@ -1100,14 +1166,14 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
                 newIPD = newIPD.plus(font.getCharWidth(ch));
                 //if (i > startIndex) {
                 if (i < stopIndex) {
-                    MinOptMax letterAdjust = letterAdjustArray[i + 1];
+                    MinOptMax letterSpaceAdjust = letterSpaceAdjustArray[i + 1];
                     if (i == stopIndex - 1 && hyphenFollows) {
                         //the letter adjust here needs to be handled further down during
                         //element generation because it depends on hyph/no-hyph condition
-                        letterAdjust = null;
+                        letterSpaceAdjust = null;
                     }
-                    if (letterAdjust != null) {
-                        newIPD = newIPD.plus(letterAdjust);
+                    if (letterSpaceAdjust != null) {
+                        newIPD = newIPD.plus(letterSpaceAdjust);
                     }
                 }
             }
@@ -1123,8 +1189,11 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
 
             if (!(nothingChanged && stopIndex == areaInfo.breakIndex && !hyphenFollows)) {
                 // the new AreaInfo object is not equal to the old one
-                changeList.add(new PendingChange(new AreaInfo(startIndex, stopIndex, 0,
-                        letterSpaceCount, newIPD, hyphenFollows, false, false, font, -1),
+                changeList.add
+                    ( new PendingChange
+                      ( new AreaInfo(startIndex, stopIndex, 0,
+                                     letterSpaceCount, newIPD, hyphenFollows,
+                                     false, false, font, -1, null),
                         ((LeafPosition) pos).getLeafPos()));
                 nothingChanged = false;
             }
@@ -1369,7 +1438,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager {
             MinOptMax widthIfNoBreakOccurs = null;
             if (areaInfo.breakIndex < foText.length()) {
                 //Add in kerning in no-break condition
-                widthIfNoBreakOccurs = letterAdjustArray[areaInfo.breakIndex];
+                widthIfNoBreakOccurs = letterSpaceAdjustArray[areaInfo.breakIndex];
             }
             //if (areaInfo.breakIndex)
 
index 6640f9b802d611c5dc106c512e7fc02b91646adb..afa86ae729f629653a6ae90a875c700bff524ba1 100644 (file)
@@ -81,8 +81,8 @@ public abstract class PDFTextUtil {
         sb.append(PDFNumber.doubleOut(lt[5], DEC));
     }
 
-    private void writeChar(char ch, StringBuffer sb) {
-        if (!useMultiByte) {
+    private static void writeChar(char ch, StringBuffer sb, boolean multibyte) {
+        if (!multibyte) {
             if (ch < 32 || ch > 127) {
                 sb.append("\\").append(Integer.toOctalString((int)ch));
             } else {
@@ -101,6 +101,10 @@ public abstract class PDFTextUtil {
         }
     }
 
+    private void writeChar(char ch, StringBuffer sb) {
+        writeChar ( ch, sb, useMultiByte );
+    }
+
     private void checkInTextObject() {
         if (!inTextObject) {
             throw new IllegalStateException("Not in text object");
@@ -260,24 +264,38 @@ public abstract class PDFTextUtil {
             bufTJ = new StringBuffer();
         }
         if (bufTJ.length() == 0) {
-            bufTJ.append("[").append(startText);
+            bufTJ.append("[");
+            bufTJ.append(startText);
         }
         writeChar(codepoint, bufTJ);
     }
 
     /**
      * Writes a glyph adjust value to the "TJ-Buffer".
+
+     * <p>Assumes the following:</p>
+     * <ol>
+     * <li>if buffer is currently empty, then this is the start of the array object
+     * that encodes the adjustment and character values, and, therfore, a LEFT
+     * SQUARE BRACKET '[' must be prepended; and
+     * </li>
+     * <li>otherwise (the buffer is
+     * not empty), then the last element written to the buffer was a mapped
+     * character, and, therefore, a terminating '&gt;' or ')' followed by a space
+     * must be appended to the buffer prior to appending the adjustment value.
+     * </li>
+     * </ol>
      * @param adjust the glyph adjust value in thousands of text unit space.
      */
     public void adjustGlyphTJ(double adjust) {
         if (bufTJ == null) {
             bufTJ = new StringBuffer();
         }
-        if (bufTJ.length() > 0) {
-            bufTJ.append(endText).append(" ");
-        }
         if (bufTJ.length() == 0) {
             bufTJ.append("[");
+        } else {
+            bufTJ.append(endText);
+            bufTJ.append(" ");
         }
         bufTJ.append(PDFNumber.doubleOut(adjust, DEC - 4));
         bufTJ.append(" ");
@@ -300,4 +318,31 @@ public abstract class PDFTextUtil {
         return bufTJ != null && bufTJ.length() > 0;
     }
 
+    /**
+     * Writes a "Td" command with specified x and y coordinates.
+     * @param x coordinate
+     * @param y coordinate
+     */
+    public void writeTd ( double x, double y ) {
+        StringBuffer sb = new StringBuffer();
+        sb.append(PDFNumber.doubleOut(x, DEC));
+        sb.append(' ');
+        sb.append(PDFNumber.doubleOut(y, DEC));
+        sb.append ( " Td\n" );
+        write ( sb.toString() );
+    }
+
+    /**
+     * Writes a "Tj" command with specified character code.
+     * @param ch character code to write
+     */
+    public void writeTj ( char ch ) {
+        StringBuffer sb = new StringBuffer();
+        sb.append ( '<' );
+        writeChar ( ch, sb, true );
+        sb.append ( '>' );
+        sb.append ( " Tj\n" );
+        write ( sb.toString() );
+    }
+
 }
index 0bbfef3e815f3a4fa58e93bcf9d4d013dde63cd3..f5e2d36a7a0e8dfc3eac5f89fd4a01e6d5fbd00d 100644 (file)
@@ -57,6 +57,7 @@ import org.apache.fop.render.intermediate.BorderPainter;
 import org.apache.fop.render.intermediate.IFContext;
 import org.apache.fop.render.intermediate.IFException;
 import org.apache.fop.render.intermediate.IFState;
+import org.apache.fop.render.intermediate.IFUtil;
 import org.apache.fop.traits.BorderProps;
 import org.apache.fop.traits.RuleStyle;
 import org.apache.fop.util.CharUtilities;
@@ -318,8 +319,8 @@ public class AFPPainter extends AbstractIFPainter {
 
     /** {@inheritDoc} */
     public void drawText(                                        // CSOK: MethodLength
-            int x, int y, final int letterSpacing, final int wordSpacing, final int[] dx,
-            final String text) throws IFException {
+            int x, int y, final int letterSpacing, final int wordSpacing,
+            final int[][] dp, final String text) throws IFException {
         final int fontSize = this.state.getFontSize();
         getPaintingState().setFontSize(fontSize);
 
@@ -368,6 +369,7 @@ public class AFPPainter extends AbstractIFPainter {
                     builder.setCodedFont((byte)fontReference);
 
                     int l = text.length();
+                    int[] dx = IFUtil.convertDPToDX ( dp );
                     int dxl = (dx != null ? dx.length : 0);
                     StringBuffer sb = new StringBuffer();
 
index 00fd74209e4297696a6aea4b9422aee2a0176f88..9844e5756494ca2c4ef3c581995a326e4d568a71 100644 (file)
@@ -151,12 +151,14 @@ public interface IFPainter {
      * @param y Y-coordinate of the starting point of the text
      * @param letterSpacing additional spacing between characters (may be 0)
      * @param wordSpacing additional spacing between words (may be 0)
-     * @param dx an array of adjustment values for each character in X-direction (may be null)
+     * @param dp an array of 4-tuples, expressing [X,Y] placment
+     * adjustments and [X,Y] advancement adjustments, in that order (may be null); if
+     * not null, then adjustments.length must be the same as text.length()
      * @param text the text
      * @throws IFException if an error occurs while handling this event
      */
     void drawText(int x, int y, int letterSpacing, int wordSpacing,
-            int[] dx, String text) throws IFException;
+            int[][] dp, String text) throws IFException;
 
     /**
      * Restricts the current clipping region with the given rectangle.
index da5d8a623ec8127c39358700fc7c497bfd6bb20a..739630669423bc722381519ba63ab8e7083ebc9f 100644 (file)
@@ -565,8 +565,14 @@ public class IFParser implements IFConstants {
                 s = lastAttributes.getValue("word-spacing");
                 int wordSpacing = (s != null ? Integer.parseInt(s) : 0);
                 int[] dx = XMLUtil.getAttributeAsIntArray(lastAttributes, "dx");
+                int[][] dp = XMLUtil.getAttributeAsPositionAdjustments(lastAttributes, "dp");
+                // if only DX present, then convert DX to DP; otherwise use only DP,
+                // effectively ignoring DX
+                if ( ( dp == null ) && ( dx != null ) ) {
+                    dp = IFUtil.convertDXToDP ( dx );
+                }
                 setStructurePointer(lastAttributes);
-                painter.drawText(x, y, letterSpacing, wordSpacing, dx, content.toString());
+                painter.drawText(x, y, letterSpacing, wordSpacing, dp, content.toString());
                 resetStructurePointer();
             }
 
index 83a2a4569d711044e89e2011a6bcc9a697c9919b..e625b85a9bab66dcb7dbf6e78a1e60ccaae74f28 100644 (file)
@@ -1029,7 +1029,12 @@ public class IFRenderer extends AbstractPathOrientedRenderer {
         Font font = getFontFromArea(word.getParentArea());
         String s = word.getWord();
 
-        renderText(s, word.getLetterAdjustArray(), word.isReversed(),
+        int[][] dp = word.getGlyphPositionAdjustments();
+        if ( dp == null ) {
+            dp = IFUtil.convertDXToDP ( word.getLetterAdjustArray() );
+        }
+
+        renderText(s, dp, word.isReversed(),
                 font, (AbstractTextArea)word.getParentArea());
 
         super.renderWord(word);
@@ -1054,25 +1059,32 @@ public class IFRenderer extends AbstractPathOrientedRenderer {
         super.renderSpace(space);
     }
 
+    private void renderText(String s,
+                              int[][] dp, boolean reversed,
+                              Font font, AbstractTextArea parentArea) {
+        if ( ( dp == null ) || IFUtil.isDPOnlyDX ( dp ) ) {
+            int[] dx = IFUtil.convertDPToDX ( dp );
+            renderTextWithAdjustments ( s, dx, reversed, font, parentArea );
+        } else {
+            renderTextWithAdjustments ( s, dp, reversed, font, parentArea );
+        }
+    }
+
     /**
-     * Does low-level rendering of text.
+     * Does low-level rendering of text using DX only position adjustments.
      * @param s text to render
-     * @param letterAdjust an array of widths for letter adjustment (may be null)
+     * @param dx an array of widths for letter adjustment (may be null)
      * @param reversed if true then text has been reversed (from logical order)
      * @param font to font in use
      * @param parentArea the parent text area to retrieve certain traits from
      */
-    protected void renderText(String s,
-                              int[] letterAdjust, boolean reversed,
+    private void renderTextWithAdjustments(String s,
+                              int[] dx, boolean reversed,
                               Font font, AbstractTextArea parentArea) {
         int l = s.length();
         if (l == 0) {
             return;
         }
-
-        if (letterAdjust != null) {
-            textUtil.adjust(letterAdjust[0]);
-        }
         for (int i = 0; i < l; i++) {
             char ch = s.charAt(i);
             textUtil.addChar(ch);
@@ -1081,18 +1093,38 @@ public class IFRenderer extends AbstractPathOrientedRenderer {
                 int tls = (i < l - 1 ? parentArea.getTextLetterSpaceAdjust() : 0);
                 glyphAdjust += tls;
             }
-            if (letterAdjust != null && i < l - 1) {
-                glyphAdjust += letterAdjust[i + 1];
+            if (dx != null && i < l) {
+                glyphAdjust += dx[i];
             }
-
             textUtil.adjust(glyphAdjust);
         }
     }
 
+    /**
+     * Does low-level rendering of text using generalized position adjustments.
+     * @param s text to render
+     * @param dp an array of 4-tuples, expressing [X,Y] placment
+     * adjustments and [X,Y] advancement adjustments, in that order (may be null)
+     * @param reversed if true then text has been reversed (from logical order)
+     * @param font to font in use
+     * @param parentArea the parent text area to retrieve certain traits from
+     */
+    private void renderTextWithAdjustments(String s,
+                              int[][] dp, boolean reversed,
+                              Font font, AbstractTextArea parentArea) {
+        assert !textUtil.combined;
+        for ( int i = 0, n = s.length(); i < n; i++ ) {
+            textUtil.addChar ( s.charAt ( i ) );
+            if ( dp != null ) {
+                textUtil.adjust ( dp[i] );
+            }
+        }
+    }
+
     private class TextUtil {
         private static final int INITIAL_BUFFER_SIZE = 16;
-        private int[] dx = new int[INITIAL_BUFFER_SIZE];
-        private int lastDXPos = 0;
+        private int[][] dp = new int[INITIAL_BUFFER_SIZE][4];
+        // private int lastDPPos = 0; // TBD - not yet used
         private final StringBuffer text = new StringBuffer();
         private int startx, starty;
         private int tls, tws;
@@ -1102,25 +1134,41 @@ public class IFRenderer extends AbstractPathOrientedRenderer {
             text.append(ch);
         }
 
-        void adjust(int adjust) {
-            if (adjust != 0) {
+        void adjust(int dx) {
+            adjust ( new int[] {
+                    dx,                         // xPlaAdjust
+                    0,                          // yPlaAdjust
+                    dx,                         // xAdvAdjust
+                    0                           // yAdvAdjust
+                } );
+        }
+
+        void adjust(int[] pa) {
+            if ( !IFUtil.isPAIdentity ( pa ) ) {
                 int idx = text.length();
-                if (idx > dx.length - 1) {
-                    int newSize = Math.max(dx.length, idx + 1) + INITIAL_BUFFER_SIZE;
-                    int[] newDX = new int[newSize];
-                    System.arraycopy(dx, 0, newDX, 0, dx.length);
-                    dx = newDX;
+                if (idx > dp.length - 1) {
+                    int newSize = Math.max(dp.length, idx + 1) + INITIAL_BUFFER_SIZE;
+                    int[][] newDP = new int[newSize][];
+                    // reuse prior PA[0]...PA[dp.length-1]
+                    System.arraycopy(dp, 0, newDP, 0, dp.length);
+                    // populate new PA[dp.length]...PA[newDP.length-1]
+                    for ( int i = dp.length, n = newDP.length; i < n; i++ ) {
+                        newDP[i] = new int[4];
+                    }
+                    dp = newDP;
                 }
-                dx[idx] += adjust;
-                lastDXPos = idx;
+                IFUtil.adjustPA ( dp[idx - 1], pa );
+                // lastDPPos = idx;
             }
         }
 
         void reset() {
             if (text.length() > 0) {
                 text.setLength(0);
-                Arrays.fill(dx, 0);
-                lastDXPos = 0;
+                for ( int i = 0, n = dp.length; i < n; i++ ) {
+                    Arrays.fill(dp[i], 0);
+                }
+                // lastDPPos = 0;
             }
         }
 
@@ -1137,16 +1185,12 @@ public class IFRenderer extends AbstractPathOrientedRenderer {
         void flush() {
             if (text.length() > 0) {
                 try {
-                    int[] effDX = null;
-                    if (lastDXPos > 0) {
-                        int size = lastDXPos + 1;
-                        effDX = new int[size];
-                        System.arraycopy(dx, 0, effDX, 0, size);
-                    }
                     if (combined) {
-                        painter.drawText(startx, starty, 0, 0, effDX, text.toString());
+                        painter.drawText(startx, starty, 0, 0,
+                                         trimAdjustments ( dp, text.length() ), text.toString());
                     } else {
-                        painter.drawText(startx, starty, tls, tws, effDX, text.toString());
+                        painter.drawText(startx, starty, tls, tws,
+                                         trimAdjustments ( dp, text.length() ), text.toString());
                     }
                 } catch (IFException e) {
                     handleIFException(e);
@@ -1154,6 +1198,38 @@ public class IFRenderer extends AbstractPathOrientedRenderer {
                 reset();
             }
         }
+
+        /**
+         * Trim adjustments array <code>dp</code> to be no greater length than
+         * text length, and where trailing all-zero entries are removed.
+         * @param dp a position adjustments array (or null)
+         * @param textLength the length of the associated text
+         * @return either the original value of <code>dp</code> or a copy
+         * of its first N significant adjustment entries, such that N is
+         * no greater than text length, and the last entry has a non-zero
+         * adjustment.
+         */
+        private int[][] trimAdjustments ( int[][] dp, int textLength ) {
+            if ( dp != null ) {
+                int tl = textLength;
+                int pl = dp.length;
+                int i  = ( tl < pl ) ? tl : pl;
+                while ( i > 0 ) {
+                    int[] pa = dp [ i - 1 ];
+                    if ( !IFUtil.isPAIdentity ( pa ) ) {
+                        break;
+                    } else {
+                        i--;
+                    }
+                }
+                if ( i == 0 ) {
+                    dp = null;
+                } else if ( i < pl ) {
+                    dp = IFUtil.copyDP ( dp, 0, i );
+                }
+            }
+            return dp;
+        }
     }
 
     /** {@inheritDoc} */
index edeef976655d8de5d4fc8f9873c516ec3665d3cd..f2832494531575b36da7b03eae30281de0bd6046 100644 (file)
@@ -538,7 +538,7 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler
 
     /** {@inheritDoc} */
     public void drawText(int x, int y, int letterSpacing, int wordSpacing,
-            int[] dx, String text) throws IFException {
+            int[][] dp, String text) throws IFException {
         try {
             AttributesImpl atts = new AttributesImpl();
             addAttribute(atts, "x", Integer.toString(x));
@@ -549,8 +549,17 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler
             if (wordSpacing != 0) {
                 addAttribute(atts, "word-spacing", Integer.toString(wordSpacing));
             }
-            if (dx != null) {
-                addAttribute(atts, "dx", IFUtil.toString(dx));
+            if (dp != null) {
+                if ( IFUtil.isDPIdentity(dp) ) {
+                    // don't add dx or dp attribute
+                } else if ( IFUtil.isDPOnlyDX(dp) ) {
+                    // add dx attribute only
+                    int[] dx = IFUtil.convertDPToDX(dp);
+                    addAttribute(atts, "dx", IFUtil.toString(dx));
+                } else {
+                    // add dp attribute only
+                    addAttribute(atts, "dp", XMLUtil.encodePositionAdjustments(dp));
+                }
             }
             addStructurePointerAttribute(atts);
             handler.startElement(EL_TEXT, atts);
index 1867b02946492124ba7e04b4ec8e237bcda99a51..86991ecb11fdd39d3e0c32c53bd30905017c8792 100644 (file)
@@ -199,4 +199,182 @@ public final class IFUtil {
         return documentHandler.getMimeType();
     }
 
+    /**
+     * Convert the general gpos 'dp' adjustments to the older 'dx' adjustments.
+     * This utility method is used to provide backward compatibility in implementations
+     * of IFPainter that have not yet been upgraded to the general position adjustments format.
+     * @param dp an array of 4-tuples, expressing [X,Y] placment
+     * adjustments and [X,Y] advancement adjustments, in that order (may be null)
+     * @param count if <code>dp</code> is not null, then a count of dp values to convert
+     * @return if <code>dp</code> is not null, then an array of adjustments to the current
+     * x position prior to rendering individual glyphs; otherwise, null
+     */
+    public static int[] convertDPToDX ( int[][] dp, int count ) {
+        int[] dx;
+        if ( dp != null ) {
+            dx = new int [ count ];
+            for ( int i = 0, n = count; i < n; i++ ) {
+                dx [ i ] = dp [ i ] [ 0 ];      // xPlaAdjust[i]
+            }
+        } else {
+            dx = null;
+        }
+        return dx;
+    }
+
+    /**
+     * Convert the general gpos 'dp' adjustments to the older 'dx' adjustments.
+     * This utility method is used to provide backward compatibility in implementations
+     * of IFPainter that have not yet been upgraded to the general position adjustments format.
+     * @param dp an array of 4-tuples, expressing [X,Y] placment
+     * adjustments and [X,Y] advancement adjustments, in that order (may be null)
+     * @return if <code>dp</code> is not null, then an array of adjustments to the current
+     * x position prior to rendering individual glyphs; otherwise, null
+     */
+    public static int[] convertDPToDX ( int[][] dp ) {
+        return convertDPToDX ( dp, ( dp != null ) ? dp.length : 0 );
+    }
+
+    /**
+     * Convert the general gpos 'dp' adjustments to the older 'dx' adjustments.
+     * This utility method is used to provide backward compatibility in implementations
+     * of IFPainter that have not yet been upgraded to the general position adjustments format.
+     * @param dx an array of adjustments to the current x position prior to rendering
+     * individual glyphs or null
+     * @param count if <code>dx</code> is not null, then a count of dx values to convert
+     * @return if <code>dx</code> is not null, then an array of 4-tuples, expressing [X,Y]
+     * placment adjustments and [X,Y] advancement adjustments, in that order; otherwise, null
+     */
+    public static int[][] convertDXToDP ( int[] dx, int count ) {
+        int[][] dp;
+        if ( dx != null ) {
+            dp = new int [ count ] [ 4 ];
+            for ( int i = 0, n = count; i < n; i++ ) {
+                int[] pa = dp [ i ];
+                int   d  = dx [ i ];
+                pa [ 0 ] = d;                   // xPlaAdjust[i]
+                pa [ 2 ] = d;                   // xAdvAdjust[i]
+            }
+        } else {
+            dp = null;
+        }
+        return dp;
+    }
+
+    /**
+     * Convert the general gpos 'dp' adjustments to the older 'dx' adjustments.
+     * This utility method is used to provide backward compatibility in implementations
+     * of IFPainter that have not yet been upgraded to the general position adjustments format.
+     * @param dx an array of adjustments to the current x position prior to rendering
+     * individual glyphs or null
+     * @return if <code>dx</code> is not null, then an array of 4-tuples, expressing [X,Y]
+     * placment adjustments and [X,Y] advancement adjustments, in that order; otherwise, null
+     */
+    public static int[][] convertDXToDP ( int[] dx ) {
+        return convertDXToDP ( dx, ( dx != null ) ? dx.length : 0 );
+    }
+
+    /**
+     * Determine if position adjustment is the identity adjustment, i.e., no non-zero adjustment.
+     * @param pa a 4-tuple, expressing [X,Y] placment and [X,Y] advance adjuustments (may be null)
+     * @return true if <code>dp</code> is null or contains no non-zero adjustment
+     */
+    public static boolean isPAIdentity ( int[] pa ) {
+        if ( pa == null ) {
+            return true;
+        } else {
+            for ( int k = 0; k < 4; k++ ) {
+                if ( pa[k] != 0 ) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    /**
+     * Determine if position adjustments is the identity adjustment, i.e., no non-zero adjustment.
+     * @param dp an array of 4-tuples, expressing [X,Y] placment
+     * adjustments and [X,Y] advancement adjustments, in that order (may be null)
+     * @return true if <code>dp</code> is null or contains no non-zero adjustment
+     */
+    public static boolean isDPIdentity ( int[][] dp ) {
+        if ( dp == null ) {
+            return true;
+        } else {
+            for ( int i = 0, n = dp.length; i < n; i++ ) {
+                if ( !isPAIdentity ( dp[i] ) ) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    /**
+     * Determine if position adjustments comprises only DX adjustments as encoded by
+     * {@link #convertDPToDX}. Note that if given a set of all all zero position
+     * adjustments, both this method and {@link #isDPIdentity} will return true;
+     * however, this method may return true when {@link #isDPIdentity} returns false.
+     * @param dp an array of 4-tuples, expressing [X,Y] placment
+     * adjustments and [X,Y] advancement adjustments, in that order (may be null)
+     * @return true if <code>dp</code> is not null and contains only xPlaAdjust
+     * and xAdvAdjust values consistent with the output of {@link #convertDPToDX}.
+     */
+    public static boolean isDPOnlyDX ( int[][] dp ) {
+        if ( dp == null ) {
+            return false;
+        } else {
+            for ( int i = 0, n = dp.length; i < n; i++ ) {
+                int[] pa = dp[i];
+                if ( pa[0] != pa[2] ) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    /**
+     * Adjust a position adjustments array. If both <code>paDst</code> and <code>paSrc</code> are
+     * non-null, then <code>paSrc[i]</code> is added to <code>paDst[i]</code>.
+     * @param paDst a 4-tuple, expressing [X,Y] placment
+     * and [X,Y] advance adjuustments (may be null)
+     * @param paSrc a 4-tuple, expressing [X,Y] placment
+     * and [X,Y] advance adjuustments (may be null)
+     */
+    public static void adjustPA ( int[] paDst, int[] paSrc ) {
+        if ( ( paDst != null ) && ( paSrc != null ) ) {
+            assert paDst.length == 4; assert paSrc.length == 4;
+            for ( int i = 0; i < 4; i++ ) {
+                paDst[i] += paSrc[i];
+            }
+        }
+    }
+
+    /**
+     * Copy entries from position adjustments.
+     * @param dp an array of 4-tuples, expressing [X,Y] placment
+     * adjustments and [X,Y] advancement adjustments, in that order
+     * @param offset starting offset from which to copy
+     * @param count number of entries to copy
+     * @return a deep copy of the count position adjustment entries start at
+     * offset
+     */
+    public static int[][] copyDP ( int[][] dp, int offset, int count ) {
+        if ( ( dp == null ) || ( offset > dp.length ) || ( ( offset + count ) > dp.length ) ) {
+            throw new IllegalArgumentException();
+        } else {
+            int[][] dpNew = new int [ count ] [ 4 ];
+            for ( int i = 0, n = count; i < n; i++ ) {
+                int[] paDst = dpNew [ i ];
+                int[] paSrc = dp [ i + offset ];
+                for ( int k = 0; k < 4; k++ ) {
+                    paDst [ k ] = paSrc [ k ];
+                }
+            }
+            return dpNew;
+        }
+    }
+
 }
index 39664576829689a5f2faa7f2f92eb5eefa87443a..4acebc621ab03ca9ecc52411a8432c304f3a5968 100644 (file)
@@ -42,6 +42,7 @@ import org.apache.fop.render.intermediate.IFContext;
 import org.apache.fop.render.intermediate.IFException;
 import org.apache.fop.render.intermediate.IFPainter;
 import org.apache.fop.render.intermediate.IFState;
+import org.apache.fop.render.intermediate.IFUtil;
 import org.apache.fop.traits.BorderProps;
 import org.apache.fop.traits.RuleStyle;
 import org.apache.fop.util.CharUtilities;
@@ -203,7 +204,7 @@ public class Java2DPainter extends AbstractIFPainter {
     }
 
     /** {@inheritDoc} */
-    public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[] dx, String text)
+    public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[][] dp, String text)
             throws IFException {
         g2dState.updateColor(state.getTextColor());
         FontTriplet triplet = new FontTriplet(
@@ -220,6 +221,7 @@ public class Java2DPainter extends AbstractIFPainter {
         Point2D cursor = new Point2D.Float(0, 0);
 
         int l = text.length();
+        int[] dx = IFUtil.convertDPToDX ( dp );
         int dxl = (dx != null ? dx.length : 0);
 
         if (dx != null && dxl > 0 && dx[0] != 0) {
index afae8ac27f286f8d488ffe4d9145657e5d97ed7f..59d9ab2b4ffde82a841588a824bc410640a7c8f6 100644 (file)
@@ -51,6 +51,7 @@ import org.apache.fop.render.intermediate.IFContext;
 import org.apache.fop.render.intermediate.IFException;
 import org.apache.fop.render.intermediate.IFPainter;
 import org.apache.fop.render.intermediate.IFState;
+import org.apache.fop.render.intermediate.IFUtil;
 import org.apache.fop.render.java2d.FontMetricsMapper;
 import org.apache.fop.render.java2d.Java2DPainter;
 import org.apache.fop.traits.BorderProps;
@@ -307,7 +308,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants {
     }
 
     /** {@inheritDoc} */
-    public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[] dx, String text)
+    public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[][] dp, String text)
                 throws IFException {
         try {
             FontTriplet triplet = new FontTriplet(
@@ -319,13 +320,13 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants {
                         ? false
                         : HardcodedFonts.setFont(gen, fontKey, state.getFontSize(), text);
             if (pclFont) {
-                drawTextNative(x, y, letterSpacing, wordSpacing, dx, text, triplet);
+                drawTextNative(x, y, letterSpacing, wordSpacing, dp, text, triplet);
             } else {
-                drawTextAsBitmap(x, y, letterSpacing, wordSpacing, dx, text, triplet);
+                drawTextAsBitmap(x, y, letterSpacing, wordSpacing, dp, text, triplet);
                 if (DEBUG) {
                     state.setTextColor(Color.GRAY);
                     HardcodedFonts.setFont(gen, "F1", state.getFontSize(), text);
-                    drawTextNative(x, y, letterSpacing, wordSpacing, dx, text, triplet);
+                    drawTextNative(x, y, letterSpacing, wordSpacing, dp, text, triplet);
                 }
             }
         } catch (IOException ioe) {
@@ -333,7 +334,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants {
         }
     }
 
-    private void drawTextNative(int x, int y, int letterSpacing, int wordSpacing, int[] dx,
+    private void drawTextNative(int x, int y, int letterSpacing, int wordSpacing, int[][] dp,
             String text, FontTriplet triplet) throws IOException {
         Color textColor = state.getTextColor();
         if (textColor != null) {
@@ -347,6 +348,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants {
         float fontSize = state.getFontSize() / 1000f;
         Font font = parent.getFontInfo().getFontInstance(triplet, state.getFontSize());
         int l = text.length();
+        int[] dx = IFUtil.convertDPToDX ( dp );
         int dxl = (dx != null ? dx.length : 0);
 
         StringBuffer sb = new StringBuffer(Math.max(16, l));
@@ -392,7 +394,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants {
 
     private Rectangle getTextBoundingBox(                        // CSOK: ParameterNumber
             int x, int y,
-            int letterSpacing, int wordSpacing, int[] dx,
+            int letterSpacing, int wordSpacing, int[][] dp,
             String text,
             Font font, FontMetricsMapper metrics) {
         int maxAscent = metrics.getMaxAscent(font.getFontSize()) / 1000;
@@ -403,6 +405,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants {
                 0, maxAscent - descent + 2 * safetyMargin);
 
         int l = text.length();
+        int[] dx = IFUtil.convertDPToDX ( dp );
         int dxl = (dx != null ? dx.length : 0);
 
         if (dx != null && dxl > 0 && dx[0] != 0) {
@@ -432,7 +435,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants {
     }
 
     private void drawTextAsBitmap(final int x, final int y,
-            final int letterSpacing, final int wordSpacing, final int[] dx,
+            final int letterSpacing, final int wordSpacing, final int[][] dp,
             final String text, FontTriplet triplet) throws IFException {
         //Use Java2D to paint different fonts via bitmap
         final Font font = parent.getFontInfo().getFontInstance(triplet, state.getFontSize());
@@ -447,7 +450,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants {
         final int baselineOffset = maxAscent + safetyMargin;
 
         final Rectangle boundingBox = getTextBoundingBox(x, y,
-                letterSpacing, wordSpacing, dx, text, font, mapper);
+                letterSpacing, wordSpacing, dp, text, font, mapper);
         final Dimension dim = boundingBox.getSize();
 
         Graphics2DImagePainter painter = new Graphics2DImagePainter() {
@@ -470,7 +473,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants {
                 Java2DPainter painter = new Java2DPainter(g2d,
                         getContext(), parent.getFontInfo(), state);
                 try {
-                    painter.drawText(x, y, letterSpacing, wordSpacing, dx, text);
+                    painter.drawText(x, y, letterSpacing, wordSpacing, dp, text);
                 } catch (IFException e) {
                     //This should never happen with the Java2DPainter
                     throw new RuntimeException("Unexpected error while painting text", e);
index 161b466172962e58c54acf174d1b9d1f04947e72..d17b70838980f54df5e778b9f59508edd936168b 100644 (file)
@@ -44,6 +44,7 @@ import org.apache.fop.render.intermediate.AbstractIFPainter;
 import org.apache.fop.render.intermediate.IFContext;
 import org.apache.fop.render.intermediate.IFException;
 import org.apache.fop.render.intermediate.IFState;
+import org.apache.fop.render.intermediate.IFUtil;
 import org.apache.fop.render.pdf.PDFLogicalStructureHandler.MarkedContentInfo;
 import org.apache.fop.traits.BorderProps;
 import org.apache.fop.traits.RuleStyle;
@@ -286,7 +287,7 @@ public class PDFPainter extends AbstractIFPainter {
     }
 
     /** {@inheritDoc} */
-    public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[] dx,
+    public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[][] dp,
             String text)
             throws IFException {
         if (accessEnabled) {
@@ -304,6 +305,19 @@ public class PDFPainter extends AbstractIFPainter {
 
         FontTriplet triplet = new FontTriplet(
                 state.getFontFamily(), state.getFontStyle(), state.getFontWeight());
+
+        if ( ( dp == null ) || IFUtil.isDPOnlyDX ( dp ) ) {
+            drawTextWithDX ( x, y, text, triplet, letterSpacing,
+                             wordSpacing, IFUtil.convertDPToDX ( dp ) );
+        } else {
+            drawTextWithDP ( x, y, text, triplet, letterSpacing,
+                             wordSpacing, dp );
+        }
+    }
+
+    private void drawTextWithDX ( int x, int y, String text, FontTriplet triplet,
+                                  int letterSpacing, int wordSpacing, int[] dx ) {
+
         //TODO Ignored: state.getFontVariant()
         //TODO Opportunity for font caching if font state is more heavily used
         String fontKey = getFontInfo().getInternalFontKey(triplet);
@@ -377,4 +391,45 @@ public class PDFPainter extends AbstractIFPainter {
         textutil.writeTJ();
     }
 
+    private static int[] paZero = new int[4];
+
+    private void drawTextWithDP ( int x, int y, String text, FontTriplet triplet,
+                                  int letterSpacing, int wordSpacing, int[][] dp ) {
+        assert text != null;
+        assert triplet != null;
+        assert dp != null;
+        assert dp.length == text.length();
+        String          fk              = getFontInfo().getInternalFontKey(triplet);
+        Typeface        tf              = getTypeface(fk);
+        if ( tf.isMultiByte() ) {
+            int         fs              = state.getFontSize();
+            float       fsPoints        = fs / 1000f;
+            Font        f               = getFontInfo().getFontInstance(triplet, fs);
+            // String      fn              = f.getFontName();
+            PDFTextUtil tu              = generator.getTextUtil();
+            double      xc              = 0f;
+            double      yc              = 0f;
+            double      xoLast          = 0f;
+            double      yoLast          = 0f;
+            tu.writeTextMatrix ( new AffineTransform ( 1, 0, 0, -1, x / 1000f, y / 1000f ) );
+            tu.writeTf ( fk, fsPoints );
+            for ( int i = 0, n = text.length(); i < n; i++ ) {
+                char    ch              = text.charAt ( i );
+                int[]   pa              = ( i < dp.length ) ? dp [ i ] : paZero;
+                double  xo              = xc + pa[0];
+                double  yo              = yc + pa[1];
+                double  xa              = f.getCharWidth(ch);
+                double  ya              = 0;
+                double  xd              = ( xo - xoLast ) / 1000f;
+                double  yd              = ( yo - yoLast ) / 1000f;
+                tu.writeTd ( xd, yd );
+                tu.writeTj ( f.mapChar ( ch ) );
+                xc += xa + pa[2];
+                yc += ya + pa[3];
+                xoLast = xo;
+                yoLast = yo;
+            }
+        }
+    }
+
 }
index b8a6d40f659c95c93bc4e6615df8bcbfbf66e6d4..f9351a260590d5b63637ce54d38b434606cc2cc0 100644 (file)
@@ -51,6 +51,7 @@ import org.apache.fop.render.intermediate.AbstractIFPainter;
 import org.apache.fop.render.intermediate.IFContext;
 import org.apache.fop.render.intermediate.IFException;
 import org.apache.fop.render.intermediate.IFState;
+import org.apache.fop.render.intermediate.IFUtil;
 import org.apache.fop.traits.BorderProps;
 import org.apache.fop.traits.RuleStyle;
 import org.apache.fop.util.CharUtilities;
@@ -338,9 +339,8 @@ public class PSPainter extends AbstractIFPainter {
 
     /** {@inheritDoc} */
     public void drawText(int x, int y, int letterSpacing, int wordSpacing,
-            int[] dx, String text) throws IFException {
+            int[][] dp, String text) throws IFException {
         try {
-            //Note: dy is currently ignored
             PSGenerator generator = getGenerator();
             generator.useColor(state.getTextColor());
             beginTextObject();
@@ -379,7 +379,7 @@ public class PSPainter extends AbstractIFPainter {
                     if (currentEncoding != encoding) {
                         if (i > 0) {
                             writeText(text, start, i - start,
-                                    letterSpacing, wordSpacing, dx, font, tf);
+                                    letterSpacing, wordSpacing, dp, font, tf);
                         }
                         if (encoding == 0) {
                             useFont(fontKey, sizeMillipoints);
@@ -391,12 +391,12 @@ public class PSPainter extends AbstractIFPainter {
                     }
                 }
                 writeText(text, start, textLen - start,
-                        letterSpacing, wordSpacing, dx, font, tf);
+                        letterSpacing, wordSpacing, dp, font, tf);
             } else {
                 //Simple single-font painting
                 useFont(fontKey, sizeMillipoints);
                 writeText(text, 0, textLen,
-                        letterSpacing, wordSpacing, dx, font, tf);
+                        letterSpacing, wordSpacing, dp, font, tf);
             }
         } catch (IOException ioe) {
             throw new IFException("I/O error in drawText()", ioe);
@@ -405,7 +405,7 @@ public class PSPainter extends AbstractIFPainter {
 
     private void writeText(                                      // CSOK: ParameterNumber
             String text, int start, int len,
-            int letterSpacing, int wordSpacing, int[] dx,
+            int letterSpacing, int wordSpacing, int[][] dp,
             Font font, Typeface tf) throws IOException {
         PSGenerator generator = getGenerator();
         int end = start + len;
@@ -418,6 +418,7 @@ public class PSPainter extends AbstractIFPainter {
         int lineStart = 0;
         StringBuffer accText = new StringBuffer(initialSize);
         StringBuffer sb = new StringBuffer(initialSize);
+        int[] dx = IFUtil.convertDPToDX ( dp );
         int dxl = (dx != null ? dx.length : 0);
         for (int i = start; i < end; i++) {
             char orgChar = text.charAt(i);
index 09e2f57ef1e38934637e7b93419dc67217a0ab84..c5064d9ee18a968717eb000348e2d4ee6cae5742 100644 (file)
@@ -91,6 +91,7 @@ import org.apache.fop.render.RendererContext;
 import org.apache.fop.render.XMLHandler;
 import org.apache.fop.util.ColorUtil;
 import org.apache.fop.util.DOM2SAX;
+import org.apache.fop.util.XMLUtil;
 
 /**
  * Renderer that renders areas to XML for debugging purposes.
@@ -836,6 +837,7 @@ public class XMLRenderer extends AbstractXMLRenderer {
             }
         }
         maybeAddLevelAttribute(word);
+        maybeAddPositionAdjustAttribute(word);
         if ( word.isReversed() ) {
             addAttribute("reversed", "true");
         }
@@ -919,4 +921,12 @@ public class XMLRenderer extends AbstractXMLRenderer {
         }
     }
 
+    private void maybeAddPositionAdjustAttribute ( WordArea w ) {
+        int[][] adjustments = w.getGlyphPositionAdjustments();
+        if ( adjustments != null ) {
+            addAttribute ( "position-adjust", XMLUtil.encodePositionAdjustments ( adjustments ) );
+        }
+    }
+
+
 }
index 05a0e0d0e13679cbb7d7016289a0fd25d4a4b327..3dfed12d8e3cbf30307b5777147b1734f460c587 100644 (file)
@@ -877,7 +877,15 @@ public class CharUtilities {
             for ( int i = 0; i < s.length(); i++ ) {
                 char c = s.charAt(i);
                 if ( ( c >= 32 ) && ( c < 127 ) ) {
-                    sb.append ( c );
+                    if ( c == '<' ) {
+                        sb.append ( "&lt;" );
+                    } else if ( c == '>' ) {
+                        sb.append ( "&gt;" );
+                    } else if ( c == '&' ) {
+                        sb.append ( "&amp;" );
+                    } else {
+                        sb.append ( c );
+                    }
                 } else {
                     sb.append ( charToNCRef ( c ) );
                 }
@@ -886,6 +894,29 @@ public class CharUtilities {
         return sb.toString();
     }
 
+    private static String padLeft ( String s, int width, char pad ) {
+        StringBuffer sb = new StringBuffer();
+        for ( int i = s.length(); i < width; i++ ) {
+            sb.append(pad);
+        }
+        sb.append ( s );
+        return sb.toString();
+    }
+
+    /**
+     * Format character for debugging output, which it is prefixed with "0x", padded left with '0'
+     * and either 4 or 6 hex characters in width according to whether it is in the BMP or not.
+     * @param c character code
+     * @return formatted character string
+     */
+    public static String format ( int c ) {
+        if ( c < 1114112 ) {
+            return "0x" + padLeft ( Integer.toString ( c, 16 ), ( c < 65536 ) ? 4 : 6, '0' );
+        } else {
+            return "!NOT A CHARACTER!";
+        }
+    }
+
     private static Map scriptTagsMap = null;
 
     private static void putScriptTag ( Map m, int code, String tag ) {
index 0a55ce573f902bf62b7248562e4db032d10b03a5..d068c1490cb4a6f9633e31fae4767c71bc3b7716 100644 (file)
@@ -209,4 +209,126 @@ public final class XMLUtil implements XMLConstants {
         }
     }
 
+    /**
+     * Encode a glyph position adjustments array as a string, where the string value
+     * adheres to the following syntax:
+     *
+     * count ( 'Z' repeat | number )
+     *
+     * where each token is separated by whitespace, except that 'Z' followed by repeat
+     * are considered to be a single token with no intervening whitespace, and where
+     * 'Z' repeat encodes repeated zeroes.
+     * @param dp the adjustments array
+     * @param paCount the number of entries to encode from adjustments array
+     * @return the encoded value
+     */
+    public static String encodePositionAdjustments ( int[][] dp, int paCount ) {
+        assert dp != null;
+        StringBuffer sb = new StringBuffer();
+        int na = paCount;
+        int nz = 0;
+        sb.append ( na );
+        for ( int i = 0; i < na; i++ ) {
+            int[] pa = dp [ i ];
+            for ( int k = 0; k < 4; k++ ) {
+                int a = pa [ k ];
+                if ( a != 0 ) {
+                    encodeNextAdjustment ( sb, nz, a ); nz = 0;
+                } else {
+                    nz++;
+                }
+            }
+        }
+        encodeNextAdjustment ( sb, nz, 0 );
+        return sb.toString();
+    }
+
+    /**
+     * Encode a glyph position adjustments array as a string, where the string value
+     * adheres to the following syntax:
+     *
+     * count ( 'Z' repeat | number )
+     *
+     * where each token is separated by whitespace, except that 'Z' followed by repeat
+     * are considered to be a single token with no intervening whitespace.
+     * @param dp the adjustments array
+     * @return the encoded value
+     */
+    public static String encodePositionAdjustments ( int[][] dp ) {
+        assert dp != null;
+        return encodePositionAdjustments ( dp, dp.length );
+    }
+
+    private static void encodeNextAdjustment ( StringBuffer sb, int nz, int a ) {
+        encodeZeroes ( sb, nz );
+        encodeAdjustment ( sb, a );
+    }
+
+    private static void encodeZeroes ( StringBuffer sb, int nz ) {
+        if ( nz > 0 ) {
+            sb.append ( ' ' );
+            if ( nz == 1 ) {
+                sb.append ( '0' );
+            } else {
+                sb.append ( 'Z' );
+                sb.append ( nz );
+            }
+        }
+    }
+
+    private static void encodeAdjustment ( StringBuffer sb, int a ) {
+        if ( a != 0 ) {
+            sb.append ( ' ' );
+            sb.append ( a );
+        }
+    }
+
+    /**
+     * Decode a string as a glyph position adjustments array, where the string
+     * shall adhere to the syntax specified by {@link #encodePositionAdjustments}.
+     * @param value the encoded value
+     * @return the position adjustments array
+     */
+    public static int[][] decodePositionAdjustments ( String value ) {
+        int[][] dp = null;
+        if ( value != null ) {
+            String[] sa = value.split ( "\\s" );
+            if ( sa != null ) {
+                if ( sa.length > 0 ) {
+                    int na = Integer.parseInt ( sa[0] );
+                    dp = new int [ na ] [ 4 ];
+                    for ( int i = 1, n = sa.length, k = 0; i < n; i++ ) {
+                        String s = sa [ i ];
+                        if ( s.charAt(0) == 'Z' ) {
+                            int nz = Integer.parseInt ( s.substring ( 1 ) );
+                            k += nz;
+                        } else {
+                            dp [ k / 4 ] [ k % 4 ] = Integer.parseInt ( s );
+                            k += 1;
+                        }
+                    }
+                }
+            }
+        }
+        return dp;
+    }
+
+    /**
+     * Returns an attribute value as a glyph position adjustments array. The string value
+     * is expected to be a non-empty sequence of either Z<repeat> or <number>, where the
+     * former encodes a repeat count (of zeroes) and the latter encodes a integer number,
+     * and where each item is separated by whitespace.
+     * @param attributes the Attributes object
+     * @param name the name of the attribute
+     * @return the position adjustments array
+     */
+    public static int[][] getAttributeAsPositionAdjustments(Attributes attributes, String name) {
+        String s = attributes.getValue(name);
+        if (s == null) {
+            return null;
+        } else {
+            return decodePositionAdjustments(s.trim());
+        }
+    }
+
 }
index bcc3a6913e361d07f5ec076683a6ebccb5e66cd7..3eba51097b628b13532c58b2b5ec3e58fe65be2e 100644 (file)
@@ -49,6 +49,7 @@ import org.apache.fop.render.intermediate.IFConstants;
 import org.apache.fop.render.intermediate.IFContext;
 import org.apache.fop.render.intermediate.IFException;
 import org.apache.fop.render.intermediate.IFState;
+import org.apache.fop.render.intermediate.IFUtil;
 import org.apache.fop.traits.BorderProps;
 import org.apache.fop.traits.RuleStyle;
 import org.apache.fop.util.ColorUtil;
@@ -319,7 +320,7 @@ public class SVGPainter extends AbstractIFPainter implements SVGConstants {
 
     /** {@inheritDoc} */
 
-    public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[] dx,
+    public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[][] dp,
             String text) throws IFException {
         try {
             establish(MODE_TEXT);
@@ -333,7 +334,8 @@ public class SVGPainter extends AbstractIFPainter implements SVGConstants {
             if (wordSpacing != 0) {
                 XMLUtil.addAttribute(atts, "word-spacing", SVGUtil.formatMptToPt(wordSpacing));
             }
-            if (dx != null) {
+            if (dp != null) {
+                int[] dx = IFUtil.convertDPToDX(dp);
                 XMLUtil.addAttribute(atts, "dx", SVGUtil.formatMptArrayToPt(dx));
             }
             handler.startElement("text", atts);