/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* $Id$ */ package org.apache.fop.complexscripts.fonts; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fop.complexscripts.scripts.ScriptProcessor; import org.apache.fop.complexscripts.util.CharAssociation; import org.apache.fop.complexscripts.util.GlyphSequence; import org.apache.fop.complexscripts.util.GlyphTester; // CSOFF: LineLengthCheck /** * <p>The <code>GlyphSubstitutionTable</code> class is a glyph table that implements * <code>GlyphSubstitution</code> functionality.</p> * * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p> */ public class GlyphSubstitutionTable extends GlyphTable { /** logging instance */ private static final Log log = LogFactory.getLog(GlyphSubstitutionTable.class); /** 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, Map<String, ScriptProcessor> processors) { super(gdef, lookups, processors); if ((subtables == null) || (subtables.size() == 0)) { throw new AdvancedTypographicTableFormatException("subtables must be non-empty"); } else { for (Object o : subtables) { 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, processors); 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(); int 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(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; } 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 glyph : glyphs) { entries.add(glyph); } 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; int n = entries.size(); int[] glyphs = new int [ n ]; for (Object o : entries) { if (o instanceof Integer) { int gid = (Integer) o; 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(); int ci; if ((ci = getCoverageIndex(gi)) < 0) { return false; } else { int[] ga = getGlyphsForCoverageIndex(ci, gi); if (ga != null) { ss.putGlyphs(ga, 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(); int 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); Collections.addAll(entries, gaa); 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; int n = entries.size(); int[][] gaa = new int [ n ][]; for (Object o : entries) { 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(); int 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]; int 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 CharAssociation[] laa = ss.getAssociations(0, nga); // output ligature glyph and its association ss.putGlyph(go, 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); Collections.addAll(entries, ligatureSets); 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; int n = entries.size(); LigatureSet[] ligatureSets = new LigatureSet [ n ]; for (Object o : entries) { 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(); int 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 (Rule r : ra) { 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(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 (Rule r : ra) { 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); } 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 (Rule r : ra) { 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(); int 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 (Rule r : ra) { 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(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 (Rule r : ra) { 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); } 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 (Rule r : ra) { 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 gc : components) { 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 (Ligature l : ligatures) { 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(); } } }