/* * 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.util; import java.io.File; import java.io.IOException; import java.nio.IntBuffer; import java.util.Arrays; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.TreeMap; import java.util.Vector; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fop.fonts.GlyphClassTable; import org.apache.fop.fonts.GlyphCoverageTable; import org.apache.fop.fonts.GlyphDefinitionSubtable; import org.apache.fop.fonts.GlyphDefinitionTable; import org.apache.fop.fonts.GlyphMappingTable; import org.apache.fop.fonts.GlyphPositioningSubtable; import org.apache.fop.fonts.GlyphPositioningTable; import org.apache.fop.fonts.GlyphPositioningTable.Anchor; import org.apache.fop.fonts.GlyphPositioningTable.MarkAnchor; import org.apache.fop.fonts.GlyphPositioningTable.PairValues; import org.apache.fop.fonts.GlyphPositioningTable.Value; import org.apache.fop.fonts.GlyphSequence; import org.apache.fop.fonts.GlyphSubstitutionSubtable; import org.apache.fop.fonts.GlyphSubstitutionTable; import org.apache.fop.fonts.GlyphSubstitutionTable.Ligature; import org.apache.fop.fonts.GlyphSubstitutionTable.LigatureSet; import org.apache.fop.fonts.GlyphSubtable; import org.apache.fop.fonts.GlyphTable; import org.apache.fop.fonts.GlyphTable.RuleLookup; import org.apache.fop.util.CharUtilities; import org.xml.sax.Attributes; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; // CSOFF: InnerAssignmentCheck // CSOFF: LineLengthCheck // CSOFF: NoWhitespaceAfterCheck /** * This class supports a subset of the TTX file as produced by the Adobe FLEX * SDK (AFDKO). In particular, it is used to parse a TTX file in order to * extract character to glyph code mapping data, glyph definition data, glyph substitution * data, and glyph positioning data. * * TTX files are used in FOP for testing and debugging purposes only. Such * files are used to represent font data employed by complex script processing, and * normally extracted directly from an opentype (or truetype) file. However, due to * copyright restrictions, it is not possible to include most opentype (or truetype) font * files directly in the FOP distribution. In such cases, TTX files are used * to distribute a subset of the complex script advanced table information contained in * certain font files to facilitate testing. * * @author Glenn Adams */ public class TTXFile { /** logging instance */ private static final Log log = LogFactory.getLog(TTXFile.class); // CSOK: ConstantNameCheck /** default script tag */ private static final String DEFAULT_SCRIPT_TAG = "dflt"; /** default language tag */ private static final String DEFAULT_LANGUAGE_TAG = "dflt"; /** ttxfile cache */ private static Map cache = new HashMap(); // transient parsing state private Locator locator; // current document locator private Stack elements; // stack of ttx elements being parsed private Map glyphIds; // map of glyph names to glyph identifiers private List cmapEntries; // list of pairs private Vector hmtxEntries; // vector of pairs private Map glyphClasses; // map of glyph names to glyph classes private Map>> scripts; // map of script tag to Map>> private Map> languages; // map of language tag to List private Map features; // map of feature id to Object[2] : { feature-tag, List } private List languageFeatures; // list of language system feature ids, where first is (possibly null) required feature id private List featureLookups; // list of lookup ids for feature being constructed private List coverageEntries; // list of entries for coverage table being constructed private Map coverages; // map of coverage table keys to coverage tables private List subtableEntries; // list of lookup subtable entries private List subtables; // list of constructed subtables private List alternates; // list of alternates in alternate set being constructed private List ligatures; // list of ligatures in ligature set being constructed private List substitutes; // list of substitutes in (multiple substitution) sequence being constructed private List pairs; // list of pair value records being constructed private List pairSets; // list of pair value sets (as arrays) being constructed private List anchors; // list of anchors of base|mark|component record being constructed private List components; // list of ligature component anchors being constructed private List markAnchors; // list of mark anchors being constructed private List baseOrMarkAnchors; // list of base|mark2 anchors being constructed private List ligatureAnchors; // list of ligature anchors being constructed private List attachmentAnchors; // list of entry|exit attachment anchors being constructed private List ruleLookups; // list of rule lookups being constructed private int glyphIdMax; // maximum glyph id private int cmPlatform; // plaform id of cmap being constructed private int cmEncoding; // plaform id of cmap being constructed private int cmLanguage; // plaform id of cmap being constructed private int flIndex; // index of feature being constructed private int flSequence; // feature sequence within feature list private int ltIndex; // index of lookup table being constructed private int ltSequence; // lookup sequence within table private int ltFlags; // flags of current lookup being constructed private int stSequence; // subtable sequence number within lookup private int stFormat; // format of current subtable being constructed private int ctFormat; // format of coverage table being constructed private int ctIndex; // index of coverage table being constructed private int rlSequence; // rule lookup sequence index private int rlLookup; // rule lookup lookup index private int psIndex; // pair set index private int vf1; // value format 1 (used with pair pos and single pos) private int vf2; // value format 2 (used with pair pos) private int g2; // glyph id 2 (used with pair pos) private int xCoord; // x coordinate of anchor being constructed private int yCoord; // y coordinate of anchor being constructed private int markClass; // mark class of mark anchor being constructed private String defaultScriptTag; // tag of default script private String scriptTag; // tag of script being constructed private String defaultLanguageTag; // tag of default language system private String languageTag; // tag of language system being constructed private String featureTag; // tag of feature being constructed private Value v1; // positioining value 1 private Value v2; // positioining value 2 // resultant state private int upem; // units per em private Map cmap; // constructed character map private Map gmap; // constructed glyph map private int[][] hmtx; // constructed horizontal metrics - array of design { width, lsb } pairs, indexed by glyph code private int[] widths; // pdf normalized widths (millipoints) private GlyphDefinitionTable gdef; // constructed glyph definition table private GlyphSubstitutionTable gsub; // constructed glyph substitution table private GlyphPositioningTable gpos; // constructed glyph positioning table public TTXFile() { elements = new Stack(); glyphIds = new HashMap(); cmapEntries = new ArrayList(); hmtxEntries = new Vector(); glyphClasses = new HashMap(); scripts = new HashMap>>(); languages = new HashMap>(); features = new HashMap(); languageFeatures = new ArrayList(); featureLookups = new ArrayList(); coverageEntries = new ArrayList(); coverages = new HashMap(); subtableEntries = new ArrayList(); subtables = new ArrayList(); alternates = new ArrayList(); ligatures = new ArrayList(); substitutes = new ArrayList(); pairs = new ArrayList(); pairSets = new ArrayList(); anchors = new ArrayList(); markAnchors = new ArrayList(); baseOrMarkAnchors = new ArrayList(); ligatureAnchors = new ArrayList(); components = new ArrayList(); attachmentAnchors = new ArrayList(); ruleLookups = new ArrayList(); glyphIdMax = -1; cmPlatform = -1; cmEncoding = -1; cmLanguage = -1; flIndex = -1; flSequence = 0; ltIndex = -1; ltSequence = 0; ltFlags = 0; stSequence = 0; stFormat = 0; ctFormat = -1; ctIndex = -1; rlSequence = -1; rlLookup = -1; psIndex = -1; vf1 = -1; vf2 = -1; g2 = -1; xCoord = Integer.MIN_VALUE; yCoord = Integer.MIN_VALUE; markClass = -1; defaultScriptTag = DEFAULT_SCRIPT_TAG; scriptTag = null; defaultLanguageTag = DEFAULT_LANGUAGE_TAG; languageTag = null; featureTag = null; v1 = null; v2 = null; upem = -1; } public void parse ( String filename ) { parse ( new File ( filename ) ); } public void parse ( File f ) { assert f != null; try { SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser sp = spf.newSAXParser(); sp.parse ( f, new Handler() ); } catch ( FactoryConfigurationError e ) { throw new RuntimeException ( e.getMessage() ); } catch ( ParserConfigurationException e ) { throw new RuntimeException ( e.getMessage() ); } catch ( SAXException e ) { throw new RuntimeException ( e.getMessage() ); } catch ( IOException e ) { throw new RuntimeException ( e.getMessage() ); } } public GlyphSequence mapCharsToGlyphs ( String s ) { Integer[] ca = CharUtilities.toUTF32 ( s, 0, true ); int ng = ca.length; IntBuffer cb = IntBuffer.allocate ( ng ); IntBuffer gb = IntBuffer.allocate ( ng ); for ( Integer c : ca ) { int g = mapCharToGlyph ( (int) c ); if ( g >= 0 ) { cb.put ( c ); gb.put ( g ); } else { throw new IllegalArgumentException ( "character " + CharUtilities.format ( c ) + " has no corresponding glyph" ); } } cb.rewind(); gb.rewind(); return new GlyphSequence ( cb, gb, null ); } public int mapCharToGlyph ( int c ) { if ( cmap != null ) { Integer g = cmap.get ( Integer.valueOf ( c ) ); if ( g != null ) { return (int) g; } else { return -1; } } else { return -1; } } public int getGlyph ( String gid ) { return mapGlyphId0 ( gid ); } public GlyphSequence getGlyphSequence ( String[] gids ) { assert gids != null; int ng = gids.length; IntBuffer cb = IntBuffer.allocate ( ng ); IntBuffer gb = IntBuffer.allocate ( ng ); for ( String gid : gids ) { int g = mapGlyphId0 ( gid ); if ( g >= 0 ) { int c = mapGlyphIdToChar ( gid ); if ( c < 0 ) { c = CharUtilities.NOT_A_CHARACTER; } cb.put ( c ); gb.put ( g ); } else { throw new IllegalArgumentException ( "unmapped glyph id \"" + gid + "\"" ); } } cb.rewind(); gb.rewind(); return new GlyphSequence ( cb, gb, null ); } public int[] getWidths ( String[] gids ) { assert gids != null; int ng = gids.length; int[] widths = new int [ ng ]; int i = 0; for ( String gid : gids ) { int g = mapGlyphId0 ( gid ); int w = 0; if ( g >= 0 ) { if ( ( hmtx != null ) && ( g < hmtx.length ) ) { int[] mtx = hmtx [ g ]; assert mtx != null; assert mtx.length > 0; w = mtx[0]; } } widths [ i++ ] = w; } assert i == ng; return widths; } public int[] getWidths() { if ( this.widths == null ) { if ( ( hmtx != null ) && ( upem > 0 ) ) { int[] widths = new int [ hmtx.length ]; for ( int i = 0, n = widths.length; i < n; i++ ) { widths [ i ] = getPDFWidth ( hmtx [ i ] [ 0 ], upem ); } this.widths = widths; } } return this.widths; } public static int getPDFWidth ( int tw, int upem ) { // N.B. The following is copied (with minor edits) from TTFFile to insure same results int pw; if ( tw < 0 ) { long rest1 = tw % upem; long storrest = 1000 * rest1; long ledd2 = ( storrest != 0 ) ? ( rest1 / storrest ) : 0; pw = - ( ( -1000 * tw ) / upem - (int) ledd2 ); } else { pw = ( tw / upem ) * 1000 + ( ( tw % upem ) * 1000 ) / upem; } return pw; } public GlyphDefinitionTable getGDEF() { return gdef; } public GlyphSubstitutionTable getGSUB() { return gsub; } public GlyphPositioningTable getGPOS() { return gpos; } public static synchronized TTXFile getFromCache ( String filename ) { assert cache != null; TTXFile f; if ( ( f = (TTXFile) cache.get ( filename ) ) == null ) { f = new TTXFile(); f.parse ( filename ); cache.put ( filename, f ); } return f; } public static synchronized void clearCache() { cache.clear(); } private class Handler extends DefaultHandler { private Handler() { } @Override public void startDocument() { } @Override public void endDocument() { } @Override public void setDocumentLocator ( Locator locator ) { TTXFile.this.locator = locator; } @Override public void startElement ( String uri, String localName, String qName, Attributes attrs ) throws SAXException { String[] en = makeExpandedName ( uri, localName, qName ); if ( en[0] != null ) { unsupportedElement ( en ); } else if ( en[1].equals ( "Alternate" ) ) { String[] pn = new String[] { null, "AlternateSet" }; if ( isParent ( pn ) ) { String glyph = attrs.getValue ( "glyph" ); if ( glyph == null ) { missingRequiredAttribute ( en, "glyph" ); } int gid = mapGlyphId ( glyph, en ); alternates.add ( Integer.valueOf ( gid ) ); } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "AlternateSet" ) ) { String[] pn = new String[] { null, "AlternateSubst" }; if ( isParent ( pn ) ) { String glyph = attrs.getValue ( "glyph" ); if ( glyph == null ) { missingRequiredAttribute ( en, "glyph" ); } int gid = mapGlyphId ( glyph, en ); coverageEntries.add ( Integer.valueOf ( gid ) ); } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "AlternateSubst" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: break; default: unsupportedFormat ( en, sf ); break; } } assertCoverageClear(); ctIndex = 0; ctFormat = 1; assertSubtableClear(); assert sf >= 0; stFormat = sf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "BacktrackCoverage" ) ) { String[] pn1 = new String[] { null, "ChainContextSubst" }; String[] pn2 = new String[] { null, "ChainContextPos" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( isParent ( pnx ) ) { String index = attrs.getValue ( "index" ); int ci = -1; if ( index == null ) { missingRequiredAttribute ( en, "index" ); } else { ci = Integer.parseInt ( index ); } String format = attrs.getValue ( "Format" ); int cf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { cf = Integer.parseInt ( format ); switch ( cf ) { case 1: case 2: break; default: unsupportedFormat ( en, cf ); break; } } assertCoverageClear(); ctIndex = ci; ctFormat = cf; } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "BaseAnchor" ) ) { String[] pn = new String[] { null, "BaseRecord" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } assert xCoord == Integer.MIN_VALUE; assert yCoord == Integer.MIN_VALUE; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "BaseArray" ) ) { String[] pn = new String[] { null, "MarkBasePos" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "BaseCoverage" ) ) { String[] pn = new String[] { null, "MarkBasePos" }; if ( isParent ( pn ) ) { String format = attrs.getValue ( "Format" ); int cf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { cf = Integer.parseInt ( format ); switch ( cf ) { case 1: case 2: break; default: unsupportedFormat ( en, cf ); break; } } assertCoverageClear(); ctIndex = 0; ctFormat = cf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "BaseRecord" ) ) { String[] pn = new String[] { null, "BaseArray" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "ChainContextPos" ) || en[1].equals ( "ChainContextSubst" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: case 2: case 3: break; default: unsupportedFormat ( en, sf ); break; } } assertSubtableClear(); assert sf >= 0; stFormat = sf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Class" ) ) { String[] pn = new String[] { null, "MarkRecord" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); int v = -1; if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { v = Integer.parseInt ( value ); } assert markClass == -1; markClass = v; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "ClassDef" ) ) { String[] pn1 = new String[] { null, "GlyphClassDef" }; String[] pn2 = new String[] { null, "MarkAttachClassDef" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( isParent ( pnx ) ) { String glyph = attrs.getValue ( "glyph" ); if ( glyph == null ) { missingRequiredAttribute ( en, "glyph" ); } String glyphClass = attrs.getValue ( "class" ); if ( glyphClass == null ) { missingRequiredAttribute ( en, "class" ); } if ( ! glyphIds.containsKey ( glyph ) ) { unsupportedGlyph ( en, glyph ); } else if ( isParent ( pn1 ) ) { if ( glyphClasses.containsKey ( glyph ) ) { duplicateGlyphClass ( en, glyph, glyphClass ); } else { glyphClasses.put ( glyph, Integer.parseInt(glyphClass) ); } } else if ( isParent ( pn2 ) ) { if ( glyphClasses.containsKey ( glyph ) ) { duplicateGlyphClass ( en, glyph, glyphClass ); } else { glyphClasses.put ( glyph, Integer.parseInt(glyphClass) ); } } } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "ComponentRecord" ) ) { String[] pn = new String[] { null, "LigatureAttach" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } assert anchors.size() == 0; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Coverage" ) ) { String[] pn1 = new String[] { null, "CursivePos" }; String[] pn2 = new String[] { null, "LigCaretList" }; String[] pn3 = new String[] { null, "MultipleSubst" }; String[] pn4 = new String[] { null, "PairPos" }; String[] pn5 = new String[] { null, "SinglePos" }; String[][] pnx = new String[][] { pn1, pn2, pn3, pn4, pn5 }; if ( isParent ( pnx ) ) { String format = attrs.getValue ( "Format" ); int cf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { cf = Integer.parseInt ( format ); switch ( cf ) { case 1: case 2: break; default: unsupportedFormat ( en, cf ); break; } } assertCoverageClear(); ctIndex = 0; ctFormat = cf; } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "CursivePos" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: break; default: unsupportedFormat ( en, sf ); break; } } assertSubtableClear(); assert sf >= 0; stFormat = sf; assert attachmentAnchors.size() == 0; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "DefaultLangSys" ) ) { String[] pn = new String[] { null, "Script" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } else { assertLanguageFeaturesClear(); assert languageTag == null; languageTag = defaultLanguageTag; } } else if ( en[1].equals ( "EntryAnchor" ) ) { String[] pn = new String[] { null, "EntryExitRecord" }; if ( isParent ( pn ) ) { String format = attrs.getValue ( "Format" ); if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } assert xCoord == Integer.MIN_VALUE; assert yCoord == Integer.MIN_VALUE; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "EntryExitRecord" ) ) { String[] pn = new String[] { null, "CursivePos" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "ExitAnchor" ) ) { String[] pn = new String[] { null, "EntryExitRecord" }; if ( isParent ( pn ) ) { String format = attrs.getValue ( "Format" ); if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } assert xCoord == Integer.MIN_VALUE; assert yCoord == Integer.MIN_VALUE; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Feature" ) ) { String[] pn = new String[] { null, "FeatureRecord" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } else { assertFeatureLookupsClear(); } } else if ( en[1].equals ( "FeatureIndex" ) ) { String[] pn1 = new String[] { null, "DefaultLangSys" }; String[] pn2 = new String[] { null, "LangSys" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( isParent ( pnx ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String value = attrs.getValue ( "value" ); int v = -1; if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { v = Integer.parseInt ( value ); } if ( languageFeatures.size() == 0 ) { languageFeatures.add ( null ); } if ( ( v >= 0 ) && ( v < 65535 ) ) { languageFeatures.add ( makeFeatureId ( v ) ); } } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "FeatureList" ) ) { String[] pn1 = new String[] { null, "GSUB" }; String[] pn2 = new String[] { null, "GPOS" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( ! isParent ( pnx ) ) { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "FeatureRecord" ) ) { String[] pn = new String[] { null, "FeatureList" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); int fi = -1; if ( index == null ) { missingRequiredAttribute ( en, "index" ); } else { fi = Integer.parseInt ( index ); } assertFeatureClear(); assert flIndex == -1; flIndex = fi; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "FeatureTag" ) ) { String[] pn = new String[] { null, "FeatureRecord" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { assert featureTag == null; featureTag = value; } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "GDEF" ) ) { String[] pn = new String[] { null, "ttFont" }; if ( isParent ( pn ) ) { assertSubtablesClear(); } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "GPOS" ) ) { String[] pn = new String[] { null, "ttFont" }; if ( isParent ( pn ) ) { assertCoveragesClear(); assertSubtablesClear(); } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "GSUB" ) ) { String[] pn = new String[] { null, "ttFont" }; if ( isParent ( pn ) ) { assertCoveragesClear(); assertSubtablesClear(); } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Glyph" ) ) { String[] pn1 = new String[] { null, "Coverage" }; String[] pn2 = new String[] { null, "InputCoverage" }; String[] pn3 = new String[] { null, "LookAheadCoverage" }; String[] pn4 = new String[] { null, "BacktrackCoverage" }; String[] pn5 = new String[] { null, "MarkCoverage" }; String[] pn6 = new String[] { null, "Mark1Coverage" }; String[] pn7 = new String[] { null, "Mark2Coverage" }; String[] pn8 = new String[] { null, "BaseCoverage" }; String[] pn9 = new String[] { null, "LigatureCoverage" }; String[][] pnx = new String[][] { pn1, pn2, pn3, pn4, pn5, pn6, pn7, pn8, pn9 }; if ( isParent ( pnx ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { int gid = mapGlyphId ( value, en ); coverageEntries.add ( Integer.valueOf ( gid ) ); } } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "GlyphClassDef" ) ) { String[] pn = new String[] { null, "GDEF" }; if ( isParent ( pn ) ) { String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: case 2: break; default: unsupportedFormat ( en, sf ); break; } } assertSubtableClear(); assert sf >= 0; // force format 1 since TTX always writes entries as non-range entries if ( sf != 1 ) { sf = 1; } stFormat = sf; assert glyphClasses.isEmpty(); } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "GlyphID" ) ) { String[] pn = new String[] { null, "GlyphOrder" }; if ( isParent ( pn ) ) { String id = attrs.getValue ( "id" ); int gid = -1; if ( id == null ) { missingRequiredAttribute ( en, "id" ); } else { gid = Integer.parseInt ( id ); } String name = attrs.getValue ( "name" ); if ( name == null ) { missingRequiredAttribute ( en, "name" ); } if ( glyphIds.containsKey ( name ) ) { duplicateGlyph ( en, name, gid ); } else { if ( gid > glyphIdMax ) { glyphIdMax = gid; } glyphIds.put ( name, gid ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "GlyphOrder" ) ) { String[] pn = new String[] { null, "ttFont" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "InputCoverage" ) ) { String[] pn1 = new String[] { null, "ChainContextSubst" }; String[] pn2 = new String[] { null, "ChainContextPos" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( isParent ( pnx ) ) { String index = attrs.getValue ( "index" ); int ci = -1; if ( index == null ) { missingRequiredAttribute ( en, "index" ); } else { ci = Integer.parseInt ( index ); } String format = attrs.getValue ( "Format" ); int cf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { cf = Integer.parseInt ( format ); switch ( cf ) { case 1: case 2: break; default: unsupportedFormat ( en, cf ); break; } } assertCoverageClear(); ctIndex = ci; ctFormat = cf; } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "LangSys" ) ) { String[] pn = new String[] { null, "LangSysRecord" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } else { assertLanguageFeaturesClear(); } } else if ( en[1].equals ( "LangSysRecord" ) ) { String[] pn = new String[] { null, "Script" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "LangSysTag" ) ) { String[] pn = new String[] { null, "LangSysRecord" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { assert languageTag == null; languageTag = value; } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "LigCaretList" ) ) { String[] pn = new String[] { null, "GDEF" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Ligature" ) ) { String[] pn = new String[] { null, "LigatureSet" }; if ( isParent ( pn ) ) { String components = attrs.getValue ( "components" ); if ( components == null ) { missingRequiredAttribute ( en, "components" ); } int[] cids = mapGlyphIds ( components, en ); String glyph = attrs.getValue ( "glyph" ); if ( glyph == null ) { missingRequiredAttribute ( en, "glyph" ); } int gid = mapGlyphId ( glyph, en ); ligatures.add ( new Ligature ( gid, cids ) ); } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "LigatureAnchor" ) ) { String[] pn = new String[] { null, "ComponentRecord" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } assert xCoord == Integer.MIN_VALUE; assert yCoord == Integer.MIN_VALUE; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "LigatureArray" ) ) { String[] pn = new String[] { null, "MarkLigPos" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "LigatureAttach" ) ) { String[] pn = new String[] { null, "LigatureArray" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } assert components.size() == 0; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "LigatureCoverage" ) ) { String[] pn = new String[] { null, "MarkLigPos" }; if ( isParent ( pn ) ) { String format = attrs.getValue ( "Format" ); int cf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { cf = Integer.parseInt ( format ); switch ( cf ) { case 1: case 2: break; default: unsupportedFormat ( en, cf ); break; } } assertCoverageClear(); ctIndex = 0; ctFormat = cf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "LigatureSet" ) ) { String[] pn = new String[] { null, "LigatureSubst" }; if ( isParent ( pn ) ) { String glyph = attrs.getValue ( "glyph" ); if ( glyph == null ) { missingRequiredAttribute ( en, "glyph" ); } int gid = mapGlyphId ( glyph, en ); coverageEntries.add ( Integer.valueOf ( gid ) ); } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "LigatureSubst" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: break; default: unsupportedFormat ( en, sf ); break; } } assertCoverageClear(); ctIndex = 0; ctFormat = 1; assertSubtableClear(); assert sf >= 0; stFormat = sf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "LookAheadCoverage" ) ) { String[] pn1 = new String[] { null, "ChainContextSubst" }; String[] pn2 = new String[] { null, "ChainContextPos" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( isParent ( pnx ) ) { String index = attrs.getValue ( "index" ); int ci = -1; if ( index == null ) { missingRequiredAttribute ( en, "index" ); } else { ci = Integer.parseInt ( index ); } String format = attrs.getValue ( "Format" ); int cf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { cf = Integer.parseInt ( format ); switch ( cf ) { case 1: case 2: break; default: unsupportedFormat ( en, cf ); break; } } assertCoverageClear(); ctIndex = ci; ctFormat = cf; } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "Lookup" ) ) { String[] pn = new String[] { null, "LookupList" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); int li = -1; if ( index == null ) { missingRequiredAttribute ( en, "index" ); } else { li = Integer.parseInt ( index ); } assertLookupClear(); assert ltIndex == -1; ltIndex = li; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "LookupFlag" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); int lf = 0; if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { lf = Integer.parseInt ( value ); } assert ltFlags == 0; ltFlags = lf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "LookupList" ) ) { String[] pn1 = new String[] { null, "GSUB" }; String[] pn2 = new String[] { null, "GPOS" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( ! isParent ( pnx ) ) { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "LookupListIndex" ) ) { String[] pn1 = new String[] { null, "Feature" }; String[] pn2 = new String[] { null, "SubstLookupRecord" }; String[] pn3 = new String[] { null, "PosLookupRecord" }; String[][] pnx = new String[][] { pn1, pn2, pn3 }; if ( isParent ( pnx ) ) { String index = attrs.getValue ( "index" ); String value = attrs.getValue ( "value" ); int v = -1; if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { v = Integer.parseInt ( value ); } String[][] pny = new String[][] { pn2, pn3 }; if ( isParent ( pny ) ) { assert rlLookup == -1; assert v != -1; rlLookup = v; } else { featureLookups.add ( makeLookupId ( v ) ); } } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "LookupType" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Mark1Array" ) ) { String[] pn = new String[] { null, "MarkMarkPos" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Mark1Coverage" ) ) { String[] pn = new String[] { null, "MarkMarkPos" }; if ( isParent ( pn ) ) { String format = attrs.getValue ( "Format" ); int cf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { cf = Integer.parseInt ( format ); switch ( cf ) { case 1: case 2: break; default: unsupportedFormat ( en, cf ); break; } } assertCoverageClear(); ctIndex = 0; ctFormat = cf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Mark2Anchor" ) ) { String[] pn = new String[] { null, "Mark2Record" }; if ( isParent ( pn ) ) { String format = attrs.getValue ( "Format" ); if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } assert xCoord == Integer.MIN_VALUE; assert yCoord == Integer.MIN_VALUE; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Mark2Array" ) ) { String[] pn = new String[] { null, "MarkMarkPos" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Mark2Coverage" ) ) { String[] pn = new String[] { null, "MarkMarkPos" }; if ( isParent ( pn ) ) { String format = attrs.getValue ( "Format" ); int cf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { cf = Integer.parseInt ( format ); switch ( cf ) { case 1: case 2: break; default: unsupportedFormat ( en, cf ); break; } } assertCoverageClear(); ctIndex = 0; ctFormat = cf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Mark2Record" ) ) { String[] pn = new String[] { null, "Mark2Array" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "MarkAnchor" ) ) { String[] pn = new String[] { null, "MarkRecord" }; if ( isParent ( pn ) ) { String format = attrs.getValue ( "Format" ); if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } assert xCoord == Integer.MIN_VALUE; assert yCoord == Integer.MIN_VALUE; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "MarkArray" ) ) { String[] pn1 = new String[] { null, "MarkBasePos" }; String[] pn2 = new String[] { null, "MarkLigPos" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( ! isParent ( pnx ) ) { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "MarkAttachClassDef" ) ) { String[] pn = new String[] { null, "GDEF" }; if ( isParent ( pn ) ) { String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: case 2: break; default: unsupportedFormat ( en, sf ); break; } } assertSubtableClear(); assert sf >= 0; // force format 1 since TTX always writes entries as non-range entries if ( sf != 1 ) { sf = 1; } stFormat = sf; assert glyphClasses.isEmpty(); } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "MarkBasePos" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: break; default: unsupportedFormat ( en, sf ); break; } } assertSubtableClear(); assert sf >= 0; stFormat = sf; assert markAnchors.size() == 0; assert baseOrMarkAnchors.size() == 0; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "MarkCoverage" ) ) { String[] pn1 = new String[] { null, "MarkBasePos" }; String[] pn2 = new String[] { null, "MarkLigPos" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( isParent ( pnx ) ) { String format = attrs.getValue ( "Format" ); int cf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { cf = Integer.parseInt ( format ); switch ( cf ) { case 1: case 2: break; default: unsupportedFormat ( en, cf ); break; } } assertCoverageClear(); ctIndex = 0; ctFormat = cf; } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "MarkLigPos" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: break; default: unsupportedFormat ( en, sf ); break; } } assertSubtableClear(); assert sf >= 0; stFormat = sf; assert markAnchors.size() == 0; assert ligatureAnchors.size() == 0; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "MarkMarkPos" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: break; default: unsupportedFormat ( en, sf ); break; } } assertSubtableClear(); assert sf >= 0; stFormat = sf; assert markAnchors.size() == 0; assert baseOrMarkAnchors.size() == 0; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "MarkRecord" ) ) { String[] pn1 = new String[] { null, "MarkArray" }; String[] pn2 = new String[] { null, "Mark1Array" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( isParent ( pnx ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "MultipleSubst" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: break; default: unsupportedFormat ( en, sf ); break; } } assertSubtableClear(); assert sf >= 0; stFormat = sf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "PairPos" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: case 2: break; default: unsupportedFormat ( en, sf ); break; } } assertSubtableClear(); assert sf >= 0; stFormat = sf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "PairSet" ) ) { String[] pn = new String[] { null, "PairPos" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); int psi = -1; if ( index == null ) { missingRequiredAttribute ( en, "index" ); } else { psi = Integer.parseInt ( index ); } assert psIndex == -1; psIndex = psi; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "PairValueRecord" ) ) { String[] pn = new String[] { null, "PairSet" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } else { assertPairClear(); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "PosLookupRecord" ) ) { String[] pn1 = new String[] { null, "ChainContextSubst" }; String[] pn2 = new String[] { null, "ChainContextPos" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( isParent ( pnx ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "ReqFeatureIndex" ) ) { String[] pn1 = new String[] { null, "DefaultLangSys" }; String[] pn2 = new String[] { null, "LangSys" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( isParent ( pnx ) ) { String value = attrs.getValue ( "value" ); int v = -1; if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { v = Integer.parseInt ( value ); } String fid; if ( ( v >= 0 ) && ( v < 65535 ) ) { fid = makeFeatureId ( v ); } else { fid = null; } assertLanguageFeaturesClear(); languageFeatures.add ( fid ); } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "Script" ) ) { String[] pn = new String[] { null, "ScriptRecord" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "ScriptList" ) ) { String[] pn1 = new String[] { null, "GSUB" }; String[] pn2 = new String[] { null, "GPOS" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( ! isParent ( pnx ) ) { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "ScriptRecord" ) ) { String[] pn = new String[] { null, "ScriptList" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "ScriptTag" ) ) { String[] pn = new String[] { null, "ScriptRecord" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { assert scriptTag == null; scriptTag = value; } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "SecondGlyph" ) ) { String[] pn = new String[] { null, "PairValueRecord" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { int gid = mapGlyphId ( value, en ); assert g2 == -1; g2 = gid; } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Sequence" ) ) { String[] pn = new String[] { null, "MultipleSubst" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } else { int i = Integer.parseInt ( index ); if ( i != subtableEntries.size() ) { invalidIndex ( en, i, subtableEntries.size() ); } } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "SequenceIndex" ) ) { String[] pn1 = new String[] { null, "PosLookupRecord" }; String[] pn2 = new String[] { null, "SubstLookupRecord" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( isParent ( pnx ) ) { String value = attrs.getValue ( "value" ); int v = -1; if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { v = Integer.parseInt ( value ); } assert rlSequence == -1; assert v != -1; rlSequence = v; } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "SinglePos" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: case 2: break; default: unsupportedFormat ( en, sf ); break; } } assertSubtableClear(); assert sf >= 0; stFormat = sf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "SingleSubst" ) ) { String[] pn = new String[] { null, "Lookup" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } String format = attrs.getValue ( "Format" ); int sf = -1; if ( format == null ) { missingRequiredAttribute ( en, "Format" ); } else { sf = Integer.parseInt ( format ); switch ( sf ) { case 1: case 2: break; default: unsupportedFormat ( en, sf ); break; } } assertCoverageClear(); ctIndex = 0; ctFormat = 1; assertSubtableClear(); assert sf >= 0; stFormat = sf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "SubstLookupRecord" ) ) { String[] pn = new String[] { null, "ChainContextSubst" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Substitute" ) ) { String[] pn = new String[] { null, "Sequence" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( index == null ) { missingRequiredAttribute ( en, "index" ); } else { int i = Integer.parseInt ( index ); if ( i != substitutes.size() ) { invalidIndex ( en, i, substitutes.size() ); } } String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { int gid = mapGlyphId ( value, en ); substitutes.add ( Integer.valueOf ( gid ) ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Substitution" ) ) { String[] pn = new String[] { null, "SingleSubst" }; if ( isParent ( pn ) ) { String in = attrs.getValue ( "in" ); int igid = -1; int ogid = -1; if ( in == null ) { missingRequiredAttribute ( en, "in" ); } else { igid = mapGlyphId ( in, en ); } String out = attrs.getValue ( "out" ); if ( out == null ) { missingRequiredAttribute ( en, "out" ); } else { ogid = mapGlyphId ( out, en ); } coverageEntries.add ( Integer.valueOf ( igid ) ); subtableEntries.add ( Integer.valueOf ( ogid ) ); } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Value" ) ) { String[] pn = new String[] { null, "SinglePos" }; if ( isParent ( pn ) ) { String index = attrs.getValue ( "index" ); if ( vf1 < 0 ) { missingParameter ( en, "value format" ); } else { subtableEntries.add ( parseValue ( en, attrs, vf1 ) ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Value1" ) ) { String[] pn = new String[] { null, "PairValueRecord" }; if ( isParent ( pn ) ) { if ( vf1 < 0 ) { missingParameter ( en, "value format 1" ); } else { assert v1 == null; v1 = parseValue ( en, attrs, vf1 ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Value2" ) ) { String[] pn = new String[] { null, "PairValueRecord" }; if ( isParent ( pn ) ) { if ( vf2 < 0 ) { missingParameter ( en, "value format 2" ); } else { assert v2 == null; v2 = parseValue ( en, attrs, vf2 ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "ValueFormat" ) ) { String[] pn = new String[] { null, "SinglePos" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); int vf = -1; if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { vf = Integer.parseInt ( value ); } assert vf1 == -1; vf1 = vf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "ValueFormat1" ) ) { String[] pn = new String[] { null, "PairPos" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); int vf = -1; if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { vf = Integer.parseInt ( value ); } assert vf1 == -1; vf1 = vf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "ValueFormat2" ) ) { String[] pn = new String[] { null, "PairPos" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); int vf = -1; if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { vf = Integer.parseInt ( value ); } assert vf2 == -1; vf2 = vf; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "Version" ) ) { String[] pn1 = new String[] { null, "GDEF" }; String[] pn2 = new String[] { null, "GPOS" }; String[] pn3 = new String[] { null, "GSUB" }; String[][] pnx = new String[][] { pn1, pn2, pn3 }; if ( isParent ( pnx ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "XCoordinate" ) ) { String[] pn1 = new String[] { null, "BaseAnchor" }; String[] pn2 = new String[] { null, "EntryAnchor" }; String[] pn3 = new String[] { null, "ExitAnchor" }; String[] pn4 = new String[] { null, "LigatureAnchor" }; String[] pn5 = new String[] { null, "MarkAnchor" }; String[] pn6 = new String[] { null, "Mark2Anchor" }; String[][] pnx = new String[][] { pn1, pn2, pn3, pn4, pn5, pn6 }; if ( isParent ( pnx ) ) { String value = attrs.getValue ( "value" ); int x = 0; if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { x = Integer.parseInt ( value ); } assert xCoord == Integer.MIN_VALUE; xCoord = x; } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "YCoordinate" ) ) { String[] pn1 = new String[] { null, "BaseAnchor" }; String[] pn2 = new String[] { null, "EntryAnchor" }; String[] pn3 = new String[] { null, "ExitAnchor" }; String[] pn4 = new String[] { null, "LigatureAnchor" }; String[] pn5 = new String[] { null, "MarkAnchor" }; String[] pn6 = new String[] { null, "Mark2Anchor" }; String[][] pnx = new String[][] { pn1, pn2, pn3, pn4, pn5, pn6 }; if ( isParent ( pnx ) ) { String value = attrs.getValue ( "value" ); int y = 0; if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { y = Integer.parseInt ( value ); } assert yCoord == Integer.MIN_VALUE; yCoord = y; } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "checkSumAdjustment" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "cmap" ) ) { String[] pn = new String[] { null, "ttFont" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "cmap_format_0" ) ) { String[] pn = new String[] { null, "cmap" }; if ( isParent ( pn ) ) { String platformID = attrs.getValue ( "platformID" ); if ( platformID == null ) { missingRequiredAttribute ( en, "platformID" ); } String platEncID = attrs.getValue ( "platEncID" ); if ( platEncID == null ) { missingRequiredAttribute ( en, "platEncID" ); } String language = attrs.getValue ( "language" ); if ( language == null ) { missingRequiredAttribute ( en, "language" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "cmap_format_4" ) ) { String[] pn = new String[] { null, "cmap" }; if ( isParent ( pn ) ) { String platformID = attrs.getValue ( "platformID" ); int pid = -1; if ( platformID == null ) { missingRequiredAttribute ( en, "platformID" ); } else { pid = Integer.parseInt ( platformID ); } String platEncID = attrs.getValue ( "platEncID" ); int eid = -1; if ( platEncID == null ) { missingRequiredAttribute ( en, "platEncID" ); } else { eid = Integer.parseInt ( platEncID ); } String language = attrs.getValue ( "language" ); int lid = -1; if ( language == null ) { missingRequiredAttribute ( en, "language" ); } else { lid = Integer.parseInt ( language ); } assert cmapEntries.size() == 0; assert cmPlatform == -1; assert cmEncoding == -1; assert cmLanguage == -1; cmPlatform = pid; cmEncoding = eid; cmLanguage = lid; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "created" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "flags" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "fontDirectionHint" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "fontRevision" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "glyphDataFormat" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "head" ) ) { String[] pn = new String[] { null, "ttFont" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "hmtx" ) ) { String[] pn = new String[] { null, "ttFont" }; if ( ! isParent ( pn ) ) { notPermittedInElementContext ( en, getParent(), pn ); } else if ( glyphIdMax > 0 ) { hmtxEntries.setSize ( glyphIdMax + 1 ); } } else if ( en[1].equals ( "indexToLocFormat" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "lowestRecPPEM" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "macStyle" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "magicNumber" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "map" ) ) { String[] pn1 = new String[] { null, "cmap_format_0" }; String[] pn2 = new String[] { null, "cmap_format_4" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( isParent ( pnx ) ) { String code = attrs.getValue ( "code" ); int cid = -1; if ( code == null ) { missingRequiredAttribute ( en, "code" ); } else { code = code.toLowerCase(); if ( code.startsWith ( "0x" ) ) { cid = Integer.parseInt ( code.substring ( 2 ), 16 ); } else { cid = Integer.parseInt ( code, 10 ); } } String name = attrs.getValue ( "name" ); int gid = -1; if ( name == null ) { missingRequiredAttribute ( en, "name" ); } else { gid = mapGlyphId ( name, en ); } if ( ( cmPlatform == 3 ) && ( cmEncoding == 1 ) ) { cmapEntries.add ( new int[] { cid, gid } ); } } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "modified" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "mtx" ) ) { String[] pn = new String[] { null, "hmtx" }; if ( isParent ( pn ) ) { String name = attrs.getValue ( "name" ); int gid = -1; if ( name == null ) { missingRequiredAttribute ( en, "name" ); } else { gid = mapGlyphId ( name, en ); } String width = attrs.getValue ( "width" ); int w = -1; if ( width == null ) { missingRequiredAttribute ( en, "width" ); } else { w = Integer.parseInt ( width ); } String lsb = attrs.getValue ( "lsb" ); int l = -1; if ( lsb == null ) { missingRequiredAttribute ( en, "lsb" ); } else { l = Integer.parseInt ( lsb ); } hmtxEntries.set ( gid, new int[] { w, l } ); } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "tableVersion" ) ) { String[] pn1 = new String[] { null, "cmap" }; String[] pn2 = new String[] { null, "head" }; String[][] pnx = new String[][] { pn1, pn2 }; if ( isParent ( pn1 ) ) { // child of cmap String version = attrs.getValue ( "version" ); if ( version == null ) { missingRequiredAttribute ( en, "version" ); } } else if ( isParent ( pn2 ) ) { // child of head String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pnx ); } } else if ( en[1].equals ( "ttFont" ) ) { String[] pn = new String[] { null, null }; if ( isParent ( pn ) ) { String sfntVersion = attrs.getValue ( "sfntVersion" ); if ( sfntVersion == null ) { missingRequiredAttribute ( en, "sfntVersion" ); } String ttLibVersion = attrs.getValue ( "ttLibVersion" ); if ( ttLibVersion == null ) { missingRequiredAttribute ( en, "ttLibVersion" ); } } else { notPermittedInElementContext ( en, getParent(), null ); } } else if ( en[1].equals ( "unitsPerEm" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); int v = -1; if ( value == null ) { missingRequiredAttribute ( en, "value" ); } else { v = Integer.parseInt ( value ); } assert upem == -1; upem = v; } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "xMax" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "xMin" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "yMax" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else if ( en[1].equals ( "yMin" ) ) { String[] pn = new String[] { null, "head" }; if ( isParent ( pn ) ) { String value = attrs.getValue ( "value" ); if ( value == null ) { missingRequiredAttribute ( en, "value" ); } } else { notPermittedInElementContext ( en, getParent(), pn ); } } else { unsupportedElement ( en ); } elements.push ( en ); } @Override public void endElement ( String uri, String localName, String qName ) throws SAXException { if ( elements.empty() ) { throw new SAXException ( "element stack is unbalanced, no elements on stack!" ); } String[] enParent = elements.peek(); if ( enParent == null ) { throw new SAXException ( "element stack is empty, elements are not balanced" ); } String[] en = makeExpandedName ( uri, localName, qName ); if ( ! sameExpandedName ( enParent, en ) ) { throw new SAXException ( "element stack is unbalanced, expanded name mismatch" ); } if ( en[0] != null ) { unsupportedElement ( en ); } else if ( isAnchorElement ( en[1] ) ) { if ( xCoord == Integer.MIN_VALUE ) { missingParameter ( en, "x coordinate" ); } else if ( yCoord == Integer.MIN_VALUE ) { missingParameter ( en, "y coordinate" ); } else { if ( en[1].equals ( "EntryAnchor" ) ) { if ( anchors.size() > 0 ) { duplicateParameter ( en, "entry anchor" ); } } else if ( en[1].equals ( "ExitAnchor" ) ) { if ( anchors.size() > 1 ) { duplicateParameter ( en, "exit anchor" ); } else if ( anchors.size() == 0 ) { anchors.add ( null ); } } anchors.add ( new GlyphPositioningTable.Anchor ( xCoord, yCoord ) ); xCoord = yCoord = Integer.MIN_VALUE; } } else if ( en[1].equals ( "AlternateSet" ) ) { subtableEntries.add ( extractAlternates() ); } else if ( en[1].equals ( "AlternateSubst" ) ) { if ( ! sortEntries ( coverageEntries, subtableEntries ) ) { mismatchedEntries ( en, coverageEntries.size(), subtableEntries.size() ); } addGSUBSubtable ( GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_ALTERNATE, extractCoverage() ); } else if ( en[1].equals ( "BacktrackCoverage" ) ) { String ck = makeCoverageKey ( "bk", ctIndex ); if ( coverages.containsKey ( ck ) ) { duplicateCoverageIndex ( en, ctIndex ); } else { coverages.put ( ck, extractCoverage() ); } } else if ( en[1].equals ( "BaseCoverage" ) ) { coverages.put ( "base", extractCoverage() ); } else if ( en[1].equals ( "BaseRecord" ) ) { baseOrMarkAnchors.add ( extractAnchors() ); } else if ( en[1].equals ( "ChainContextPos" ) || en[1].equals ( "ChainContextSubst" ) ) { GlyphCoverageTable coverage = null; if ( stFormat == 3 ) { GlyphCoverageTable igca[] = getCoveragesWithPrefix ( "in" ); GlyphCoverageTable bgca[] = getCoveragesWithPrefix ( "bk" ); GlyphCoverageTable lgca[] = getCoveragesWithPrefix ( "la" ); if ( ( igca.length == 0 ) || hasMissingCoverage ( igca ) ) { missingCoverage ( en, "input", igca.length ); } else if ( hasMissingCoverage ( bgca ) ) { missingCoverage ( en, "backtrack", bgca.length ); } else if ( hasMissingCoverage ( lgca ) ) { missingCoverage ( en, "lookahead", lgca.length ); } else { GlyphTable.Rule r = new GlyphTable.ChainedCoverageSequenceRule ( extractRuleLookups(), igca.length, igca, bgca, lgca ); GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet ( new GlyphTable.Rule[] {r} ); GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[] {rs}; coverage = igca [ 0 ]; subtableEntries.add ( rsa ); } } else { unsupportedFormat ( en, stFormat ); } if ( en[1].equals ( "ChainContextPos" ) ) { addGPOSSubtable ( GlyphPositioningTable.GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL, coverage ); } else if ( en[1].equals ( "ChainContextSubst" ) ) { addGSUBSubtable ( GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL, coverage ); } } else if ( en[1].equals ( "ComponentRecord" ) ) { components.add ( extractAnchors() ); } else if ( en[1].equals ( "Coverage" ) ) { coverages.put ( "main", extractCoverage() ); } else if ( en[1].equals ( "DefaultLangSys" ) || en[1].equals ( "LangSysRecord" ) ) { if ( languageTag == null ) { missingTag ( en, "language" ); } else if ( languages.containsKey ( languageTag ) ) { duplicateTag ( en, "language", languageTag ); } else { languages.put ( languageTag, extractLanguageFeatures() ); languageTag = null; } } else if ( en[1].equals ( "CursivePos" ) ) { GlyphCoverageTable ct = coverages.get ( "main" ); if ( ct == null ) { missingParameter ( en, "coverages" ); } else if ( stFormat == 1 ) { subtableEntries.add ( extractAttachmentAnchors() ); } else { unsupportedFormat ( en, stFormat ); } addGPOSSubtable ( GlyphPositioningTable.GPOS_LOOKUP_TYPE_CURSIVE, ct ); } else if ( en[1].equals ( "EntryExitRecord" ) ) { int na = anchors.size(); if ( na == 0 ) { missingParameter ( en, "entry or exit anchor" ); } else if ( na == 1 ) { anchors.add ( null ); } else if ( na > 2 ) { duplicateParameter ( en, "entry or exit anchor" ); } attachmentAnchors.add ( extractAnchors() ); } else if ( en[1].equals ( "BaseRecord" ) ) { baseOrMarkAnchors.add ( extractAnchors() ); } else if ( en[1].equals ( "FeatureRecord" ) ) { if ( flIndex != flSequence ) { mismatchedIndex ( en, "feature", flIndex, flSequence ); } else if ( featureTag == null ) { missingTag ( en, "feature" ); } else { String fid = makeFeatureId ( flIndex ); features.put ( fid, extractFeature() ); nextFeature(); } } else if ( en[1].equals ( "GDEF" ) ) { if ( subtables.size() > 0 ) { gdef = new GlyphDefinitionTable ( subtables ); } clearTable(); } else if ( en[1].equals ( "GPOS" ) ) { if ( subtables.size() > 0 ) { gpos = new GlyphPositioningTable ( gdef, extractLookups(), subtables ); } clearTable(); } else if ( en[1].equals ( "GSUB" ) ) { if ( subtables.size() > 0 ) { gsub = new GlyphSubstitutionTable ( gdef, extractLookups(), subtables ); } clearTable(); } else if ( en[1].equals ( "GlyphClassDef" ) ) { GlyphMappingTable mapping = extractClassDefMapping ( glyphClasses, stFormat, true ); addGDEFSubtable ( GlyphDefinitionTable.GDEF_LOOKUP_TYPE_GLYPH_CLASS, mapping ); } else if ( en[1].equals ( "InputCoverage" ) ) { String ck = makeCoverageKey ( "in", ctIndex ); if ( coverages.containsKey ( ck ) ) { duplicateCoverageIndex ( en, ctIndex ); } else { coverages.put ( ck, extractCoverage() ); } } else if ( en[1].equals ( "LigatureAttach" ) ) { ligatureAnchors.add ( extractComponents() ); } else if ( en[1].equals ( "LigatureCoverage" ) ) { coverages.put ( "liga", extractCoverage() ); } else if ( en[1].equals ( "LigatureSet" ) ) { subtableEntries.add ( extractLigatures() ); } else if ( en[1].equals ( "LigatureSubst" ) ) { if ( ! sortEntries ( coverageEntries, subtableEntries ) ) { mismatchedEntries ( en, coverageEntries.size(), subtableEntries.size() ); } GlyphCoverageTable coverage = extractCoverage(); addGSUBSubtable ( GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_LIGATURE, coverage ); } else if ( en[1].equals ( "LookAheadCoverage" ) ) { String ck = makeCoverageKey ( "la", ctIndex ); if ( coverages.containsKey ( ck ) ) { duplicateCoverageIndex ( en, ctIndex ); } else { coverages.put ( ck, extractCoverage() ); } } else if ( en[1].equals ( "Lookup" ) ) { if ( ltIndex != ltSequence ) { mismatchedIndex ( en, "lookup", ltIndex, ltSequence ); } else { nextLookup(); } } else if ( en[1].equals ( "MarkAttachClassDef" ) ) { GlyphMappingTable mapping = extractClassDefMapping ( glyphClasses, stFormat, true ); addGDEFSubtable ( GlyphDefinitionTable.GDEF_LOOKUP_TYPE_MARK_ATTACHMENT, mapping ); } else if ( en[1].equals ( "MarkCoverage" ) ) { coverages.put ( "mark", extractCoverage() ); } else if ( en[1].equals ( "Mark1Coverage" ) ) { coverages.put ( "mrk1", extractCoverage() ); } else if ( en[1].equals ( "Mark2Coverage" ) ) { coverages.put ( "mrk2", extractCoverage() ); } else if ( en[1].equals ( "MarkBasePos" ) ) { GlyphCoverageTable mct = coverages.get ( "mark" ); GlyphCoverageTable bct = coverages.get ( "base" ); if ( mct == null ) { missingParameter ( en, "mark coverages" ); } else if ( bct == null ) { missingParameter ( en, "base coverages" ); } else if ( stFormat == 1 ) { MarkAnchor[] maa = extractMarkAnchors(); Anchor[][] bam = extractBaseOrMarkAnchors(); subtableEntries.add ( bct ); subtableEntries.add ( computeClassCount ( bam ) ); subtableEntries.add ( maa ); subtableEntries.add ( bam ); } else { unsupportedFormat ( en, stFormat ); } addGPOSSubtable ( GlyphPositioningTable.GPOS_LOOKUP_TYPE_MARK_TO_BASE, mct ); } else if ( en[1].equals ( "MarkLigPos" ) ) { GlyphCoverageTable mct = coverages.get ( "mark" ); GlyphCoverageTable lct = coverages.get ( "liga" ); if ( mct == null ) { missingParameter ( en, "mark coverages" ); } else if ( lct == null ) { missingParameter ( en, "ligature coverages" ); } else if ( stFormat == 1 ) { MarkAnchor[] maa = extractMarkAnchors(); Anchor[][][] lam = extractLigatureAnchors(); subtableEntries.add ( lct ); subtableEntries.add ( computeLigaturesClassCount ( lam ) ); subtableEntries.add ( computeLigaturesComponentCount ( lam ) ); subtableEntries.add ( maa ); subtableEntries.add ( lam ); } else { unsupportedFormat ( en, stFormat ); } addGPOSSubtable ( GlyphPositioningTable.GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE, mct ); } else if ( en[1].equals ( "MarkMarkPos" ) ) { GlyphCoverageTable mct1 = coverages.get ( "mrk1" ); GlyphCoverageTable mct2 = coverages.get ( "mrk2" ); if ( mct1 == null ) { missingParameter ( en, "mark coverages 1" ); } else if ( mct2 == null ) { missingParameter ( en, "mark coverages 2" ); } else if ( stFormat == 1 ) { MarkAnchor[] maa = extractMarkAnchors(); Anchor[][] mam = extractBaseOrMarkAnchors(); subtableEntries.add ( mct2 ); subtableEntries.add ( computeClassCount ( mam ) ); subtableEntries.add ( maa ); subtableEntries.add ( mam ); } else { unsupportedFormat ( en, stFormat ); } addGPOSSubtable ( GlyphPositioningTable.GPOS_LOOKUP_TYPE_MARK_TO_MARK, mct1 ); } else if ( en[1].equals ( "MarkRecord" ) ) { if ( markClass == -1 ) { missingParameter ( en, "mark class" ); } else if ( anchors.size() == 0 ) { missingParameter ( en, "mark anchor" ); } else if ( anchors.size() > 1 ) { duplicateParameter ( en, "mark anchor" ); } else { markAnchors.add ( new GlyphPositioningTable.MarkAnchor ( markClass, anchors.get(0) ) ); markClass = -1; anchors.clear(); } } else if ( en[1].equals ( "Mark2Record" ) ) { baseOrMarkAnchors.add ( extractAnchors() ); } else if ( en[1].equals ( "MultipleSubst" ) ) { GlyphCoverageTable coverage = coverages.get ( "main" ); addGSUBSubtable ( GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_MULTIPLE, coverage, extractSequenceEntries() ); } else if ( en[1].equals ( "PairPos" ) ) { assertSubtableEntriesClear(); if ( stFormat == 1 ) { if ( pairSets.size() == 0 ) { missingParameter ( en, "pair set" ); } else { subtableEntries.add ( extractPairSets() ); } } else if ( stFormat == 2 ) { unsupportedFormat ( en, stFormat ); } GlyphCoverageTable coverage = coverages.get ( "main" ); addGPOSSubtable ( GlyphPositioningTable.GPOS_LOOKUP_TYPE_PAIR, coverage ); vf1 = vf2 = -1; psIndex = -1; } else if ( en[1].equals ( "PairSet" ) ) { if ( psIndex != pairSets.size() ) { invalidIndex ( en, psIndex, pairSets.size() ); } else { pairSets.add ( extractPairs() ); } } else if ( en[1].equals ( "PairValueRecord" ) ) { if ( g2 == -1 ) { missingParameter ( en, "second glyph" ); } else if ( ( v1 == null ) && ( v2 == null ) ) { missingParameter ( en, "first or second value" ); } else { pairs.add ( new PairValues ( g2, v1, v2 ) ); clearPair(); } } else if ( en[1].equals ( "PosLookupRecord" ) || en[1].equals ( "SubstLookupRecord" ) ) { if ( rlSequence < 0 ) { missingParameter ( en, "sequence index" ); } else if ( rlLookup < 0 ) { missingParameter ( en, "lookup index" ); } else { ruleLookups.add ( new GlyphTable.RuleLookup ( rlSequence, rlLookup ) ); rlSequence = rlLookup = -1; } } else if ( en[1].equals ( "Script" ) ) { if ( scriptTag == null ) { missingTag ( en, "script" ); } else if ( scripts.containsKey ( scriptTag ) ) { duplicateTag ( en, "script", scriptTag ); } else { scripts.put ( scriptTag, extractLanguages() ); scriptTag = null; } } else if ( en[1].equals ( "Sequence" ) ) { subtableEntries.add ( extractSubstitutes() ); } else if ( en[1].equals ( "SinglePos" ) ) { int nv = subtableEntries.size(); if ( stFormat == 1 ) { if ( nv < 0 ) { missingParameter ( en, "value" ); } else if ( nv > 1 ) { duplicateParameter ( en, "value" ); } } else if ( stFormat == 2 ) { GlyphPositioningTable.Value[] pva = (GlyphPositioningTable.Value[]) subtableEntries.toArray ( new GlyphPositioningTable.Value [ nv ] ); subtableEntries.clear(); subtableEntries.add ( pva ); } GlyphCoverageTable coverage = coverages.get ( "main" ); addGPOSSubtable ( GlyphPositioningTable.GPOS_LOOKUP_TYPE_SINGLE, coverage ); vf1 = -1; } else if ( en[1].equals ( "SingleSubst" ) ) { if ( ! sortEntries ( coverageEntries, subtableEntries ) ) { mismatchedEntries ( en, coverageEntries.size(), subtableEntries.size() ); } GlyphCoverageTable coverage = extractCoverage(); addGSUBSubtable ( GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_SINGLE, coverage ); } else if ( en[1].equals ( "cmap" ) ) { cmap = getCMAP(); gmap = getGMAP(); cmapEntries.clear(); } else if ( en[1].equals ( "cmap_format_4" ) ) { cmPlatform = cmEncoding = cmLanguage = -1; } else if ( en[1].equals ( "hmtx" ) ) { hmtx = getHMTX(); hmtxEntries.clear(); } else if ( en[1].equals ( "ttFont" ) ) { if ( cmap == null ) { missingParameter ( en, "cmap" ); } if ( hmtx == null ) { missingParameter ( en, "hmtx" ); } } elements.pop(); } @Override public void characters ( char[] chars, int start, int length ) { } private String[] getParent() { if ( ! elements.empty() ) { return elements.peek(); } else { return new String[] { null, null }; } } private boolean isParent ( Object enx ) { if ( enx instanceof String[][] ) { for ( String[] en : (String[][]) enx ) { if ( isParent ( en ) ) { return true; } } return false; } else if ( enx instanceof String[] ) { String[] en = (String[]) enx; if ( ! elements.empty() ) { String[] pn = elements.peek(); return ( pn != null ) && sameExpandedName ( en, pn ); } else if ( ( en[0] == null ) && ( en[1] == null ) ) { return true; } else { return false; } } else { return false; } } private boolean isAnchorElement ( String ln ) { if ( ln.equals ( "BaseAnchor" ) ) { return true; } else if ( ln.equals ( "EntryAnchor" ) ) { return true; } else if ( ln.equals ( "ExitAnchor" ) ) { return true; } else if ( ln.equals ( "LigatureAnchor" ) ) { return true; } else if ( ln.equals ( "MarkAnchor" ) ) { return true; } else if ( ln.equals ( "Mark2Anchor" ) ) { return true; } else { return false; } } private Map getCMAP() { Map cmap = new TreeMap(); for ( int[] cme : cmapEntries ) { Integer c = Integer.valueOf ( cme[0] ); Integer g = Integer.valueOf ( cme[1] ); cmap.put ( c, g ); } return cmap; } private Map getGMAP() { Map gmap = new TreeMap(); for ( int[] cme : cmapEntries ) { Integer c = Integer.valueOf ( cme[0] ); Integer g = Integer.valueOf ( cme[1] ); gmap.put ( g, c ); } return gmap; } private int[][] getHMTX() { int ne = hmtxEntries.size(); int[][] hmtx = new int [ ne ] [ 2 ]; for ( int i = 0; i < ne; i++ ) { int[] ea = hmtxEntries.get(i); if ( ea != null ) { hmtx [ i ] [ 0 ] = ea[0]; hmtx [ i ] [ 1 ] = ea[1]; } } return hmtx; } private GlyphClassTable extractClassDefMapping ( Map glyphClasses, int format, boolean clearSourceMap ) { GlyphClassTable ct; if ( format == 1 ) { ct = extractClassDefMapping1 ( extractClassMappings ( glyphClasses, clearSourceMap ) ); } else if ( format == 2 ) { ct = extractClassDefMapping2 ( extractClassMappings ( glyphClasses, clearSourceMap ) ); } else { ct = null; } return ct; } private GlyphClassTable extractClassDefMapping1 ( int[][] cma ) { List entries = new ArrayList(); int s = -1; int l = -1; Integer zero = Integer.valueOf(0); for ( int[] m : cma ) { int g = m[0]; int c = m[1]; if ( s < 0 ) { s = g; l = g - 1; entries.add ( Integer.valueOf ( s ) ); } while ( g > ( l + 1 ) ) { entries.add ( zero ); l++; } assert l == ( g - 1 ); entries.add ( Integer.valueOf ( c ) ); l = g; } return GlyphClassTable.createClassTable ( entries ); } private GlyphClassTable extractClassDefMapping2 ( int[][] cma ) { List entries = new ArrayList(); int s = -1; int e = s; int l = -1; for ( int[] m : cma ) { int g = m[0]; int c = m[1]; if ( c != l ) { if ( s >= 0 ) { entries.add ( new GlyphClassTable.MappingRange ( s, e, l ) ); } s = e = g; } else { e = g; } l = c; } return GlyphClassTable.createClassTable ( entries ); } private int[][] extractClassMappings ( Map glyphClasses, boolean clearSourceMap ) { int nc = glyphClasses.size(); int i = 0; int[][] cma = new int [ nc ] [ 2 ]; for ( Map.Entry e : glyphClasses.entrySet() ) { Integer gid = glyphIds.get ( e.getKey() ); assert gid != null; int[] m = cma [ i ]; m [ 0 ] = (int) gid; m [ 1 ] = (int) e.getValue(); i++; } if ( clearSourceMap ) { glyphClasses.clear(); } return sortClassMappings ( cma ); } private int[][] sortClassMappings ( int[][] cma ) { Arrays.sort ( cma, new Comparator() { public int compare ( int[] m1, int[] m2 ) { assert m1.length > 0; assert m2.length > 0; if ( m1[0] < m2[0] ) { return -1; } else if ( m1[0] > m2[0] ) { return 1; } else { return 0; } } } ); return cma; } // sort coverage entries and subtable entries together private boolean sortEntries ( List cel, List sel ) { assert cel != null; assert sel != null; if ( cel.size() == sel.size() ) { int np = cel.size(); Object[][] pa = new Object [ np ] [ 2 ]; for ( int i = 0; i < np; i++ ) { pa [ i ] [ 0 ] = cel.get ( i ); pa [ i ] [ 1 ] = sel.get ( i ); } Arrays.sort ( pa, new Comparator() { public int compare ( Object[] p1, Object[] p2 ) { assert p1.length == 2; assert p2.length == 2; int c1 = (Integer) p1[0]; int c2 = (Integer) p2[0]; if ( c1 < c2 ) { return -1; } else if ( c1 > c2 ) { return 1; } else { return 0; } } } ); cel.clear(); sel.clear(); for ( int i = 0; i < np; i++ ) { cel.add ( pa [ i ] [ 0 ] ); sel.add ( pa [ i ] [ 1 ] ); } assert cel.size() == sel.size(); return true; } else { return false; } } private String makeCoverageKey ( String prefix, int index ) { assert prefix != null; assert prefix.length() == 2; assert index < 100; return prefix + CharUtilities.padLeft ( Integer.toString ( index, 10 ), 2, '0' ); } private List extractCoverageEntries() { List entries = new ArrayList ( coverageEntries ); clearCoverage(); return entries; } private void clearCoverageEntries() { coverageEntries.clear(); ctFormat = -1; ctIndex = -1; } private void assertCoverageEntriesClear() { assert coverageEntries.size() == 0; } private GlyphCoverageTable extractCoverage() { assert ( ctFormat == 1 ) || ( ctFormat == 2 ); assert ctIndex >= 0; GlyphCoverageTable coverage = GlyphCoverageTable.createCoverageTable ( extractCoverageEntries() ); clearCoverage(); return coverage; } private void clearCoverages() { coverages.clear(); } private void assertCoverageClear() { assert ctFormat == -1; assert ctIndex == -1; assertCoverageEntriesClear(); } private void clearCoverage() { ctFormat = -1; ctIndex = -1; clearCoverageEntries(); } private void assertCoveragesClear() { assert coverages.size() == 0; } private GlyphCoverageTable[] getCoveragesWithPrefix ( String prefix ) { assert prefix != null; int prefixLength = prefix.length(); Set keys = coverages.keySet(); int mi = -1; // maximum coverage table index for ( String k : keys ) { if ( k.startsWith ( prefix ) ) { int i = Integer.parseInt ( k.substring ( prefixLength ) ); if ( i > mi ) { mi = i; } } } GlyphCoverageTable[] gca = new GlyphCoverageTable [ mi + 1 ]; for ( String k : keys ) { if ( k.startsWith ( prefix ) ) { int i = Integer.parseInt ( k.substring ( prefixLength ) ); if ( i >= 0 ) { gca [ i ] = coverages.get ( k ); } } } return gca; } private boolean hasMissingCoverage ( GlyphCoverageTable[] gca ) { assert gca != null; int nc = 0; for ( int i = 0, n = gca.length; i < n; i++ ) { if ( gca [ i ] != null ) { nc++; } } return nc != gca.length; } private String makeFeatureId ( int fid ) { assert fid >= 0; return "f" + fid; } private String makeLookupId ( int lid ) { assert lid >= 0; return "lu" + lid; } private void clearScripts() { scripts.clear(); } private List extractLanguageFeatures() { List lfl = new ArrayList(languageFeatures); clearLanguageFeatures(); return lfl; } private void assertLanguageFeaturesClear() { assert languageFeatures.size() == 0; } private void clearLanguageFeatures() { languageFeatures.clear(); } private Map> extractLanguages() { Map> lm = new HashMap ( languages ); clearLanguages(); return lm; } private void clearLanguages() { languages.clear(); } private void assertFeatureLookupsClear() { assert featureLookups.size() == 0; } private List extractFeatureLookups() { List lookups = new ArrayList ( featureLookups ); clearFeatureLookups(); return lookups; } private void clearFeatureLookups() { featureLookups.clear(); } private void assertFeatureClear() { assert flIndex == -1; assert featureTag == null; assertFeatureLookupsClear(); } private Object[] extractFeature() { Object[] fa = new Object [ 2 ]; fa[0] = featureTag; fa[1] = extractFeatureLookups(); clearFeature(); return fa; } private void clearFeature() { flIndex = -1; featureTag = null; clearFeatureLookups(); } private void nextFeature() { flSequence++; } private void clearFeatures() { features.clear(); } private void clearSubtableInLookup() { stFormat = 0; clearCoverages(); } private void clearSubtablesInLookup() { clearSubtableInLookup(); stSequence = 0; } private void clearSubtablesInTable() { clearSubtablesInLookup(); subtables.clear(); } private void nextSubtableInLookup() { stSequence++; clearSubtableInLookup(); } private void assertLookupClear() { assert ltIndex == -1; assert ltFlags == 0; } private void clearLookup() { ltIndex = -1; ltFlags = 0; clearSubtablesInLookup(); } private Map> extractLookups() { Map> lookups = new LinkedHashMap>(); for ( String st : scripts.keySet() ) { Map> lm = scripts.get ( st ); if ( lm != null ) { for ( String lt : lm.keySet() ) { List fids = lm.get ( lt ); if ( fids != null ) { for ( String fid : fids ) { if ( fid != null ) { Object[] fa = features.get ( fid ); if ( fa != null ) { assert fa.length == 2; String ft = (String) fa[0]; List lids = (List) fa[1]; if ( ( lids != null ) && ( lids.size() > 0 ) ) { GlyphTable.LookupSpec ls = new GlyphTable.LookupSpec ( st, lt, ft ); lookups.put ( ls, lids ); } } } } } } } } clearScripts(); clearLanguages(); clearFeatures(); return lookups; } private void clearLookups() { clearLookup(); clearSubtablesInTable(); ltSequence = 0; flSequence = 0; } private void nextLookup() { ltSequence++; clearLookup(); } private void clearTable() { clearLookups(); } private void assertSubtableClear() { assert stFormat == 0; assertCoverageEntriesClear(); } private void assertSubtablesClear() { assertSubtableClear(); assert subtables.size() == 0; } private void clearSubtableEntries() { subtableEntries.clear(); } private void assertSubtableEntriesClear() { assert subtableEntries.size() == 0; } private List extractSubtableEntries() { List entries = new ArrayList ( subtableEntries ); clearSubtableEntries(); return entries; } private int[] extractAlternates() { int[] aa = new int [ alternates.size() ]; int i = 0; for ( Integer a : alternates ) { aa[i++] = (int) a; } clearAlternates(); return aa; } private void clearAlternates() { alternates.clear(); } private LigatureSet extractLigatures() { LigatureSet ls = new LigatureSet ( ligatures ); clearLigatures(); return ls; } private void clearLigatures() { ligatures.clear(); } private int[] extractSubstitutes() { int[] aa = new int [ substitutes.size() ]; int i = 0; for ( Integer a : substitutes ) { aa[i++] = (int) a; } clearSubstitutes(); return aa; } private void clearSubstitutes() { substitutes.clear(); } private List extractSequenceEntries() { List sequences = extractSubtableEntries(); int[][] sa = new int [ sequences.size() ] []; int i = 0; for ( Object s : sequences ) { if ( s instanceof int[] ) { sa[i++] = (int[]) s; } } List entries = new ArrayList(); entries.add ( sa ); return entries; } private RuleLookup[] extractRuleLookups() { RuleLookup[] lookups = (RuleLookup[]) ruleLookups.toArray ( new RuleLookup [ ruleLookups.size() ] ); clearRuleLookups(); return lookups; } private void clearRuleLookups() { ruleLookups.clear(); } private GlyphPositioningTable.Value parseValue ( String[] en, Attributes attrs, int format ) throws SAXException { String xPlacement = attrs.getValue ( "XPlacement" ); int xp = 0; if ( xPlacement != null ) { xp = Integer.parseInt ( xPlacement ); } else if ( ( format & GlyphPositioningTable.Value.X_PLACEMENT ) != 0 ) { missingParameter ( en, "xPlacement" ); } String yPlacement = attrs.getValue ( "YPlacement" ); int yp = 0; if ( yPlacement != null ) { yp = Integer.parseInt ( yPlacement ); } else if ( ( format & GlyphPositioningTable.Value.Y_PLACEMENT ) != 0 ) { missingParameter ( en, "yPlacement" ); } String xAdvance = attrs.getValue ( "XAdvance" ); int xa = 0; if ( xAdvance != null ) { xa = Integer.parseInt ( xAdvance ); } else if ( ( format & GlyphPositioningTable.Value.X_ADVANCE ) != 0 ) { missingParameter ( en, "xAdvance" ); } String yAdvance = attrs.getValue ( "YAdvance" ); int ya = 0;; if ( yAdvance != null ) { ya = Integer.parseInt ( yAdvance ); } else if ( ( format & GlyphPositioningTable.Value.Y_ADVANCE ) != 0 ) { missingParameter ( en, "yAdvance" ); } return new GlyphPositioningTable.Value ( xp, yp, xa, ya, null, null, null, null ); } private void assertPairClear() { assert g2 == -1; assert v1 == null; assert v2 == null; } private void clearPair() { g2 = -1; v1 = null; v2 = null; } private void assertPairsClear() { assert pairs.size() == 0; } private void clearPairs() { pairs.clear(); psIndex = -1; } private PairValues[] extractPairs() { PairValues[] pva = (PairValues[]) pairs.toArray ( new PairValues [ pairs.size() ] ); clearPairs(); return pva; } private void assertPairSetsClear() { assert pairSets.size() == 0; } private void clearPairSets() { pairSets.clear(); } private PairValues[][] extractPairSets() { PairValues[][] pvm = (PairValues[][]) pairSets.toArray ( new PairValues [ pairSets.size() ][] ); clearPairSets(); return pvm; } private Anchor[] extractAnchors() { Anchor[] aa = (Anchor[]) anchors.toArray ( new Anchor [ anchors.size() ] ); anchors.clear(); return aa; } private MarkAnchor[] extractMarkAnchors() { MarkAnchor[] maa = new MarkAnchor [ markAnchors.size() ]; maa = (MarkAnchor[]) markAnchors.toArray ( new MarkAnchor [ maa.length ] ); markAnchors.clear(); return maa; } private Anchor[][] extractBaseOrMarkAnchors() { int na = baseOrMarkAnchors.size(); int ncMax = 0; for ( Anchor[] aa : baseOrMarkAnchors ) { if ( aa != null ) { int nc = aa.length; if ( nc > ncMax ) { ncMax = nc; } } } Anchor[][] am = new Anchor [ na ][ ncMax ]; for ( int i = 0; i < na; i++ ) { Anchor[] aa = baseOrMarkAnchors.get(i); if ( aa != null ) { for ( int j = 0; j < ncMax; j++ ) { if ( j < aa.length ) { am [ i ] [ j ] = aa [ j ]; } } } } baseOrMarkAnchors.clear(); return am; } private Integer computeClassCount ( Anchor[][] am ) { int ncMax = 0; for ( int i = 0, n = am.length; i < n; i++ ) { Anchor[] aa = am [ i ]; if ( aa != null ) { int nc = aa.length; if ( nc > ncMax ) { ncMax = nc; } } } return Integer.valueOf ( ncMax ); } private Anchor[][] extractComponents() { Anchor[][] cam = new Anchor [ components.size() ][]; cam = (Anchor[][]) components.toArray ( new Anchor [ cam.length ][] ); components.clear(); return cam; } private Anchor[][][] extractLigatureAnchors() { int na = ligatureAnchors.size(); int ncMax = 0; int nxMax = 0; for ( Anchor[][] cm : ligatureAnchors ) { if ( cm != null ) { int nx = cm.length; if ( nx > nxMax ) { nxMax = nx; } for ( Anchor[] aa : cm ) { if ( aa != null ) { int nc = aa.length; if ( nc > ncMax ) { ncMax = nc; } } } } } Anchor[][][] lam = new Anchor [ na ] [ nxMax ] [ ncMax ]; for ( int i = 0; i < na; i++ ) { Anchor[][] cm = ligatureAnchors.get(i); if ( cm != null ) { for ( int j = 0; j < nxMax; j++ ) { if ( j < cm.length ) { Anchor[] aa = cm [ j ]; if ( aa != null ) { for ( int k = 0; k < ncMax; k++ ) { if ( k < aa.length ) { lam [ i ] [ j ] [ k ] = aa [ k ]; } } } } } } } ligatureAnchors.clear(); return lam; } private Integer computeLigaturesClassCount ( Anchor[][][] lam ) { int ncMax = 0; if ( lam != null ) { for ( Anchor[][] cm : lam ) { if ( cm != null ) { for ( Anchor[] aa : cm ) { if ( aa != null ) { int nc = aa.length;; if ( nc > ncMax ) { ncMax = nc; } } } } } } return Integer.valueOf ( ncMax ); } private Integer computeLigaturesComponentCount ( Anchor[][][] lam ) { int nxMax = 0; if ( lam != null ) { for ( Anchor[][] cm : lam ) { if ( cm != null ) { int nx = cm.length;; if ( nx > nxMax ) { nxMax = nx; } } } } return Integer.valueOf ( nxMax ); } private Anchor[] extractAttachmentAnchors() { int na = attachmentAnchors.size(); Anchor[] aa = new Anchor [ na * 2 ]; for ( int i = 0; i < na; i++ ) { Anchor[] ea = attachmentAnchors.get(i); int ne = ea.length; if ( ne > 0 ) { aa [ ( i * 2 ) + 0 ] = ea[0]; } if ( ne > 1 ) { aa [ ( i * 2 ) + 1 ] = ea[1]; } } attachmentAnchors.clear(); return aa; } private void addGDEFSubtable ( int stType, GlyphMappingTable mapping ) { subtables.add ( GlyphDefinitionTable.createSubtable ( stType, makeLookupId ( ltSequence ), stSequence, ltFlags, stFormat, mapping, extractSubtableEntries() ) ); nextSubtableInLookup(); } private void addGSUBSubtable ( int stType, GlyphCoverageTable coverage, List entries ) { subtables.add ( GlyphSubstitutionTable.createSubtable ( stType, makeLookupId ( ltSequence ), stSequence, ltFlags, stFormat, coverage, entries ) ); nextSubtableInLookup(); } private void addGSUBSubtable ( int stType, GlyphCoverageTable coverage ) { addGSUBSubtable ( stType, coverage, extractSubtableEntries() ); } private void addGPOSSubtable ( int stType, GlyphCoverageTable coverage, List entries ) { subtables.add ( GlyphPositioningTable.createSubtable ( stType, makeLookupId ( ltSequence ), stSequence, ltFlags, stFormat, coverage, entries ) ); nextSubtableInLookup(); } private void addGPOSSubtable ( int stType, GlyphCoverageTable coverage ) { addGPOSSubtable ( stType, coverage, extractSubtableEntries() ); } } private int mapGlyphId0 ( String glyph ) { assert glyphIds != null; Integer gid = glyphIds.get ( glyph ); if ( gid != null ) { return (int) gid; } else { return -1; } } private int mapGlyphId ( String glyph, String[] currentElement ) throws SAXException { int g = mapGlyphId0 ( glyph ); if ( g < 0 ) { unsupportedGlyph ( currentElement, glyph ); return -1; } else { return g; } } private int[] mapGlyphIds ( String glyphs, String[] currentElement ) throws SAXException { String[] ga = glyphs.split(","); int[] gids = new int [ ga.length ]; int i = 0; for ( String glyph : ga ) { gids[i++] = mapGlyphId ( glyph, currentElement ); } return gids; } private int mapGlyphIdToChar ( String glyph ) { assert glyphIds != null; Integer gid = glyphIds.get ( glyph ); if ( gid != null ) { if ( gmap != null ) { Integer cid = gmap.get ( gid ); if ( cid != null ) { return cid.intValue(); } } } return -1; } private String formatLocator() { if ( locator == null ) { return "{null}"; } else { return "{" + locator.getSystemId() + ":" + locator.getLineNumber() + ":" + locator.getColumnNumber() + "}"; } } private void unsupportedElement ( String[] en ) throws SAXException { throw new SAXException ( formatLocator() + ": unsupported element " + formatExpandedName ( en ) ); } private void notPermittedInElementContext ( String[] en, String[] cn, Object xns ) throws SAXException { assert en != null; assert cn != null; String s = "element " + formatExpandedName(en) + " not permitted in current element context " + formatExpandedName(cn); if ( xns == null ) { s += ", expected root context"; } else if ( xns instanceof String[][] ) { int nxn = 0; s += ", expected one of { "; for ( String[] xn : (String[][]) xns ) { if ( nxn++ > 0 ) { s += ", "; } s += formatExpandedName ( xn ); } s += " }"; } else if ( xns instanceof String[] ) { s += ", expected " + formatExpandedName ( (String[]) xns ); } throw new SAXException ( formatLocator() + ": " + s ); } private void missingRequiredAttribute ( String[] en, String name ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " missing required attribute " + name ); } private void duplicateGlyph ( String[] en, String name, int gid ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " contains duplicate name \"" + name + "\", with identifier value " + gid ); } private void unsupportedGlyph ( String[] en, String name ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " refers to unsupported glyph id \"" + name + "\"" ); } private void duplicateCMAPCharacter ( String[] en, int cid ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " contains duplicate cmap character code: " + CharUtilities.format ( cid ) ); } private void duplicateCMAPGlyph ( String[] en, int gid ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " contains duplicate cmap glyph code: " + gid ); } private void duplicateGlyphClass ( String[] en, String name, String glyphClass ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " contains duplicate glyph class for \"" + name + "\", with class value " + glyphClass ); } private void unsupportedFormat ( String[] en, int format ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " refers to unsupported table format \"" + format + "\"" ); } private void invalidIndex ( String[] en, int actual, int expected ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " specifies invalid index " + actual + ", expected " + expected ); } private void mismatchedIndex ( String[] en, String label, int actual, int expected ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " mismatched " + label + " index: got " + actual + ", expected " + expected ); } private void mismatchedEntries ( String[] en, int nce, int nse ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " mismatched coverage and subtable entry counts, # coverages " + nce + ", # entries " + nse ); } private void missingParameter ( String[] en, String label ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " missing " + label + " parameter" ); } private void duplicateParameter ( String[] en, String label ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " duplicate " + label + " parameter" ); } private void duplicateCoverageIndex ( String[] en, int index ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " duplicate coverage table index " + index ); } private void missingCoverage ( String[] en, String type, int expected ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " missing " + type + " coverage table, expected " + ( ( expected > 0 ) ? expected : 1 ) + " table(s)" ); } private void missingTag ( String[] en, String label ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " missing " + label + " tag" ); } private void duplicateTag ( String[] en, String label, String tag ) throws SAXException { throw new SAXException ( formatLocator() + ": element " + formatExpandedName(en) + " duplicate " + label + " tag: " + tag ); } private static String[] makeExpandedName ( String uri, String localName, String qName ) { if ( ( uri != null ) && ( uri.length() == 0 ) ) { uri = null; } if ( ( localName != null ) && ( localName.length() == 0 ) ) { localName = null; } if ( ( uri == null ) && ( localName == null ) ) { uri = extractPrefix ( qName ); localName = extractLocalName ( qName ); } return new String[] { uri, localName }; } private static String extractPrefix ( String qName ) { String[] sa = qName.split(":"); if ( sa.length == 2 ) { return sa[0]; } else { return null; } } private static String extractLocalName ( String qName ) { String[] sa = qName.split(":"); if ( sa.length == 2 ) { return sa[1]; } else if ( sa.length == 1 ) { return sa[0]; } else { return null; } } private static boolean sameExpandedName ( String[] n1, String[] n2 ) { String u1 = n1[0]; String u2 = n2[0]; if ( ( u1 == null ) ^ ( u2 == null ) ) { return false; } if ( ( u1 != null ) && ( u2 != null ) ) { if ( ! u1.equals ( u2 ) ) { return false; } } String l1 = n1[1]; String l2 = n2[1]; if ( ( l1 == null ) ^ ( l2 == null ) ) { return false; } if ( ( l1 != null ) && ( l2 != null ) ) { if ( ! l1.equals ( l2 ) ) { return false; } } return true; } private static String formatExpandedName ( String[] n ) { String u = ( n[0] != null ) ? n[0] : "null"; String l = ( n[1] != null ) ? n[1] : "null"; return "{" + u + "}" + l; } }