| /* |
| * 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.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 { |
| |
| /** 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; |
| /** multiple substitution subtable type */ |
| public static final int GSUB_LOOKUP_TYPE_MULTIPLE = 2; |
| /** alternate substitution subtable type */ |
| public static final int GSUB_LOOKUP_TYPE_ALTERNATE = 3; |
| /** ligature substitution subtable type */ |
| public static final int GSUB_LOOKUP_TYPE_LIGATURE = 4; |
| /** 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 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 ( GlyphDefinitionTable gdef, Map lookups, List subtables ) { |
| super ( gdef, lookups ); |
| if ( ( subtables == null ) || ( subtables.size() == 0 ) ) { |
| throw new AdvancedTypographicTableFormatException ( "subtables must be non-empty" ); |
| } else { |
| for ( Iterator it = subtables.iterator(); it.hasNext();) { |
| Object o = it.next(); |
| if ( o instanceof GlyphSubstitutionSubtable ) { |
| addSubtable ( (GlyphSubtable) o ); |
| } else { |
| throw new AdvancedTypographicTableFormatException ( "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; |
| } |
| |
| /** |
| * 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 ( "single".equals ( s ) ) { |
| t = GSUB_LOOKUP_TYPE_SINGLE; |
| } else if ( "multiple".equals ( s ) ) { |
| t = GSUB_LOOKUP_TYPE_MULTIPLE; |
| } else if ( "alternate".equals ( s ) ) { |
| t = GSUB_LOOKUP_TYPE_ALTERNATE; |
| } else if ( "ligature".equals ( s ) ) { |
| t = GSUB_LOOKUP_TYPE_LIGATURE; |
| } 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 ( "reversechainiingcontextualsingle".equals ( s ) ) { |
| t = GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE; |
| } 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 GSUB_LOOKUP_TYPE_SINGLE: |
| tn = "single"; |
| break; |
| case GSUB_LOOKUP_TYPE_MULTIPLE: |
| tn = "multiple"; |
| break; |
| case GSUB_LOOKUP_TYPE_ALTERNATE: |
| tn = "alternate"; |
| break; |
| case GSUB_LOOKUP_TYPE_LIGATURE: |
| tn = "ligature"; |
| break; |
| case GSUB_LOOKUP_TYPE_CONTEXTUAL: |
| tn = "contextual"; |
| break; |
| case GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL: |
| tn = "chainedcontextual"; |
| break; |
| case GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION: |
| tn = "extensionsubstitution"; |
| break; |
| case GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE: |
| tn = "reversechainiingcontextualsingle"; |
| break; |
| default: |
| tn = "unknown"; |
| break; |
| } |
| return tn; |
| } |
| |
| /** |
| * 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 subtable coverage table |
| * @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 GSUB_LOOKUP_TYPE_SINGLE: |
| st = SingleSubtable.create ( id, sequence, flags, format, coverage, entries ); |
| break; |
| case GSUB_LOOKUP_TYPE_MULTIPLE: |
| st = MultipleSubtable.create ( id, sequence, flags, format, coverage, entries ); |
| break; |
| case GSUB_LOOKUP_TYPE_ALTERNATE: |
| st = AlternateSubtable.create ( id, sequence, flags, format, coverage, entries ); |
| break; |
| case GSUB_LOOKUP_TYPE_LIGATURE: |
| st = LigatureSubtable.create ( id, sequence, flags, format, coverage, entries ); |
| break; |
| case GSUB_LOOKUP_TYPE_CONTEXTUAL: |
| st = ContextualSubtable.create ( id, sequence, flags, format, coverage, entries ); |
| break; |
| case GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL: |
| st = ChainedContextualSubtable.create ( id, sequence, flags, format, coverage, entries ); |
| break; |
| case GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE: |
| st = ReverseChainedSingleSubtable.create ( id, sequence, flags, format, coverage, entries ); |
| break; |
| default: |
| break; |
| } |
| return st; |
| } |
| |
| /** |
| * 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 ); |
| } |
| |
| 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 boolean isCompatible ( GlyphSubtable subtable ) { |
| return subtable instanceof SingleSubtable; |
| } |
| /** {@inheritDoc} */ |
| 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; |
| } |
| ss.putGlyph ( go, ss.getAssociation(), Boolean.TRUE ); |
| 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 AdvancedTypographicTableFormatException ( "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 AdvancedTypographicTableFormatException ( "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 ]; |
| } |
| } |
| private void populate ( List entries ) { |
| int i = 0, n = entries.size(); |
| 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 ) ) { |
| glyphs [ i++ ] = gid; |
| } else { |
| throw new AdvancedTypographicTableFormatException ( "illegal glyph index: " + gid ); |
| } |
| } else { |
| throw new AdvancedTypographicTableFormatException ( "illegal entries entry, must be Integer: " + o ); |
| } |
| } |
| assert i == n; |
| 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 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 ), Boolean.TRUE ); |
| ss.consume(1); |
| } |
| 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(); |
| } |
| } |
| } |
| |
| 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 List getEntries() { |
| if ( gsa != null ) { |
| List entries = new ArrayList ( 1 ); |
| entries.add ( gsa ); |
| return entries; |
| } else { |
| return null; |
| } |
| } |
| /** {@inheritDoc} */ |
| 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 AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); |
| } else if ( entries.size() != 1 ) { |
| throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); |
| } else { |
| Object o; |
| if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof int[][] ) ) { |
| throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an int[][], but is: " + ( ( o != null ) ? o.getClass() : null ) ); |
| } else { |
| gsa = (int[][]) o; |
| } |
| } |
| } |
| } |
| |
| 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 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(), Boolean.TRUE ); |
| 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(); |
| } |
| } |
| } |
| |
| 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 List getEntries() { |
| List entries = new ArrayList ( gaa.length ); |
| for ( int i = 0, n = gaa.length; i < n; i++ ) { |
| entries.add ( gaa[i] ); |
| } |
| return entries; |
| } |
| /** {@inheritDoc} */ |
| 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 ]; |
| } |
| } |
| private void populate ( List entries ) { |
| int i = 0, n = entries.size(); |
| int[][] gaa = new int [ n ][]; |
| for ( Iterator it = entries.iterator(); it.hasNext();) { |
| Object o = it.next(); |
| if ( o instanceof int[] ) { |
| gaa [ i++ ] = (int[]) o; |
| } else { |
| throw new AdvancedTypographicTableFormatException ( "illegal entries entry, must be int[]: " + o ); |
| } |
| } |
| assert i == n; |
| 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 ), Boolean.TRUE ); |
| // fetch and output ignored glyphs (if necessary) |
| if ( ngi > 0 ) { |
| ss.putGlyphs ( ss.getIgnoredGlyphs ( 0, ngi ), ss.getIgnoredAssociations ( 0, ngi ), null ); |
| } |
| ss.consume ( nga + ngi ); |
| } |
| } |
| } |
| return true; |
| } |
| } |
| 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 ( glyphs ) ) { |
| int nc = l.getNumComponents(); |
| if ( nc > maxComponents ) { |
| maxComponents = nc; |
| k = i; |
| } |
| } |
| } |
| if ( k >= 0 ) { |
| return la [ k ]; |
| } else { |
| 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 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 AdvancedTypographicTableFormatException ( "illegal ligatures entry, must be LigatureSet: " + o ); |
| } |
| } |
| assert i == n; |
| assert this.ligatureSets == null; |
| this.ligatureSets = ligatureSets; |
| } |
| } |
| |
| 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_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() { |
| 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 AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); |
| } else if ( entries.size() != 1 ) { |
| throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); |
| } else { |
| Object o; |
| if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) { |
| throw new AdvancedTypographicTableFormatException ( "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, 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 AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); |
| } else if ( entries.size() != 3 ) { |
| throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 3 entries" ); |
| } else { |
| Object o; |
| if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphClassTable ) ) { |
| throw new AdvancedTypographicTableFormatException ( "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 AdvancedTypographicTableFormatException ( "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 AdvancedTypographicTableFormatException ( "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 AdvancedTypographicTableFormatException ( "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, 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 AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); |
| } else if ( entries.size() != 1 ) { |
| throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); |
| } else { |
| Object o; |
| if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) { |
| throw new AdvancedTypographicTableFormatException ( "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 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_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 AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); |
| } else if ( entries.size() != 1 ) { |
| throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); |
| } else { |
| Object o; |
| if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) { |
| throw new AdvancedTypographicTableFormatException ( "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 AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); |
| } else if ( entries.size() != 5 ) { |
| throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 5 entries" ); |
| } else { |
| Object o; |
| if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphClassTable ) ) { |
| throw new AdvancedTypographicTableFormatException ( "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 AdvancedTypographicTableFormatException ( "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 AdvancedTypographicTableFormatException ( "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 AdvancedTypographicTableFormatException ( "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 AdvancedTypographicTableFormatException ( "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 AdvancedTypographicTableFormatException ( "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, 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 AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); |
| } else if ( entries.size() != 1 ) { |
| throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); |
| } else { |
| Object o; |
| if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) { |
| throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); |
| } else { |
| rsa = (RuleSet[]) o; |
| } |
| } |
| } |
| } |
| |
| 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_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; |
| } |
| private void populate ( List entries ) { |
| } |
| } |
| |
| /** |
| * The <code>Ligature</code> class implements a ligature lookup result in terms of |
| * a ligature glyph (code) and the <emph>N+1...</emph> components that comprise the ligature, |
| * where the <emph>Nth</emph> component was consumed in the coverage table lookup mapping to |
| * this ligature instance. |
| */ |
| public static class Ligature { |
| |
| private final int ligature; // (resulting) ligature glyph |
| private final int[] components; // component glyph codes (note that first component is implied) |
| |
| /** |
| * Instantiate a ligature. |
| * @param ligature glyph id |
| * @param components sequence of <emph>N+1...</emph> component glyph (or character) identifiers |
| */ |
| public Ligature ( int ligature, int[] components ) { |
| if ( ( ligature < 0 ) || ( ligature > 65535 ) ) { |
| throw new AdvancedTypographicTableFormatException ( "invalid ligature glyph index: " + ligature ); |
| } else if ( components == null ) { |
| throw new AdvancedTypographicTableFormatException ( "invalid ligature components, must be non-null array" ); |
| } else { |
| for ( int i = 0, n = components.length; i < n; i++ ) { |
| int gc = components [ i ]; |
| if ( ( gc < 0 ) || ( gc > 65535 ) ) { |
| throw new AdvancedTypographicTableFormatException ( "invalid component glyph index: " + gc ); |
| } |
| } |
| this.ligature = ligature; |
| this.components = components; |
| } |
| } |
| |
| /** @return ligature glyph id */ |
| public int getLigature() { |
| return ligature; |
| } |
| |
| /** @return array of <emph>N+1...</emph> components */ |
| public int[] getComponents() { |
| return components; |
| } |
| |
| /** @return components count */ |
| public int getNumComponents() { |
| return components.length; |
| } |
| |
| /** |
| * 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 ( int[] glyphs ) { |
| if ( glyphs.length < ( components.length + 1 ) ) { |
| return false; |
| } else { |
| for ( int i = 0, n = components.length; i < n; i++ ) { |
| if ( glyphs [ i + 1 ] != components [ i ] ) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public String toString() { |
| StringBuffer sb = new StringBuffer(); |
| sb.append("{components={"); |
| for ( int i = 0, n = components.length; i < n; i++ ) { |
| if ( i > 0 ) { |
| sb.append(','); |
| } |
| sb.append(Integer.toString(components[i])); |
| } |
| sb.append("},ligature="); |
| sb.append(Integer.toString(ligature)); |
| sb.append("}"); |
| return sb.toString(); |
| } |
| |
| } |
| |
| /** |
| * The <code>LigatureSet</code> class implements a set of ligatures. |
| */ |
| 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. |
| * @param ligatures collection of ligatures |
| */ |
| public LigatureSet ( List ligatures ) { |
| this ( (Ligature[]) ligatures.toArray ( new Ligature [ ligatures.size() ] ) ); |
| } |
| |
| /** |
| * Instantiate a set of ligatures. |
| * @param ligatures array of ligatures |
| */ |
| public LigatureSet ( Ligature[] ligatures ) { |
| if ( ligatures == null ) { |
| throw new AdvancedTypographicTableFormatException ( "invalid ligatures, must be non-null 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; |
| } |
| } |
| |
| /** @return array of ligatures in this ligature set */ |
| public Ligature[] getLigatures() { |
| return ligatures; |
| } |
| |
| /** @return count of ligatures in this ligature set */ |
| public int getNumLigatures() { |
| 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(); |
| sb.append("{ligs={"); |
| for ( int i = 0, n = ligatures.length; i < n; i++ ) { |
| if ( i > 0 ) { |
| sb.append(','); |
| } |
| sb.append(ligatures[i]); |
| } |
| sb.append("}}"); |
| return sb.toString(); |
| } |
| |
| } |
| |
| } |
| |