/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* $Id$ */ package org.apache.fop.complexscripts.fonts.ttx; import java.io.File; import java.io.IOException; import java.nio.IntBuffer; import java.util.ArrayList; import java.util.Arrays; 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.xml.sax.Attributes; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fop.complexscripts.fonts.GlyphClassTable; import org.apache.fop.complexscripts.fonts.GlyphCoverageTable; import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; import org.apache.fop.complexscripts.fonts.GlyphMappingTable; import org.apache.fop.complexscripts.fonts.GlyphPositioningTable; import org.apache.fop.complexscripts.fonts.GlyphPositioningTable.Anchor; import org.apache.fop.complexscripts.fonts.GlyphPositioningTable.MarkAnchor; import org.apache.fop.complexscripts.fonts.GlyphPositioningTable.PairValues; import org.apache.fop.complexscripts.fonts.GlyphPositioningTable.Value; import org.apache.fop.complexscripts.fonts.GlyphSubstitutionTable; import org.apache.fop.complexscripts.fonts.GlyphSubstitutionTable.Ligature; import org.apache.fop.complexscripts.fonts.GlyphSubstitutionTable.LigatureSet; import org.apache.fop.complexscripts.fonts.GlyphSubtable; import org.apache.fop.complexscripts.fonts.GlyphTable; import org.apache.fop.complexscripts.fonts.GlyphTable.RuleLookup; import org.apache.fop.complexscripts.scripts.ScriptProcessor; import org.apache.fop.complexscripts.util.GlyphSequence; import org.apache.fop.complexscripts.util.UTF32; import org.apache.fop.util.CharUtilities; // CSOFF: LineLengthCheck /** * This class supports a subset of the <code>TTX</code> file as produced by the Adobe FLEX * SDK (AFDKO). In particular, it is used to parse a <code>TTX</code> file in order to * extract character to glyph code mapping data, glyph definition data, glyph substitution * data, and glyph positioning data. * * <code>TTX</code> 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, <code>TTX</code> files are used * to distribute a subset of the complex script advanced table information contained in * certain font files to facilitate testing. */ 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<String, TTXFile> cache = new HashMap<String, TTXFile>(); // transient parsing state private Locator locator; // current document locator private Stack<String[]> elements; // stack of ttx elements being parsed private Map<String, Integer> glyphIds; // map of glyph names to glyph identifiers private List<int[]> cmapEntries; // list of <charCode,glyphCode> pairs private Vector<int[]> hmtxEntries; // vector of <width,lsb> pairs private Map<String, Integer> glyphClasses; // map of glyph names to glyph classes private Map<String, Map<String, List<String>>> scripts; // map of script tag to Map<language-tag,List<features-id>>> private Map<String, List<String>> languages; // map of language tag to List<feature-id> private Map<String, Object[]> features; // map of feature id to Object[2] : { feature-tag, List<lookup-id> } private List<String> languageFeatures; // list of language system feature ids, where first is (possibly null) required feature id private List<String> featureLookups; // list of lookup ids for feature being constructed private List<Integer> coverageEntries; // list of entries for coverage table being constructed private Map<String, GlyphCoverageTable> coverages; // map of coverage table keys to coverage tables private List subtableEntries; // list of lookup subtable entries private List<GlyphSubtable> subtables; // list of constructed subtables private List<Integer> alternates; // list of alternates in alternate set being constructed private List<Ligature> ligatures; // list of ligatures in ligature set being constructed private List<Integer> substitutes; // list of substitutes in (multiple substitution) sequence being constructed private List<PairValues> pairs; // list of pair value records being constructed private List<PairValues[]> pairSets; // list of pair value sets (as arrays) being constructed private List<Anchor> anchors; // list of anchors of base|mark|component record being constructed private List<Anchor[]> components; // list of ligature component anchors being constructed private List<MarkAnchor> markAnchors; // list of mark anchors being constructed private List<Anchor[]> baseOrMarkAnchors; // list of base|mark2 anchors being constructed private List<Anchor[][]> ligatureAnchors; // list of ligature anchors being constructed private List<Anchor[]> attachmentAnchors; // list of entry|exit attachment anchors being constructed private List<RuleLookup> 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<Integer, Integer> cmap; // constructed character map private Map<Integer, Integer> 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 private Map<String, ScriptProcessor> processors = new HashMap<String, ScriptProcessor>(); public TTXFile() { elements = new Stack<String[]>(); glyphIds = new HashMap<String, Integer>(); cmapEntries = new ArrayList<int[]>(); hmtxEntries = new Vector<int[]>(); glyphClasses = new HashMap<String, Integer>(); scripts = new HashMap<String, Map<String, List<String>>>(); languages = new HashMap<String, List<String>>(); features = new HashMap<String, Object[]>(); languageFeatures = new ArrayList<String>(); featureLookups = new ArrayList<String>(); coverageEntries = new ArrayList<Integer>(); coverages = new HashMap<String, GlyphCoverageTable>(); subtableEntries = new ArrayList(); subtables = new ArrayList<GlyphSubtable>(); alternates = new ArrayList<Integer>(); ligatures = new ArrayList<Ligature>(); substitutes = new ArrayList<Integer>(); pairs = new ArrayList<PairValues>(); pairSets = new ArrayList<PairValues[]>(); anchors = new ArrayList<Anchor>(); markAnchors = new ArrayList<MarkAnchor>(); baseOrMarkAnchors = new ArrayList<Anchor[]>(); ligatureAnchors = new ArrayList<Anchor[][]>(); components = new ArrayList<Anchor[]>(); attachmentAnchors = new ArrayList<Anchor[]>(); ruleLookups = new ArrayList<RuleLookup>(); 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 = UTF32.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(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 final 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(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(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(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(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(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(igid); subtableEntries.add(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, processors); } clearTable(); } else if (en[1].equals("GPOS")) { if (subtables.size() > 0) { gpos = new GlyphPositioningTable(gdef, extractLookups(), subtables, processors); } clearTable(); } else if (en[1].equals("GSUB")) { if (subtables.size() > 0) { gsub = new GlyphSubstitutionTable(gdef, extractLookups(), subtables, processors); } 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 { return ((en[0] == null) && (en[1] == null)); } } 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 { return ln.equals("Mark2Anchor"); } } private Map<Integer, Integer> getCMAP() { Map<Integer, Integer> cmap = new TreeMap(); for (int[] cme : cmapEntries) { Integer c = cme[0]; Integer g = cme[1]; cmap.put(c, g); } return cmap; } private Map<Integer, Integer> getGMAP() { Map<Integer, Integer> gmap = new TreeMap(); for (int[] cme : cmapEntries) { Integer c = cme[0]; Integer g = 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<String, Integer> 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<Integer>(); int s = -1; int l = -1; Integer zero = 0; for (int[] m : cma) { int g = m[0]; int c = m[1]; if (s < 0) { s = g; l = g - 1; entries.add(s); } while (g > (l + 1)) { entries.add(zero); l++; } assert l == (g - 1); entries.add(c); l = g; } return GlyphClassTable.createClassTable(entries); } private GlyphClassTable extractClassDefMapping2(int[][] cma) { List entries = new ArrayList<Integer>(); 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<String, Integer> glyphClasses, boolean clearSourceMap) { int nc = glyphClasses.size(); int i = 0; int[][] cma = new int [ nc ] [ 2 ]; for (Map.Entry<String, Integer> 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<int[]>() { 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<Object[]>() { 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<Integer>(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<String> 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 (Map.Entry<String, GlyphCoverageTable> stringGlyphCoverageTableEntry : coverages.entrySet()) { if (stringGlyphCoverageTableEntry.getKey().startsWith(prefix)) { int i = Integer.parseInt(stringGlyphCoverageTableEntry.getKey().substring(prefixLength)); if (i >= 0) { gca [ i ] = stringGlyphCoverageTableEntry.getValue(); } } } return gca; } private boolean hasMissingCoverage(GlyphCoverageTable[] gca) { assert gca != null; int nc = 0; for (GlyphCoverageTable aGca : gca) { if (aGca != 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<String> extractLanguageFeatures() { List<String> lfl = new ArrayList<String>(languageFeatures); clearLanguageFeatures(); return lfl; } private void assertLanguageFeaturesClear() { assert languageFeatures.size() == 0; } private void clearLanguageFeatures() { languageFeatures.clear(); } private Map<String, List<String>> extractLanguages() { Map<String, List<String>> 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<String>(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<GlyphTable.LookupSpec, List<String>> extractLookups() { Map<GlyphTable.LookupSpec, List<String>> lookups = new LinkedHashMap<GlyphTable.LookupSpec, List<String>>(); for (Map.Entry<String, Map<String, List<String>>> stringMapEntry : scripts.entrySet()) { Map<String, List<String>> lm = stringMapEntry.getValue(); if (lm != null) { for (Map.Entry<String, List<String>> stringListEntry : lm.entrySet()) { List<String> fids = stringListEntry.getValue(); 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<String> lids = (List<String>) fa[1]; if ((lids != null) && (lids.size() > 0)) { GlyphTable.LookupSpec ls = new GlyphTable.LookupSpec(stringMapEntry.getKey(), stringListEntry.getKey(), 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 (Anchor[] aa : am) { if (aa != null) { int nc = aa.length; if (nc > ncMax) { ncMax = nc; } } } return 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 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 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; } } } 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; } }